diff --git a/docs/_guide/styleable.md b/docs/_guide/styleable.md
new file mode 100644
index 00000000..9e203600
--- /dev/null
+++ b/docs/_guide/styleable.md
@@ -0,0 +1,104 @@
+---
+chapter: 8
+subtitle: Bringing CSS into ShadowDOM
+hidden: true
+---
+
+Components with ShadowDOM typically want to introduce some CSS into their ShadowRoots. This is done with the use of `adoptedStyleSheets`, which can be a little cumbersome, so Catalyst provides the `@style` decorator and `css` utility function to more easily add CSS to your component.
+
+If your CSS lives in a different file, you can import the file with using the `assert { type: 'css' }` import assertion. You might need to configure your bundler tool to allow for this. If you're unfamiliar with this feature, you can [check out the web.dev article on CSS Module Scripts](https://web.dev/css-module-scripts/):
+
+```typescript
+import {controller, style} from '@github/catalyst'
+import DesignSystemCSS from './my-design-system.css' assert { type: 'css' }
+
+@controller
+class UserRow extends HTMLElement {
+ @style designSystem = DesignSystemCSS
+
+ connectedCallback() {
+ this.attachShadow({ mode: 'open' })
+ // adoptedStyleSheets now includes our DesignSystemCSS!
+ console.assert(this.shadowRoot.adoptedStyleSheets.includes(this.designSystem))
+ }
+}
+```
+
+Multiple `@style` tags are allowed, each one will be applied to the `adoptedStyleSheets` meaning you can split your CSS without worry!
+
+```typescript
+import {controller} from '@github/catalyst'
+import UtilityCSS from './my-design-system/utilities.css' assert { type: 'css' }
+import NormalizeCSS from './my-design-system/normalize.css' assert { type: 'css' }
+import UserRowCSS from './my-design-system/components/user-row.css' assert { type: 'css' }
+
+@controller
+class UserRow extends HTMLElement {
+ @style utilityCSS = UtilityCSS
+ @style normalizeCSS = NormalizeCSS
+ @style userRowCSS = UserRowCSS
+
+ connectedCallback() {
+ this.attachShadow({ mode: 'open' })
+ }
+}
+```
+
+### Defining CSS in JS
+
+Sometimes it can be useful to define small snippets of CSS within JavaScript itself, and so for this we have the `css` helper function which can create a `CSSStyleSheet` object on-the-fly:
+
+```typescript
+import {controller, style, css} from '@github/catalyst'
+
+@controller
+class UserRow extends HTMLElement {
+ @style componentCSS = css`:host { display: flex }`
+
+ connectedCallback() {
+ this.attachShadow({ mode: 'open' })
+ }
+}
+```
+
+As always though, the best way to handle dynamic per-instance values is with CSS variables:
+
+```typescript
+import {controller, style, css} from '@github/catalyst'
+
+const sizeCSS = (size = 1) => css`:host { font-size: var(--font-size, ${size}em); }`
+
+@controller
+class UserRow extends HTMLElement {
+ @style componentCSS = sizeCSS
+
+ @attr set fontSize(n: number) {
+ this.style.setProperty('--font-size', n)
+ }
+}
+```
+```html
+Alex
+Riley
+```
+
+The `css` function is memoized; it will always return the same `CSSStyleSheet` object for every callsite. This allows you to "lift" it into a function that can change the CSS for all components by calling the function, which will replace the CSS inside it.
+
+```typescript
+import {controller, style, css} from '@github/catalyst'
+
+const sizeCSS = (size = 1) => css`:host { font-size: ${size}em; }`
+
+// Calling sizeCSS will always result in the same CSSStyleSheet object
+console.assert(sizeCSS(1) === sizeCSS(2))
+
+@controller
+class UserRow extends HTMLElement {
+ @style componentCSS = sizeCSS
+
+ #size = 1
+ makeAllUsersLargerFont() {
+ sizeCSS(this.#size++)
+ }
+}
+```
diff --git a/package-lock.json b/package-lock.json
index 0ec7de9e..006b3ba7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,7 @@
"sinon": "^13.0.1",
"size-limit": "^7.0.8",
"tslib": "^2.4.0",
- "typescript": "^4.6.3"
+ "typescript": "^4.8.2"
}
},
"node_modules/@babel/code-frame": {
@@ -8052,9 +8052,9 @@
}
},
"node_modules/typescript": {
- "version": "4.6.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
- "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+ "version": "4.8.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
+ "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -14826,9 +14826,9 @@
}
},
"typescript": {
- "version": "4.6.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
- "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+ "version": "4.8.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
+ "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
"dev": true
},
"typical": {
diff --git a/package.json b/package.json
index b487ad71..fb867e6b 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"sinon": "^13.0.1",
"size-limit": "^7.0.8",
"tslib": "^2.4.0",
- "typescript": "^4.6.3"
+ "typescript": "^4.8.2"
},
"size-limit": [
{
diff --git a/src/controllable.ts b/src/controllable.ts
index a92368a0..4b25900d 100644
--- a/src/controllable.ts
+++ b/src/controllable.ts
@@ -1,9 +1,11 @@
import type {CustomElementClass, CustomElement} from './custom-element.js'
import {createAbility} from './ability.js'
+import {observe} from './mark.js'
export interface Controllable {
[attachShadowCallback]?(shadowRoot: ShadowRoot): void
[attachInternalsCallback]?(internals: ElementInternals): void
+ [markChangedCallback]?(props: Map): void
}
export interface ControllableClass {
// TS mandates Constructors that get mixins have `...args: any[]`
@@ -13,6 +15,7 @@ export interface ControllableClass {
export const attachShadowCallback = Symbol()
export const attachInternalsCallback = Symbol()
+export const markChangedCallback = Symbol()
const shadows = new WeakMap()
const internals = new WeakMap()
@@ -33,6 +36,16 @@ export const controllable = createAbility(
// Ignore errors
}
}
+ const queue = new Map()
+ observe(this, async (prop: PropertyKey, oldValue: unknown, newValue: unknown) => {
+ if (Object.is(newValue, oldValue)) return
+ queue.set(prop, oldValue)
+ if (queue.size > 1) return
+ await Promise.resolve()
+ const changed = new Map(queue)
+ queue.clear()
+ ;(this as Controllable)[markChangedCallback]?.(changed)
+ })
}
connectedCallback() {
diff --git a/src/mark.ts b/src/mark.ts
index 31e263a0..02db86af 100644
--- a/src/mark.ts
+++ b/src/mark.ts
@@ -23,9 +23,16 @@ const getType = (descriptor?: PropertyDescriptor): PropertyType => {
return 'field'
}
+type observer = (key: PropertyKey, oldValue: unknown, newValue: unknown) => void
+const observers = new WeakMap