diff --git a/CHANGELOG.md b/CHANGELOG.md index 2357776..44d17ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v0.2.31] - 2026-01-08 + +### Added +- Add `createStyleScope()` for scoped dynamic CSS + ### Changed - Use `@aegisjsproject/escape` for HTML escaping diff --git a/core.js b/core.js index e66198f..7385dd7 100644 --- a/core.js +++ b/core.js @@ -16,7 +16,7 @@ export { text, createStyleSheet, createCSSParser, css, lightCSS, darkCSS , styleSheetToFile, styleSheetToLink, createHTMLParser, html, doc, trustedHTML, htmlUnsafe, docUnsafe, htmlToFile, createTrustedHTMLTemplate, xml, svg, json, math, url, - createShadowParser, shadow, styledShadow, el, createBoundParser, adoptStyles, prefixCSSRules, + createShadowParser, shadow, styledShadow, el, createBoundParser, adoptStyles, prefixCSSRules, createStyleScope, } from './parsers.js'; export { diff --git a/package-lock.json b/package-lock.json index bbb47d4..8f21112 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aegisjsproject/core", - "version": "0.2.30", + "version": "0.2.31", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@aegisjsproject/core", - "version": "0.2.30", + "version": "0.2.31", "funding": [ { "type": "librepay", diff --git a/package.json b/package.json index 3680213..e8732c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aegisjsproject/core", - "version": "0.2.30", + "version": "0.2.31", "description": "A fast, secure, modern, light-weight, and simple JS library for creating web components and more!", "keywords": [ "aegis", diff --git a/parsers.js b/parsers.js index 3a55e21..b7eb624 100644 --- a/parsers.js +++ b/parsers.js @@ -1,7 +1,7 @@ export { text } from './parsers/text.js'; export { createStyleSheet, createCSSParser, css, lightCSS, prefixCSSRules, - darkCSS, styleSheetToFile, styleSheetToLink, createBoundParser, adoptStyles, + darkCSS, styleSheetToFile, styleSheetToLink, createBoundParser, adoptStyles, createStyleScope, } from './parsers/css.js'; export { createHTMLParser, html, doc, htmlUnsafe, docUnsafe, htmlToFile, diff --git a/parsers/css.js b/parsers/css.js index 718f5af..9e68c5f 100644 --- a/parsers/css.js +++ b/parsers/css.js @@ -1,5 +1,42 @@ import { createStyleSheet, createCSSParser, css, setStyleSheets, addStyleSheets } from '@aegisjsproject/parsers/css.js'; +const PREFIX = '_aegis_style_scope_'; + +/** + * Creates a scoped CSSStyleSheet attached to the provided root and returns a + * tagged template function for generating unique class names within that scope. + * + * @param {Document | ShadowRoot | Element} [root=document] The DOM scope to attach the stylesheet to. + * If an Element is passed, its root node (Document or ShadowRoot) is used. + * + * @param {Object} [options] Configuration options. + * @param {string} [options.baseURL] The base URL used to resolve relative URLs in the stylesheet. + * @param {string|MediaList|MediaQueryList} [options.media] The intended media for the stylesheet (e.g., "screen", "print"). + * @param {boolean} [options.disabled] Whether the stylesheet is disabled by default. + * @param {string} [options.prefix] A custom prefix for generated class names. + * @returns {(strings: TemplateStringsArray, ...values: any[]) => string} A tagged template function that + * inserts a new CSS rule and returns the generated class name. + */ +export function createStyleScope(root = document, { baseURL, media, disabled, prefix = PREFIX } = {}) { + if (root instanceof Element) { + return createStyleScope(root.getRootNode(), { baseURL, media, disabled, prefix }); + } else if (! (root instanceof Document || root instanceof ShadowRoot)) { + throw new TypeError('Root must be a Document, ShadowRoot, or Element.'); + } else { + const sheet = new CSSStyleSheet({ baseURL, media: media instanceof MediaQueryList ? media.media : media, disabled }); + root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet]; + + return (strings, ...values) => { + const uuid = crypto.randomUUID(); + const className = `${prefix}${uuid}`; + const rule = `.${CSS.escape(prefix) + uuid} { ${String.raw(strings, ...values)} }`; + sheet.insertRule(rule, sheet.cssRules.length); + + return className; + }; + } +} + export const lightCSS = createCSSParser({ media: '(prefers-color-scheme: light)', baseURL: document.baseURI }); export const darkCSS = createCSSParser({ media: '(prefers-color-scheme: dark)', baseURL: document.baseURI }); diff --git a/test/index.js b/test/index.js index e6a0170..1bf5312 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,6 @@ import { html, css, replaceStyles, getUniqueSelector, createComponent, closeRegistration, - data, attr, createTrustedHTMLTemplate, + data, attr, createTrustedHTMLTemplate, createStyleScope, } from '@aegisjsproject/core'; import { sanitizer as defaultSanitizer } from '@aegisjsproject/sanitizer/config/html.js'; @@ -42,7 +42,7 @@ document.body.setAttribute(fooEvent, FUNCS.debug.log); document.body.dataset[stateKey] = 'bg'; document.body.dataset[stateStyle] = 'background-color'; -replaceStyles(document, reset, baseTheme, lightTheme, darkTheme, btn, btnPrimary, btnDanger, btnWarning, +replaceStyles(document, ...document.adoptedStyleSheets, reset, baseTheme, lightTheme, darkTheme, btn, btnPrimary, btnDanger, btnWarning, btnInfo, btnSystemAccent, btnSuccess, btnLink, btnSecondary, css`.${scope} { color: red; @@ -57,6 +57,8 @@ replaceStyles(document, reset, baseTheme, lightTheme, darkTheme, btn, btnPrimary } `); +const style = createStyleScope(document.body, { media: matchMedia('(min-width: 800px)')}); + const DadJoke = await customElements.whenDefined('dad-joke'); const frag = document.createDocumentFragment(); const h1 = document.createElement('h1'); @@ -66,7 +68,10 @@ h1.textContent = 'Hello, World!'; frag.append(h1); try { - document.body.append(html`
+ document.body.append(html`
${frag}

Click Me!