From 35603c95dcdd89f867f65c2f933a9ab41332d2cd Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 19 Feb 2026 09:22:46 +0100 Subject: [PATCH 01/11] fix: remove duplicate button style --- ui/button.mod/button copy.css | 126 ++++++++++++++++------------------ 1 file changed, 61 insertions(+), 65 deletions(-) diff --git a/ui/button.mod/button copy.css b/ui/button.mod/button copy.css index e90cf9d4f..399676be5 100644 --- a/ui/button.mod/button copy.css +++ b/ui/button.mod/button copy.css @@ -1,68 +1,64 @@ @layer Mod { - -@scope (.Mod-Button) { - - - :scope { - border: none; - display: inline-flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - font-weight: 600; - position: relative; - - /* FIXME: Temporary arbitrary values, waiting for Button Themes */ - transition: background-color .2s ease-in-out; - gap: 6px; - } - - :hover { - cursor: pointer; - } - - /* Main Orientation */ - - .mod--vertical { - flex-direction: column; - } - - .mod--horizontal { - flex-direction: row; - } - - /* Main Axis Direction */ - - .mod--horizontal.mod--end { - flex-direction: row-reverse; - } - - .mod--vertical.mod--end { - flex-direction: column-reverse; - } - - /* Hide Empty Elements */ - >div:empty { - display: none; - } - - /* Image Specifications */ - - >.ModButton-image { - position: relative; - } - - >svg.ModButton-image { - fill: currentColor; - } - - /* Pending State */ - - .mod--pending { - pointer-events: none; - /* FIXME: Temporary arbitrary values, waiting for Button Themes */ - opacity: 0.75; - } - + @scope (.Mod-Button) { + :scope { + border: none; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + font-weight: 600; + position: relative; + + /* FIXME: Temporary arbitrary values, waiting for Button Themes */ + transition: background-color 0.2s ease-in-out; + gap: 6px; + } + + :hover { + cursor: pointer; + } + + /* Main Orientation */ + + .mod--vertical { + flex-direction: column; + } + + .mod--horizontal { + flex-direction: row; + } + + /* Main Axis Direction */ + + .mod--horizontal.mod--end { + flex-direction: row-reverse; + } + + .mod--vertical.mod--end { + flex-direction: column-reverse; + } + + /* Hide Empty Elements */ + > div:empty { + display: none; + } + + /* Image Specifications */ + + > .ModButton-image { + position: relative; + } + + > svg.ModButton-image { + fill: currentColor; + } + + /* Pending State */ + + .mod--pending { + pointer-events: none; + /* FIXME: Temporary arbitrary values, waiting for Button Themes */ + opacity: 0.75; + } } } From 2d8895e2f752b03d6410ffc06af8a0709850b88f Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 19 Feb 2026 09:24:21 +0100 Subject: [PATCH 02/11] feat: add scoping app --- examples/scoping-app/index.html | 20 ++++++++++++ examples/scoping-app/package.json | 10 ++++++ examples/scoping-app/styles.css | 8 +++++ examples/scoping-app/ui/circle.mod/circle.css | 20 ++++++++++++ .../scoping-app/ui/circle.mod/circle.html | 24 ++++++++++++++ examples/scoping-app/ui/circle.mod/circle.js | 6 ++++ examples/scoping-app/ui/main.mod/main.css | 32 +++++++++++++++++++ examples/scoping-app/ui/main.mod/main.html | 30 +++++++++++++++++ examples/scoping-app/ui/main.mod/main.js | 6 ++++ index.html | 1 + 10 files changed, 157 insertions(+) create mode 100644 examples/scoping-app/index.html create mode 100644 examples/scoping-app/package.json create mode 100644 examples/scoping-app/styles.css create mode 100644 examples/scoping-app/ui/circle.mod/circle.css create mode 100644 examples/scoping-app/ui/circle.mod/circle.html create mode 100644 examples/scoping-app/ui/circle.mod/circle.js create mode 100644 examples/scoping-app/ui/main.mod/main.css create mode 100644 examples/scoping-app/ui/main.mod/main.html create mode 100644 examples/scoping-app/ui/main.mod/main.js diff --git a/examples/scoping-app/index.html b/examples/scoping-app/index.html new file mode 100644 index 000000000..21786e9f2 --- /dev/null +++ b/examples/scoping-app/index.html @@ -0,0 +1,20 @@ + + + + Scoping App Demo + + + + + + + + + + diff --git a/examples/scoping-app/package.json b/examples/scoping-app/package.json new file mode 100644 index 000000000..81fffb11f --- /dev/null +++ b/examples/scoping-app/package.json @@ -0,0 +1,10 @@ +{ + "name": "scoping-app", + "version": "1.0.0", + "dependencies": { + "mod": "*" + }, + "mappings": { + "mod": "../../" + } +} diff --git a/examples/scoping-app/styles.css b/examples/scoping-app/styles.css new file mode 100644 index 000000000..a8ac95c34 --- /dev/null +++ b/examples/scoping-app/styles.css @@ -0,0 +1,8 @@ +body { + font-family: system-ui, sans-serif; + background-color: #1a1a1a; + color: #eee; + height: 100vh; + margin: 0; + display: flex; +} diff --git a/examples/scoping-app/ui/circle.mod/circle.css b/examples/scoping-app/ui/circle.mod/circle.css new file mode 100644 index 000000000..e53347aad --- /dev/null +++ b/examples/scoping-app/ui/circle.mod/circle.css @@ -0,0 +1,20 @@ +.Circle { + display: flex; + justify-content: center; + align-items: center; + width: 92cqi; + height: 92cqi; + border-radius: 50%; + background-color: lightblue; + border: 4cqi solid white; + + .inner { + width: 35cqi; + height: 35cqi; + background-color: white; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + } +} diff --git a/examples/scoping-app/ui/circle.mod/circle.html b/examples/scoping-app/ui/circle.mod/circle.html new file mode 100644 index 000000000..7eb7da0bb --- /dev/null +++ b/examples/scoping-app/ui/circle.mod/circle.html @@ -0,0 +1,24 @@ + + + + Montage Hello + + + + +
+
+ Circle +
+
+ + diff --git a/examples/scoping-app/ui/circle.mod/circle.js b/examples/scoping-app/ui/circle.mod/circle.js new file mode 100644 index 000000000..48873801d --- /dev/null +++ b/examples/scoping-app/ui/circle.mod/circle.js @@ -0,0 +1,6 @@ +/** + * @module "ui/circle.mod" + */ +const Component = require("mod/ui/component").Component; + +exports.Circle = class Circle extends Component {}; diff --git a/examples/scoping-app/ui/main.mod/main.css b/examples/scoping-app/ui/main.mod/main.css new file mode 100644 index 000000000..db767d31c --- /dev/null +++ b/examples/scoping-app/ui/main.mod/main.css @@ -0,0 +1,32 @@ +.Main { + color: #eee; + padding: 32px 24px; + display: flex; + flex-direction: column; + align-items: center; + gap: 32px; + flex: 1; + + .circles { + display: flex; + gap: 16px; + flex: 1; + + .inner { + /* SHOULD NOT BE APPLIED CHILDREN CIRCLES */ + border: 4px solid black; + } + + > .CircleCssContainer { + width: 150px; + height: 150px; + } + + .Circle { + height: 150px; + width: 150px; + background-color: white; + border-radius: 50%; + } + } +} diff --git a/examples/scoping-app/ui/main.mod/main.html b/examples/scoping-app/ui/main.mod/main.html new file mode 100644 index 000000000..79c27b63b --- /dev/null +++ b/examples/scoping-app/ui/main.mod/main.html @@ -0,0 +1,30 @@ + + + + + + + +
+

Scoping App

+
+
+
+
+
+ + diff --git a/examples/scoping-app/ui/main.mod/main.js b/examples/scoping-app/ui/main.mod/main.js new file mode 100644 index 000000000..055d6f625 --- /dev/null +++ b/examples/scoping-app/ui/main.mod/main.js @@ -0,0 +1,6 @@ +/** + * @module "ui/main.mod" + */ +const Component = require("mod/ui/component").Component; + +exports.Main = class Main extends Component {}; diff --git a/index.html b/index.html index b81359a87..1337a295f 100644 --- a/index.html +++ b/index.html @@ -68,6 +68,7 @@

Tests:

Examples:

Data App Responsive App + Scoping App From d987749914f3b9488f4ee16f7010146e14459445 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 19 Feb 2026 10:36:41 +0100 Subject: [PATCH 03/11] refactor: simplify CSS layering --- core/document-resources.js | 186 ++++++++++++++----------------------- ui/component.js | 179 ++++++++++++++++++++++------------- 2 files changed, 187 insertions(+), 178 deletions(-) diff --git a/core/document-resources.js b/core/document-resources.js index 1c39f2243..9dd5ff5a2 100644 --- a/core/document-resources.js +++ b/core/document-resources.js @@ -15,6 +15,7 @@ exports.DocumentResources = class DocumentResources extends Montage { static { Montage.defineProperties(this.prototype, { + enclosesComponentStylesheetsInCSSLayer: { value: true }, domain: { value: global.location?.origin ?? "" }, _isPollingDocumentStyleSheets: { value: false }, _SCRIPT_TIMEOUT: { value: 5_000 }, @@ -22,21 +23,6 @@ exports.DocumentResources = class DocumentResources extends Montage { _resources: { value: null }, _preloaded: { value: null }, _document: { value: null }, - - // Scope and Layering configuration - /** - * #WARNING - EXPERIMENTAL if true, it will trigger the use of the _scopeStylesheetRulesWithSelectorInCSSLayerName() method - * above to wrap an component's CSS into a @scope rule. modifying selectors such that they work within the new @scope, meaning - * using pseudo selector :scope as necessary. - * - * This works in some limited use cases and would need a lot more subtlety to be robust, reliable - * and useful - * - * @property {boolean} - */ - automaticallyAddsCSSLayerToUnscoppedCSS: { value: true }, - _scopeSelectorRegExp: { value: /scope\(([^()]*)\)/g }, - automaticallyAddsCSSScope: { value: false }, }); } @@ -125,85 +111,15 @@ exports.DocumentResources = class DocumentResources extends Montage { if (index >= 0) { this._expectedStyles.splice(index, 1); - const cssContext = this.cssContextForResource(target.href); - const classListScope = cssContext.classListScope; - const cssLayerName = cssContext.cssLayerName; - const stylesheet = target.sheet; - const cssRules = stylesheet.cssRules; - - /** - * Adding CSS Layers, and Scoping for components in dev mode. - * When we mop, we'll add it in the CSS. - * - * target.ownerDocument is the page's document. - * We captured the Component's element's classes before we got here, in this._resources[target.href] - * - * @scope (.ComponentElementClass1.ComponentElementClass2) { - * -> All Component's CSS file's rules needs to be relocated here <- - * } - * - * target.ownerDocument.styleSheets, but we need the component's element's classList - */ - - if (classListScope && stylesheet.disabled === false && typeof CSSScopeRule === "function") { - let iStart = 0; - - // Insert the scope rule, after any CSSImportRule - while (cssRules[iStart] instanceof CSSImportRule) { - iStart++; - } - // If it's not using CSS Layers - if (!(cssRules[iStart] instanceof CSSLayerBlockRule)) { - // If it's not using CSSScope - if (!(cssRules[iStart] instanceof CSSScopeRule) && this.automaticallyAddsCSSScope) { - this._scopeStylesheetRulesWithSelectorInCSSLayerName( - stylesheet, - classListScope, - cssLayerName, - ); - } else if (cssRules[iStart] instanceof CSSScopeRule) { - // Add the layer name in scope - const scopeSelectorRegExp = this._scopeSelectorRegExp; - const scopeRule = stylesheet.cssRules[iStart]; - const scopeRuleCSSText = scopeRule.cssText; - let scopeSelector; - let match; - - // Delete current scopeRule - stylesheet.deleteRule(iStart); - - while ((match = scopeSelectorRegExp.exec(scopeRuleCSSText)) !== null) { - scopeSelector = `.${cssLayerName}${match[1]}`; - scopeRuleCSSText = scopeRuleCSSText.replace(match[1], scopeSelector); - } - - stylesheet.insertRule(scopeRuleCSSText); - } - - let scopeRule = stylesheet.cssRules[iStart]; - - // If the CSS is scoped, we move it into the CSSLayerBlockRule - if (scopeRule && scopeRule instanceof CSSScopeRule) { - stylesheet.insertRule(`@layer ${cssLayerName} {}`, iStart); - let packageLayer = stylesheet.cssRules[iStart]; - - scopeRule = stylesheet.cssRules[++iStart]; - - stylesheet.deleteRule(iStart); - packageLayer.insertRule(scopeRule.cssText); - } else if (this.automaticallyAddsCSSLayerToUnscoppedCSS) { - stylesheet.insertRule(`@layer ${cssLayerName} {}`, iStart); - let packageLayer = stylesheet.cssRules[iStart]; - - // We layer all rules - for (let i = cssRules.length - 1; i > iStart; i--) { - packageLayer.insertRule(cssRules[i].cssText); - stylesheet.deleteRule(i); - } - } - } + if (cssContext && typeof cssContext === "object") { + const stylesheet = target.sheet; + + // Adding CSS Layers, and Scoping for components in dev mode. + // When we mop, we'll add it in the CSS. + // @benoit: do we have a flag for dev mode? + this._wrapStyleSheetInLayer(stylesheet, cssContext); } } @@ -212,7 +128,7 @@ exports.DocumentResources = class DocumentResources extends Montage { } } - addStyle(element, DOMParent, classListScope, cssLayerName) { + addStyle(element, DOMParent, context) { let url = element.getAttribute("href"); if (url) { @@ -220,7 +136,7 @@ exports.DocumentResources = class DocumentResources extends Montage { if (this.hasResource(url)) return; - this._addResource(url, classListScope, cssLayerName); + this._addResource(url, context); this._expectedStyles.push(url); if (!this._isPollingDocumentStyleSheets) { @@ -385,34 +301,72 @@ exports.DocumentResources = class DocumentResources extends Montage { return promise; } - _addResource(url, classListScope, cssLayerName) { - this._resources[url] = { classListScope, cssLayerName }; + /** + * Registers a resource with its associated CSS context. + * + * @param {string} url The URL of the resource. + */ + _addResource(url, context = {}) { + this._resources[url] = context; } - _scopeStylesheetRulesWithSelectorInCSSLayerName(stylesheet, classListScope, cssLayerName) { - if (classListScope && stylesheet.disabled === false && typeof CSSScopeRule === "function") { - const classListScopeRegexp = new RegExp(`(${classListScope})+(?=$)|(${classListScope})+(?= >)`, "dg"); - const classListScopeContentRegexp = new RegExp(`(${classListScope})+(?=[.,:,\s,>]|$)`, "dg"); - const cssRules = stylesheet.cssRules; - let iStart = 0; - - // Insert the scope rule, but after any CSSImportRule - while (cssRules[iStart] instanceof CSSImportRule) { - iStart++; + /** + * Modifies an existing CSSStyleSheet in-place to wrap it in a scoped layer structure. + * + * @param {CSSStyleSheet} sheet - The existing CSSStyleSheet to modify. + * @param {Object} cssContext - The CSS context. + * @returns {CSSStyleSheet} The modified stylesheet instance. + */ + _wrapStyleSheetInLayer(sheet, cssContext) { + // Validate requirements for scoping and layering + if (!CSSLayerBlockRule || !CSSScopeRule || sheet.disabled) return; + + try { + const { moduleLayerClassName, moduleLayerPath } = cssContext; + const rulesToWrap = []; + let insertionIndex = 0; + + // Iterate backwards so deleting rules doesn't shift indices of unvisited rules + for (let i = sheet.cssRules.length - 1; i >= 0; i--) { + const rule = sheet.cssRules[i]; + + const isImport = rule instanceof CSSImportRule; + const isLayer = rule instanceof CSSLayerBlockRule || rule instanceof CSSLayerStatementRule; + const isRoot = rule instanceof CSSStyleRule && rule.selectorText?.startsWith(":root"); + + if (!isImport && !isLayer && !isRoot) { + // Unshift to maintain the original top-to-bottom order + rulesToWrap.unshift(rule.cssText); + sheet.deleteRule(i); + insertionIndex = i; + } } - stylesheet.insertRule(`@scope (.${cssLayerName}${classListScope}) {}`, iStart); - const scopeRule = cssRules[iStart]; + if (rulesToWrap.length === 0) return sheet; - // Now loop on rules to move - re-create them as there's no other way :-( - for (let i = cssRules.length - 1; i > iStart; i--) { - cssRules[i].selectorText = cssRules[i].selectorText - .replaceAll(classListScopeRegexp, ":scope") - .replaceAll(classListScopeContentRegexp, ""); + const extractedCssText = rulesToWrap.join("\n"); - scopeRule.insertRule(cssRules[i].cssText); - stylesheet.deleteRule(i); - } + // Create the new wrapped CSS string + const wrappedCss = `@layer ${moduleLayerPath} { + @scope (.${moduleLayerClassName}) { + :scope, * { all: revert-layer !important; } + } + + @layer style { + * { + all: revert; + } + + ${extractedCssText} + } + }`; + + // Insert the new wrapped CSS into the existing stylesheet + sheet.insertRule(wrappedCss, insertionIndex); + } catch (error) { + console.error("Unable to wrap scoped stylesheet (likely cross-origin)", error); } + + return sheet; } }; diff --git a/ui/component.js b/ui/component.js index 7fdba3be6..0eca216c2 100644 --- a/ui/component.js +++ b/ui/component.js @@ -33,6 +33,8 @@ var Montage = require("../core/core").Montage, currentEnvironment = require("core/environment").currentEnvironment, PropertyChanges = require("core/collections/listen/property-changes"); +const kebabCaseConverter = require("../core/converter/kebab-case-converter").singleton; + /* For supporting Live Edit in apps connected to the studio, the connected app sets the global: _montage_le_flag = true; @@ -798,6 +800,7 @@ Component.addClassProperties({ this.blockDrawGate.setField("element", true); } } + this._initializeClassListFromElement(value); }, }, @@ -2683,7 +2686,7 @@ Component.addClassProperties({ _addTemplateStylesIfNeeded: { value: function () { if (this._templateDocumentPart) { - this.rootComponent.addStyleSheetsFromTemplate(this._templateDocumentPart.template, this.packageName); + this.rootComponent.addStyleSheetsFromTemplate(this._templateDocumentPart.template, this); } }, }, @@ -5042,90 +5045,148 @@ var RootComponent = Component.specialize( * @private * Those 3 arrays keep related data at the same index */ - _stylesheets: { - value: [], + _stylesheetContexts: { + value: new Map(), }, + _cssLayerNames: { - value: [], + value: new Set(), }, - _stylesheetsclassListScopes: { - value: [], + + _addedStyleSheetsByTemplate: { + value: null, + }, + + extensionModuleIdRegex: { + value: /\.(mod|reel|js|css|ts)$/i, + }, + + moduleLayerNameRegex: { + value: /[^a-z0-9\-]+/gi, + }, + + trimDotsRegex: { + value: /(^\.+|\.+$)/g, + }, + + enclosesComponentStylesheetsInCSSLayer: { + get: function () { + return this._documentResources.enclosesComponentStylesheetsInCSSLayer; + }, }, /** * @function */ - addStylesheetWithClassListScopeInCSSLayerName: { - value: function (style, classListScope, cssLayerName) { - this._stylesheets.push(style); - this._stylesheetsclassListScopes.push( - classListScope ? `.${this._stylesheets.join.call(classListScope, ".")}` : undefined, - ); - this._cssLayerNames.push(cssLayerName); + registerComponentStyle: { + value: function (component, stylesheetElement) { + const moduleId = component.fullModuleId.trim().toLowerCase().replace(this.extensionModuleIdRegex, ""); + const moduleLayerClassName = kebabCaseConverter.convert(moduleId); + + const moduleLayerPath = moduleId + .replace(this.moduleLayerNameRegex, ".") + .replace(this.trimDotsRegex, ""); + + const layerNames = moduleLayerPath.split("."); + + let layersHaveChanged = false; + const previousSize = this._cssLayerNames.size; + + for (const layerName of layerNames) { + if (!this._cssLayerNames.has(layerName)) { + this._cssLayerNames.add(layerName); + } + } + + layersHaveChanged = this._cssLayerNames.size > previousSize; + + if (layersHaveChanged && this.enclosesComponentStylesheetsInCSSLayer) { + this._updateCssLayerOrder(); + } + + this._stylesheetContexts.set(stylesheetElement, { + moduleLayerClassName, + moduleLayerPath, + }); + + component.classList.add(moduleLayerClassName); this._needsStylesheetsDraw = true; }, }, - _addedStyleSheetsByTemplate: { - value: null, - }, - _addCssLayerOrder: { + /** + * Updates the @layer statement in the head. + */ + _updateCssLayerOrder: { value: function () { - let cssLayers = global.require.modDependencies(); - (cssLayers.push(global.require.config.name), (styleElement = document.createElement("style"))); + this._addCssLayerOrderElementIfNeeded(); - styleElement.textContent = `@layer ${cssLayers - .map((layer) => { - return layer.replace(".", "_"); - }) - .join(", ")};`; + const layerList = Array.from(this._cssLayerNames); - styleElement.setAttribute("data-mod-id", "mod-layer-statement"); + // TODO: @Benoit probably not the best way of doing it, maybe we should throttle it... + this._cssLayerOrderElement.textContent = `@layer ${layerList.join(", ")};`; - document.head.firstChild.before(styleElement); - return styleElement; + return this._cssLayerOrderElement; }, }, - addStyleSheetsFromTemplate: { - value: function (template, cssLayerName) { - if (this._documentResources.automaticallyAddsCSSLayerToUnscoppedCSS && !this._cssLayerOrderElement) { - this._cssLayerOrderElement = - document.querySelector('[data-mod-id="mod-layer-statement"]') || this._addCssLayerOrder(); + _addCssLayerOrderElementIfNeeded: { + value: function () { + if (!this.enclosesComponentStylesheetsInCSSLayer || !!this._cssLayerOrderElement) { + return false; } - cssLayerName = cssLayerName.replace(".", "_"); - if (!this._addedStyleSheetsByTemplate.has(template)) { - var resources = template.getResources(), - ownerDocument = this.element.ownerDocument, - styles = resources.createStylesForDocument(ownerDocument), - componentElementClassList = template.document.querySelector("body > [data-mod-id]")?.classList; + const cssLayers = global.require.modDependencies(); + const styleElement = document.createElement("style"); + cssLayers.push(global.require.config.name); - /* - What we need to scope is a selector made of all the classes of the template's root element + for (let i = 0; i < cssLayers.length; i++) { + const cleanId = cssLayers[i].trim().toLowerCase().replace(this.extensionModuleIdRegex, ""); + this._cssLayerNames.add(cleanId); + } - template.document.querySelector("[data-mod-id=owner]").classList + styleElement.setAttribute("data-mod-id", "mod-layer-statement"); - */ + if (document.head.firstChild) { + document.head.firstChild.before(styleElement); + } else { + document.head.appendChild(styleElement); + } + + this._cssLayerOrderElement = styleElement; + + return true; + }, + }, + + addStyleSheetsFromTemplate: { + value: function (template, component) { + if (!this._addedStyleSheetsByTemplate.has(template)) { + const resources = template.getResources(); + const styles = resources.createStylesForDocument(this.element.ownerDocument); + + /** + * What we need to scope is a selector made of all the classes of the template's root element + * template.document.querySelector("[data-mod-id=owner]").classList + */ for (var i = 0, style; (style = styles[i]); i++) { - /* - Flow is one component where the owner's element doesn't have data-mod-id="owner", but data-mod-id="montage-flow". - So to avoid that we'll consider the root element the first direct child of the body that has a data-mod-id attribute - */ - this.addStylesheetWithClassListScopeInCSSLayerName( - style, - componentElementClassList, - cssLayerName, - ); + /** + * Flow is one component where the owner's element doesn't have data-mod-id="owner", but data-mod-id="montage-flow". + * So to avoid that we'll consider the root element the first direct child of the body that has a data-mod-id attribute + */ + this.registerComponentStyle(component, style); } + this._addedStyleSheetsByTemplate.set(template, true); } }, }, + __bufferDocumentFragment: { value: null, }, + _bufferDocumentFragment: { get: function () { return ( @@ -5140,22 +5201,16 @@ var RootComponent = Component.specialize( drawStylesheets: { value: function () { var documentResources = this._documentResources, - stylesheets = this._stylesheets, - stylesheetsclassListScopes = this._stylesheetsclassListScopes, - cssLayerNames = this._cssLayerNames, - stylesheet, + stylesheetContexts = this._stylesheetContexts, documentHead = documentResources._document.head, bufferDocumentFragment = this._bufferDocumentFragment; - while ((stylesheet = stylesheets.shift())) { - documentResources.addStyle( - stylesheet, - bufferDocumentFragment, - stylesheetsclassListScopes.shift(), - cssLayerNames.shift(), - ); + for (const [stylesheetElement, context] of stylesheetContexts.entries()) { + documentResources.addStyle(stylesheetElement, bufferDocumentFragment, context); } + stylesheetContexts.clear(); + /* Add all stylesheets after the CSS layer statement in the DOM to ensure the order defined in the statement is honored. From cbf012911c0591170e3d864b6047902bf725fcb2 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 19 Feb 2026 10:36:53 +0100 Subject: [PATCH 04/11] feat: add style extras module for automatic !important enforcement --- core/core.js | 1 + core/extras/style.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 core/extras/style.js diff --git a/core/core.js b/core/core.js index 00c08240d..3d38a5b57 100644 --- a/core/core.js +++ b/core/core.js @@ -359,6 +359,7 @@ require("./shim/array"); require("./extras/object"); // require("./extras/date"); require("./extras/element"); +require("./extras/style"); require("./extras/function"); require("./extras/map"); require("./extras/regexp"); diff --git a/core/extras/style.js b/core/extras/style.js new file mode 100644 index 000000000..15fe5f1fb --- /dev/null +++ b/core/extras/style.js @@ -0,0 +1,31 @@ +if (!window.__mod__styleImportantForcerInitialized) { + window.__mod__styleImportantForcerInitialized = true; + + const styleObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + // We only care about style attribute changes + if (mutation.attributeName !== "style") return; + + const el = mutation.target; + + // Iterate through all CSS properties currently applied to the element inline + for (let i = 0; i < el.style.length; i++) { + const propName = el.style[i]; + const priority = el.style.getPropertyPriority(propName); + + // If the property doesn't have the '!important' flag, force it + if (priority !== "important") { + const value = el.style.getPropertyValue(propName); + el.style.setProperty(propName, value, "important"); + } + } + } + }); + + styleObserver.observe(document.documentElement, { + attributes: true, + // Only listen for 'style' changes to save performance + attributeFilter: ["style"], + subtree: true, + }); +} From 23e0af8aa4c11667a4477a0c241c40dbfa26dbbb Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 19 Feb 2026 10:38:39 +0100 Subject: [PATCH 05/11] attempt to fix teaches --- ui/button.mod/teach/index.css | 10 + ui/button.mod/teach/index.html | 30 +-- ui/button.mod/teach/package.json | 2 +- ui/button.mod/teach/ui/main.mod/main.css | 71 ++----- ui/button.mod/teach/ui/main.mod/main.html | 20 +- .../segmented-control.css | 176 +++++++++--------- ui/segmented-control.mod/teach/index.css | 7 + ui/segmented-control.mod/teach/index.html | 3 +- .../teach/ui/main.mod/main.css | 8 +- 9 files changed, 159 insertions(+), 168 deletions(-) create mode 100644 ui/button.mod/teach/index.css create mode 100644 ui/segmented-control.mod/teach/index.css diff --git a/ui/button.mod/teach/index.css b/ui/button.mod/teach/index.css new file mode 100644 index 000000000..aa39b57d3 --- /dev/null +++ b/ui/button.mod/teach/index.css @@ -0,0 +1,10 @@ +html, +body { + padding: 0; + margin: 0; + height: 100%; + width: 100%; + display: flex; + -webkit-font-smoothing: antialiased; + font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; +} diff --git a/ui/button.mod/teach/index.html b/ui/button.mod/teach/index.html index 82ac0a706..2b9637e7a 100644 --- a/ui/button.mod/teach/index.html +++ b/ui/button.mod/teach/index.html @@ -1,19 +1,19 @@ - + - - + + - Button Sample + Button Sample - - - - - + + + + + diff --git a/ui/button.mod/teach/package.json b/ui/button.mod/teach/package.json index 5735dbd03..38bf4b672 100644 --- a/ui/button.mod/teach/package.json +++ b/ui/button.mod/teach/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "mod": "*" - }, + }, "mappings": { "mod": "../../../" } diff --git a/ui/button.mod/teach/ui/main.mod/main.css b/ui/button.mod/teach/ui/main.mod/main.css index a3d6c30db..a79908110 100644 --- a/ui/button.mod/teach/ui/main.mod/main.css +++ b/ui/button.mod/teach/ui/main.mod/main.css @@ -1,59 +1,28 @@ -html, body, .Main { - padding: 0; - margin: 0; - height: 100%; - width: 100%; - font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; -} - -.Main, .buttons { +.Main { display: flex; align-items: center; flex-direction: column; + padding: 24px; flex: 1; - -webkit-font-smoothing: antialiased; -} + gap: 32px; -header { - margin-top: 20px; - font-size: 42px; - font-weight: bold; - color: #333; -} - -.buttons { - height: 100%; - width: 100%; -} + header { + font-size: 42px; + font-weight: bold; + color: #333; + } -.button { - height: 80px; - width: 200px; - font-size: 15px; - box-sizing: border-box; - padding: 0; - margin: 0; - border: 1px solid #AAA; - color: #666; - background: white; - border-radius: 4px; - margin-top: 40px; - text-transform: capitalize; - user-select: none; -} - -.button.mod--active { - background: #DDD; - color: white; -} - -.button.mod--pending { - background:red; -} + .buttons { + display: flex; + align-items: center; + flex-direction: column; + flex: 1; + gap: 24px; + } -.infoText { - display: block; - margin-top: 40px; - font-size: 13px; - color: #666; + .infoText { + display: block; + font-size: 13px; + color: #666; + } } diff --git a/ui/button.mod/teach/ui/main.mod/main.html b/ui/button.mod/teach/ui/main.mod/main.html index 97eaf0ed1..ff9ffacf9 100644 --- a/ui/button.mod/teach/ui/main.mod/main.html +++ b/ui/button.mod/teach/ui/main.mod/main.html @@ -1,4 +1,4 @@ - + @@ -6,7 +6,8 @@ { "owner": { "values": { - "element": {"#": "main"} + "element": {"#": "main"}, + "visualStyle": {"@": "visualStyle"} }, "listeners":[ { @@ -18,6 +19,17 @@ "listener": {"@": "owner"} } ] + }, + "visualStyle": { + "prototype": "mod/ui/visual-style.mod", + "values": { + "controlBorderWidth": "1px", + "controlBorderRadius": "8px", + "controlLabelSize": "16px", + "controlBackgroundFill": "rgba(243, 244, 246, 1)", + "controlSelectionFill": "rgba(252, 252, 252, 1)", + "textFill": "hsl(0, 0%, 0%, 1)" + } }, "simpleButton": { "prototype": "mod/ui/button.mod", @@ -91,10 +103,10 @@
+
- -
+ + diff --git a/ui/segmented-control.mod/segmented-control.css b/ui/segmented-control.mod/segmented-control.css index 3c2902478..68296d10f 100644 --- a/ui/segmented-control.mod/segmented-control.css +++ b/ui/segmented-control.mod/segmented-control.css @@ -16,115 +16,113 @@ --mod-segment-active-text: rgba(0, 0, 0, 1); } -@scope (.ModSegmentedControl) { - :scope { +.ModSegmentedControl { + display: flex; + -webkit-tap-highlight-color: transparent; + + > .ModSegmentedControl-container { display: flex; - -webkit-tap-highlight-color: transparent; + background-color: var(--visual-style-control-background-fill); + padding: var(--mod-segmented-control-padding); /** TODO Incorporate into visual-style? **/ + position: relative; + flex: 1; + border-radius: var(--visual-style-control-border-radius); + + > .ModSegmentedControl-thumb { + position: absolute; + top: var(--mod-segmented-control-padding); + left: var(--mod-segmented-control-padding); + background: var(--visual-style-control-selection-fill); + backdrop-filter: blur(10px); + border: 2px solid var(--visual-style-control-border-color); + box-sizing: border-box; + box-shadow: var(--visual-style-control-thumb-shadow); /** TODO Incorporate into visual-style? **/ + z-index: 1; + pointer-events: none; + height: calc(100% - var(--mod-segmented-control-padding) * 2); + } - > .ModSegmentedControl-container { + > .ModSegmentedControl-segments { display: flex; - background-color: var(--visual-style-control-background-fill); - padding: var(--mod-segmented-control-padding); /** TODO Incorporate into visual-style? **/ + flex-direction: row; + flex-wrap: nowrap; + overflow: hidden; position: relative; flex: 1; - border-radius: var(--visual-style-control-border-radius); + justify-content: space-around; - > .ModSegmentedControl-thumb { - position: absolute; - top: var(--mod-segmented-control-padding); - left: var(--mod-segmented-control-padding); - background: var(--visual-style-control-selection-fill); - backdrop-filter: blur(10px); - border: 2px solid var(--visual-style-control-border-color); + > .ModSegmentedControl-segment { + flex: 1; + padding: 0.25rem 0.5rem; + min-height: 2.25rem; + display: flex; + align-items: center; + justify-content: center; + color: var(--visual-style-text-fill); + cursor: pointer; + transition: var(--mod-segment-transition); + z-index: 2; box-sizing: border-box; - box-shadow: var(--visual-style-control-thumb-shadow); /** TODO Incorporate into visual-style? **/ - z-index: 1; - pointer-events: none; - height: calc(100% - var(--mod-segmented-control-padding) * 2); - } + white-space: nowrap; + -webkit-user-select: none; + user-select: none; + outline: none; + border: none; + background: transparent; + font-family: inherit; + font-size: 1rem; + gap: 0.5rem; + + &:hover:not(.mod--selected) { + background-color: var(--visual-style-control-hover-fill); + color: var(--mod-segment-hover-text); + } - > .ModSegmentedControl-segments { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - overflow: hidden; - position: relative; - flex: 1; - justify-content: space-around; + &:active:not(.mod--selected) { + background-color: var(--visual-style-control-active-fill); + color: var(--mod-segment-active-text); + } - > .ModSegmentedControl-segment { - flex: 1; - padding: 0.25rem 0.5rem; - min-height: 2.25rem; - display: flex; - align-items: center; - justify-content: center; + &.mod--selected { color: var(--visual-style-text-fill); - cursor: pointer; - transition: var(--mod-segment-transition); - z-index: 2; - box-sizing: border-box; - white-space: nowrap; - -webkit-user-select: none; - user-select: none; - outline: none; - border: none; - background: transparent; - font-family: inherit; - font-size: 1rem; - gap: 0.5rem; - - &:hover:not(.mod--selected) { - background-color: var(--visual-style-control-hover-fill); - color: var(--mod-segment-hover-text); - } - - &:active:not(.mod--selected) { - background-color: var(--visual-style-control-active-fill); - color: var(--mod-segment-active-text); - } - - &.mod--selected { - color: var(--visual-style-text-fill); - } - - > * { - pointer-events: none; - user-select: none; - } } - } - > .ModSegmentedControl-thumb, - > .ModSegmentedControl-segments > .ModSegmentedControl-segment { - border-radius: calc(var(--visual-style-control-border-radius) - var(--mod-segmented-control-padding)); + > * { + pointer-events: none; + user-select: none; + } } } - &.mod--disabled { - opacity: 0.6; - pointer-events: none; - cursor: not-allowed; + > .ModSegmentedControl-thumb, + > .ModSegmentedControl-segments > .ModSegmentedControl-segment { + border-radius: calc(var(--visual-style-control-border-radius) - var(--mod-segmented-control-padding)); } + } - &.mod--readyForAnimation { - > .ModSegmentedControl-container { - > .ModSegmentedControl-thumb { - transition: var(--mod-segmented-control-thumb-transition); - } + &.mod--disabled { + opacity: 0.6; + pointer-events: none; + cursor: not-allowed; + } + + &.mod--readyForAnimation { + > .ModSegmentedControl-container { + > .ModSegmentedControl-thumb { + transition: var(--mod-segmented-control-thumb-transition); } } + } - /* "Compact Mode" (e.g., for narrow containers) */ - @container (max-width: 400px) { - > .ModSegmentedControl-container { - > .ModSegmentedControl-segments { - > .ModSegmentedControl-segment { - padding: 0.2rem 0.4rem; - font-size: 0.75rem; - min-height: 2rem; - gap: 0.25rem; - } + /* "Compact Mode" (e.g., for narrow containers) */ + @container (max-width: 400px) { + > .ModSegmentedControl-container { + > .ModSegmentedControl-segments { + > .ModSegmentedControl-segment { + padding: 0.2rem 0.4rem; + font-size: 0.75rem; + min-height: 2rem; + gap: 0.25rem; } } } diff --git a/ui/segmented-control.mod/teach/index.css b/ui/segmented-control.mod/teach/index.css new file mode 100644 index 000000000..b8dc379ee --- /dev/null +++ b/ui/segmented-control.mod/teach/index.css @@ -0,0 +1,7 @@ +body { + font-family: -apple-system, Roboto, sans-serif; + background-color: #fafafa; + max-width: 800px; + margin: 0 auto; + padding: 20px; +} diff --git a/ui/segmented-control.mod/teach/index.html b/ui/segmented-control.mod/teach/index.html index ad0d6c62b..c25cb0334 100644 --- a/ui/segmented-control.mod/teach/index.html +++ b/ui/segmented-control.mod/teach/index.html @@ -1,9 +1,10 @@ - + Teach SegmentControl Mod +