@elastic/distillate 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/cjs/collector.js +129 -58
  2. package/dist/cjs/collector.js.map +1 -1
  3. package/dist/cjs/dom_sink.js +2 -2
  4. package/dist/cjs/dom_sink.js.map +1 -1
  5. package/dist/cjs/emotion.js.map +1 -1
  6. package/dist/cjs/engine.js +24 -8
  7. package/dist/cjs/engine.js.map +1 -1
  8. package/dist/cjs/index.js +3 -1
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/sink.js +9 -0
  11. package/dist/cjs/sink.js.map +1 -0
  12. package/dist/collector.d.ts +18 -1
  13. package/dist/collector.d.ts.map +1 -1
  14. package/dist/collector.js +129 -58
  15. package/dist/collector.js.map +1 -1
  16. package/dist/dom_sink.d.ts +8 -12
  17. package/dist/dom_sink.d.ts.map +1 -1
  18. package/dist/dom_sink.js +2 -2
  19. package/dist/dom_sink.js.map +1 -1
  20. package/dist/emotion.d.ts +3 -6
  21. package/dist/emotion.d.ts.map +1 -1
  22. package/dist/emotion.js.map +1 -1
  23. package/dist/engine.d.ts +26 -1
  24. package/dist/engine.d.ts.map +1 -1
  25. package/dist/engine.js +24 -8
  26. package/dist/engine.js.map +1 -1
  27. package/dist/index.d.ts +3 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/sink.d.ts +16 -0
  32. package/dist/sink.d.ts.map +1 -0
  33. package/dist/sink.js +8 -0
  34. package/dist/sink.js.map +1 -0
  35. package/docs/concepts/collection.md +1 -1
  36. package/docs/getting-started/installation.md +5 -5
  37. package/docs/guides/react-renderer.md +45 -1
  38. package/docs/reference/api/emotion.md +58 -140
  39. package/docs/reference/api/index-1.md +452 -245
  40. package/docs/reference/api/testing.md +6 -6
  41. package/docs/reference/entry-points.md +18 -4
  42. package/docs/reference/okf-map.md +2 -1
  43. package/package.json +1 -1
  44. package/src/collector.ts +145 -59
  45. package/src/dom_sink.ts +9 -10
  46. package/src/emotion.ts +3 -6
  47. package/src/engine.ts +59 -9
  48. package/src/index.ts +14 -1
  49. package/src/sink.ts +25 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  ### VarInvariantViolation [#varinvariantviolation]
6
6
 
7
- Defined in: [var\_invariant.ts:14](https://github.com/elastic/distillate/blob/f131912edadeb90a659ad22ee215412bedb1394d/src/var_invariant.ts#L14)
7
+ Defined in: [var\_invariant.ts:14](https://github.com/elastic/distillate/blob/d1dccb0eb5372d5984331c21603220340752a304/src/var_invariant.ts#L14)
8
8
 
9
9
  A `var(...)` whose custom property is never declared in the same CSS text.
10
10
 
@@ -12,8 +12,8 @@ A `var(...)` whose custom property is never declared in the same CSS text.
12
12
 
13
13
  | Property | Modifier | Type | Description | Defined in |
14
14
  | ------ | ------ | ------ | ------ | ------ |
15
- | $$$context$$$ `context` | `readonly` | `string` | Surrounding CSS text for error reporting. | [var\_invariant.ts:18](https://github.com/elastic/distillate/blob/f131912edadeb90a659ad22ee215412bedb1394d/src/var_invariant.ts#L18) |
16
- | $$$reference$$$ `reference` | `readonly` | `string` | The undeclared custom property name (e.g. `--aui-color-bg`). | [var\_invariant.ts:16](https://github.com/elastic/distillate/blob/f131912edadeb90a659ad22ee215412bedb1394d/src/var_invariant.ts#L16) |
15
+ | $$$context$$$ `context` | `readonly` | `string` | Surrounding CSS text for error reporting. | [var\_invariant.ts:18](https://github.com/elastic/distillate/blob/d1dccb0eb5372d5984331c21603220340752a304/src/var_invariant.ts#L18) |
16
+ | $$$reference$$$ `reference` | `readonly` | `string` | The undeclared custom property name (e.g. `--aui-color-bg`). | [var\_invariant.ts:16](https://github.com/elastic/distillate/blob/d1dccb0eb5372d5984331c21603220340752a304/src/var_invariant.ts#L16) |
17
17
 
18
18
  ## Functions
19
19
 
@@ -23,7 +23,7 @@ A `var(...)` whose custom property is never declared in the same CSS text.
23
23
  function assertVarRefsHaveDeclarations(css): void;
24
24
  ```
25
25
 
26
- Defined in: [var\_invariant.ts:135](https://github.com/elastic/distillate/blob/f131912edadeb90a659ad22ee215412bedb1394d/src/var_invariant.ts#L135)
26
+ Defined in: [var\_invariant.ts:135](https://github.com/elastic/distillate/blob/d1dccb0eb5372d5984331c21603220340752a304/src/var_invariant.ts#L135)
27
27
 
28
28
  Throws if any `var(...)` in `css` has no matching custom-property declaration.
29
29
 
@@ -49,7 +49,7 @@ If any reference has no declaration in the same text.
49
49
  function findVarRefViolations(css): readonly VarInvariantViolation[];
50
50
  ```
51
51
 
52
- Defined in: [var\_invariant.ts:109](https://github.com/elastic/distillate/blob/f131912edadeb90a659ad22ee215412bedb1394d/src/var_invariant.ts#L109)
52
+ Defined in: [var\_invariant.ts:109](https://github.com/elastic/distillate/blob/d1dccb0eb5372d5984331c21603220340752a304/src/var_invariant.ts#L109)
53
53
 
54
54
  Returns all `var(...)` references whose custom property is never declared in `css`.
55
55
 
@@ -75,7 +75,7 @@ function tokenTreeDts(
75
75
  ): string;
76
76
  ```
77
77
 
78
- Defined in: [testing.ts:23](https://github.com/elastic/distillate/blob/f131912edadeb90a659ad22ee215412bedb1394d/src/testing.ts#L23)
78
+ Defined in: [testing.ts:23](https://github.com/elastic/distillate/blob/d1dccb0eb5372d5984331c21603220340752a304/src/testing.ts#L23)
79
79
 
80
80
  Emits a TypeScript `interface` for a derived token tree, for Monaco extraLib text.
81
81
 
@@ -31,13 +31,16 @@ import {
31
31
  | `css` / `decls` | runtime | Handle and declaration templates. Also on the `createStyleModule` factory argument. |
32
32
  | `rule` / `media` / `container` / `variants` / `mapDomain` | runtime | Root-entry authoring helpers. `mapDomain` has no collector side effect. |
33
33
  | `combineClassNames` | runtime | `context.resolveClassName(...handles)`. |
34
- | `StylesCollector` | runtime | Class; `artifactCollector` / `stylesheetCollector` return instances. |
34
+ | `StylesCollector` | runtime | Class; `artifactCollector` / `stylesheetCollector` / `liveCollection` return or wrap instances. `subscribe` reports growth. |
35
+ | `createDomSink` | runtime | One `<style>` element in `document.head` or a given `parent` (e.g. a `ShadowRoot`), flushed per turn. Also exported from `./emotion`. |
35
36
  | `renderStyles` | runtime | Unbound renderer; prefer `distillery.renderStyles`. |
36
37
  | `createStyleNameResolver` | runtime | Standalone compact/readable name map. |
37
38
  | `StyleRegistry` | runtime | Module registry class. |
38
39
  | `Distillery` / `DistilleryOptions` / `DistilleryEnvironment` / `ThemeVarDefinition` | type | Bindings. |
39
40
  | `TokensOf` / `ValuesOf` / `PathsOf` / `ThemeTree` / `SchemePair` / `ThemeVariation` / `ResolvedThemeVariation` / `ThemeAlternate` / `RenderStylesOptions` | type | Theme derivation and render selection. |
40
41
  | `StyleHandle` / `StylesModule` / `StyleNameResolver` / `StyleNameMode` / `StyleTarget` | type | Handles, modules, naming. |
42
+ | `LiveCollection` / `LiveCollectionOptions` | type | `distillery.liveCollection` result and options. |
43
+ | `StyleSink` / `CreateDomSinkOptions` / `DocumentLike` / `StyleElementLike` / `StyleParentLike` | type | Sink surface. |
41
44
 
42
45
  ### Key signatures
43
46
 
@@ -57,6 +60,11 @@ distillery.stylesheetCollector(
57
60
  names?: 'compact' | 'readable',
58
61
  options?: { warn?: (message: string) => void }
59
62
  ): StylesCollector;
63
+ distillery.liveCollection(options?: {
64
+ sink?: StyleSink;
65
+ render?: RenderStylesOptions;
66
+ warn?: (message: string) => void;
67
+ }): LiveCollection; // { collector, resolveClassName(...handles), css() }
60
68
  distillery.renderStyles(
61
69
  collector: StylesCollector,
62
70
  resolver?: StyleNameResolver,
@@ -66,6 +74,12 @@ distillery.renderStyles(
66
74
  collector.use(module | entry | entries): void;
67
75
  collector.useHandles(handles: readonly StyleHandle[]): readonly StyleHandle[];
68
76
  collector.createResolver(): StyleNameResolver;
77
+ collector.subscribe(listener: () => void): () => void; // once per outermost call that grows
78
+ createDomSink(options: {
79
+ document: DocumentLike;
80
+ parent?: StyleParentLike;
81
+ schedule?: (flush: () => void) => void;
82
+ }): StyleSink;
69
83
  ```
70
84
 
71
85
  Field-level environment types: [Environment](environment.md).
@@ -88,13 +102,13 @@ const { css, cx, injectGlobal, stylesheet, globalModules } = createEmotion(
88
102
  | Export | Kind | Role |
89
103
  | ------ | ---- | ---- |
90
104
  | `createEmotion` | runtime | `css` / `cx` / `injectGlobal` over the distillery registry. |
91
- | `createDomSink` | runtime | One `<style>` element, flushed per turn. |
105
+ | `createDomSink` | runtime | One `<style>` element, flushed per turn. Re-exported from the root entry. |
92
106
  | `Emotion` / `EmotionCss` / `CreateEmotionOptions` / `StyleSink` | type | Return and option types. |
93
- | `CreateDomSinkOptions` / `DocumentLike` / `StyleElementLike` | type | DOM sink surface. |
107
+ | `CreateDomSinkOptions` / `DocumentLike` / `StyleElementLike` / `StyleParentLike` | type | DOM sink surface. Same as the root entry. |
94
108
 
95
109
  ```ts
96
110
  createEmotion(distillery, options?: { sink?: StyleSink }): Emotion;
97
- createDomSink(options: { document: DocumentLike; schedule?: (flush: () => void) => void }): StyleSink;
111
+ createDomSink(options: { document: DocumentLike; parent?: StyleParentLike; schedule?: (flush: () => void) => void }): StyleSink;
98
112
  ```
99
113
 
100
114
  The string form of a `css` template is readable and does not collect. The wrapper is still a `StyleHandle` for `useHandles` / `resolveClassName`.
@@ -8,7 +8,7 @@ description: Generated map of the Distillate OKF concept graph.
8
8
  Generated from `.okf/distillate` by `pnpm okf:map`. Do not edit by hand.
9
9
 
10
10
  - Concepts: 19
11
- - Links: 29
11
+ - Links: 30
12
12
  - Isolated concepts: 0
13
13
 
14
14
  ## Graph
@@ -44,6 +44,7 @@ flowchart LR
44
44
  concepts_ordering --> concepts_naming
45
45
  concepts_tokens --> concepts_distillery
46
46
  concepts_tokens --> reference_public_contract
47
+ entry_points_emotion --> entry_points_root
47
48
  entry_points_emotion --> playbooks_migrate_from_emotion
48
49
  entry_points_root --> entry_points_emotion
49
50
  entry_points_root --> entry_points_testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elastic/distillate",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Theme-agnostic CSS style engine: typed authoring with dependency capture, render-driven reachability collection, and dual readable/compact output targets. One runtime dependency: stylis.",
5
5
  "license": "Elastic-2.0",
6
6
  "author": "Elastic",
package/src/collector.ts CHANGED
@@ -113,6 +113,9 @@ export class StylesCollector {
113
113
  private readonly handles = new Map<string, StyleHandle>();
114
114
  private readonly themeDeps = new Set<string>();
115
115
  private readonly varDeps = new Set<ContextualVarPath>();
116
+ private readonly listeners = new Set<() => void>();
117
+ private depth = 0;
118
+ private dirty = false;
116
119
 
117
120
  constructor({
118
121
  target,
@@ -138,6 +141,12 @@ export class StylesCollector {
138
141
  * @param value Module (all non-variant entries), a single entry, or an array of entries.
139
142
  */
140
143
  use(value: StylesModule | StyleEntry | readonly StyleEntry[]): void {
144
+ this.batch(() => this.useValue(value));
145
+ }
146
+
147
+ private useValue(
148
+ value: StylesModule | StyleEntry | readonly StyleEntry[]
149
+ ): void {
141
150
  if (isReadonlyArray(value)) {
142
151
  value.forEach((entry) => this.use(entry));
143
152
  return;
@@ -155,7 +164,7 @@ export class StylesCollector {
155
164
  if (entry.kind === 'handle' && !this.retainHandle(entry)) {
156
165
  continue;
157
166
  }
158
- this.entries.set(entryKey(entry), entry);
167
+ this.setEntry(entry);
159
168
  if (entry.kind === 'handle' || entry.kind === 'rule') {
160
169
  this.addDeclarationDeps(entry.declarations);
161
170
  } else {
@@ -171,7 +180,7 @@ export class StylesCollector {
171
180
  if (!this.retainHandle(handle)) {
172
181
  continue;
173
182
  }
174
- this.handles.set(handle.key, handle);
183
+ this.setHandle(handle);
175
184
  }
176
185
  return;
177
186
  }
@@ -185,69 +194,89 @@ export class StylesCollector {
185
194
  * @param module Module whose entries should all be collected.
186
195
  */
187
196
  useAllEntries(module: StylesModule): void {
188
- for (const entry of module.entries) {
189
- if (entry.kind === 'handle' && !this.retainHandle(entry)) {
190
- continue;
197
+ this.batch(() => {
198
+ for (const entry of module.entries) {
199
+ if (entry.kind === 'handle' && !this.retainHandle(entry)) {
200
+ continue;
201
+ }
202
+ this.setEntry(entry);
191
203
  }
192
- this.entries.set(entryKey(entry), entry);
193
- }
194
- for (const handle of module.handleList) {
195
- if (!this.retainHandle(handle)) {
196
- continue;
204
+ for (const handle of module.handleList) {
205
+ if (!this.retainHandle(handle)) {
206
+ continue;
207
+ }
208
+ this.setHandle(handle);
197
209
  }
198
- this.handles.set(handle.key, handle);
199
- }
200
- module.themeDeps.forEach((path) => this.themeDeps.add(path));
201
- module.varDeps.forEach((path) => this.varDeps.add(path));
210
+ this.addDeps(module.themeDeps, module.varDeps);
211
+ });
202
212
  }
203
213
 
204
214
  /**
205
215
  * Collects handles and auto-includes rules whose selector deps are all present.
206
216
  *
207
- * Media blocks include only inner rules whose deps are met.
217
+ * Media blocks gain inner rules whose deps are met and keep rules already collected.
208
218
  *
209
219
  * @param handles Handles resolved during this render pass (e.g. from `resolveClassName`).
210
220
  * @returns Handles that were retained. Empty untargeted handles are omitted in compact mode.
211
221
  */
212
222
  useHandles(handles: readonly StyleHandle[]): readonly StyleHandle[] {
213
- const retained: StyleHandle[] = [];
214
- for (const handle of handles) {
215
- if (!this.retainHandle(handle)) {
216
- continue;
217
- }
218
- retained.push(handle);
219
- if (this.handles.has(handle.key)) {
220
- continue;
221
- }
222
- this.entries.set(entryKey(handle), handle);
223
- this.handles.set(handle.key, handle);
224
- this.addDeclarationDeps(handle.declarations);
225
- }
226
- for (const handle of retained) {
227
- const module = this.registry.module(handle.moduleName);
228
- if (!module) {
229
- continue;
223
+ return this.batch(() => {
224
+ const retained: StyleHandle[] = [];
225
+ for (const handle of handles) {
226
+ if (!this.retainHandle(handle)) {
227
+ continue;
228
+ }
229
+ retained.push(handle);
230
+ if (this.handles.has(handle.key)) {
231
+ continue;
232
+ }
233
+ this.setEntry(handle);
234
+ this.setHandle(handle);
235
+ this.addDeclarationDeps(handle.declarations);
230
236
  }
231
- for (const dependent of dependentEntries(module, handle.key)) {
232
- this.tryActivateDependent(dependent);
237
+ for (const handle of retained) {
238
+ const module = this.registry.module(handle.moduleName);
239
+ if (!module) {
240
+ continue;
241
+ }
242
+ for (const dependent of dependentEntries(module, handle.key)) {
243
+ this.tryActivateDependent(dependent);
244
+ }
233
245
  }
234
- }
235
- return retained;
246
+ return retained;
247
+ });
236
248
  }
237
249
 
238
250
  /** Collects `auto` rules on `module` whose every selector dependency is already collected. */
239
251
  useRulesWhenDepsMet(module: StylesModule): void {
240
- for (const entry of module.entries) {
241
- if (entry.kind !== 'rule') {
242
- continue;
243
- }
244
- if (this.entries.has(entryKey(entry))) {
245
- continue;
246
- }
247
- if (this.allDepsMet(entry.dependsOn)) {
248
- this.addEntry(entry);
252
+ this.batch(() => {
253
+ for (const entry of module.entries) {
254
+ if (entry.kind !== 'rule') {
255
+ continue;
256
+ }
257
+ if (this.entries.has(entryKey(entry))) {
258
+ continue;
259
+ }
260
+ if (this.allDepsMet(entry.dependsOn)) {
261
+ this.addEntry(entry);
262
+ }
249
263
  }
250
- }
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Calls `listener` after a public mutation adds an entry, handle, theme var, or contextual var.
269
+ *
270
+ * Fires at most once per outermost call; nested calls such as `use([a, b])` batch into it.
271
+ *
272
+ * @param listener Called with no arguments after the collection grows.
273
+ * @returns Unsubscribes `listener`.
274
+ */
275
+ subscribe(listener: () => void): () => void {
276
+ this.listeners.add(listener);
277
+ return () => {
278
+ this.listeners.delete(listener);
279
+ };
251
280
  }
252
281
 
253
282
  private tryActivateDependent(entry: StyleEntry): void {
@@ -265,18 +294,22 @@ export class StylesCollector {
265
294
  return;
266
295
  }
267
296
  if (entry.kind === 'media') {
268
- const liveRules = entry.rules.filter(
269
- (rule) => rule.auto && this.allDepsMet(rule.dependsOn)
297
+ // Add newly live rules; keep rules already collected (e.g. by `use`).
298
+ const existing = this.entries.get(entryKey(entry));
299
+ const collected = new Set<StyleRule>(
300
+ existing?.kind === 'media' ? existing.rules : []
301
+ );
302
+ const rules = entry.rules.filter(
303
+ (rule) =>
304
+ collected.has(rule) || (rule.auto && this.allDepsMet(rule.dependsOn))
270
305
  );
271
- if (liveRules.length === 0) {
306
+ if (rules.length === collected.size) {
272
307
  return;
273
308
  }
274
309
  const filtered: StyleMedia =
275
- liveRules.length === entry.rules.length
276
- ? entry
277
- : { ...entry, rules: liveRules };
278
- this.entries.set(entryKey(filtered), filtered);
279
- for (const rule of liveRules) {
310
+ rules.length === entry.rules.length ? entry : { ...entry, rules };
311
+ this.setEntry(filtered);
312
+ for (const rule of rules) {
280
313
  this.addDeclarationDeps(rule.declarations);
281
314
  }
282
315
  }
@@ -293,7 +326,7 @@ export class StylesCollector {
293
326
 
294
327
  /** Marks a theme token path as reachable so it appears in the emitted theme block, even if no collected declaration reads it. */
295
328
  useThemeVar(path: string): void {
296
- this.themeDeps.add(path);
329
+ this.batch(() => this.addDeps([path], []));
297
330
  }
298
331
 
299
332
  /** Creates a {@link StyleNameResolver} scoped to this collector's current dep set. */
@@ -439,9 +472,9 @@ export class StylesCollector {
439
472
  if (entry.kind === 'handle' && !this.retainHandle(entry)) {
440
473
  return;
441
474
  }
442
- this.entries.set(entryKey(entry), entry);
475
+ this.setEntry(entry);
443
476
  if (entry.kind === 'handle') {
444
- this.handles.set(entry.key, entry);
477
+ this.setHandle(entry);
445
478
  this.addDeclarationDeps(entry.declarations);
446
479
  return;
447
480
  }
@@ -455,8 +488,61 @@ export class StylesCollector {
455
488
  }
456
489
 
457
490
  private addDeclarationDeps(declarations: Declarations): void {
458
- declarations.deps.theme.forEach((path) => this.themeDeps.add(path));
459
- declarations.deps.vars.forEach((path) => this.varDeps.add(path));
491
+ this.addDeps(declarations.deps.theme, declarations.deps.vars);
492
+ }
493
+
494
+ private addDeps(
495
+ theme: Iterable<string>,
496
+ vars: Iterable<ContextualVarPath>
497
+ ): void {
498
+ const before = this.themeDeps.size + this.varDeps.size;
499
+ for (const path of theme) {
500
+ this.themeDeps.add(path);
501
+ }
502
+ for (const path of vars) {
503
+ this.varDeps.add(path);
504
+ }
505
+ if (this.themeDeps.size + this.varDeps.size !== before) {
506
+ this.dirty = true;
507
+ }
508
+ }
509
+
510
+ // A media entry changes when it gains inner rules.
511
+ private setEntry(entry: StyleEntry): void {
512
+ const key = entryKey(entry);
513
+ const existing = this.entries.get(key);
514
+ this.entries.set(key, entry);
515
+ if (
516
+ !existing ||
517
+ (existing.kind === 'media' &&
518
+ entry.kind === 'media' &&
519
+ existing.rules.length !== entry.rules.length)
520
+ ) {
521
+ this.dirty = true;
522
+ }
523
+ }
524
+
525
+ private setHandle(handle: StyleHandle): void {
526
+ if (!this.handles.has(handle.key)) {
527
+ this.dirty = true;
528
+ }
529
+ this.handles.set(handle.key, handle);
530
+ }
531
+
532
+ // Listeners run once the outermost public mutation returns.
533
+ private batch<T>(mutate: () => T): T {
534
+ this.depth += 1;
535
+ try {
536
+ return mutate();
537
+ } finally {
538
+ this.depth -= 1;
539
+ if (this.depth === 0 && this.dirty) {
540
+ this.dirty = false;
541
+ for (const listener of [...this.listeners]) {
542
+ listener();
543
+ }
544
+ }
545
+ }
460
546
  }
461
547
  }
462
548
 
package/src/dom_sink.ts CHANGED
@@ -5,26 +5,24 @@
5
5
  * 2.0.
6
6
  */
7
7
 
8
- import type { StyleSink } from './emotion';
8
+ import type { StyleElementLike, StyleParentLike, StyleSink } from './sink';
9
9
 
10
- /** Minimal interface for a DOM `<style>` element. */
11
- export interface StyleElementLike {
12
- /** Current stylesheet text, or `null` before the first flush. */
13
- textContent: string | null;
14
- }
10
+ export type { StyleElementLike, StyleParentLike } from './sink';
15
11
 
16
12
  /** Minimal interface for a DOM `Document` able to create and append style tags. */
17
13
  export interface DocumentLike {
18
14
  /** Creates a `<style>` element. */
19
15
  createElement(tagName: 'style'): StyleElementLike;
20
- /** Node that receives the created style element. */
21
- readonly head: { appendChild(node: StyleElementLike): void };
16
+ /** Default parent for the created style element. */
17
+ readonly head: StyleParentLike;
22
18
  }
23
19
 
24
20
  /** Host document and optional flush scheduler for a `<style>` sink. */
25
21
  export interface CreateDomSinkOptions {
26
- /** The host document where the style element will be appended. */
22
+ /** Document that creates the style element. */
27
23
  document: DocumentLike;
24
+ /** Node that receives the style element, e.g. a `ShadowRoot`. Defaults to `document.head`. */
25
+ parent?: StyleParentLike;
28
26
  /** Defers the flush. Defaults to `queueMicrotask`. */
29
27
  schedule?: (flush: () => void) => void;
30
28
  }
@@ -34,6 +32,7 @@ export interface CreateDomSinkOptions {
34
32
  */
35
33
  export const createDomSink = ({
36
34
  document,
35
+ parent,
37
36
  schedule = queueMicrotask,
38
37
  }: CreateDomSinkOptions): StyleSink => {
39
38
  let element: StyleElementLike | null = null;
@@ -47,7 +46,7 @@ export const createDomSink = ({
47
46
  }
48
47
  if (!element) {
49
48
  element = document.createElement('style');
50
- document.head.appendChild(element);
49
+ (parent ?? document.head).appendChild(element);
51
50
  }
52
51
  element.textContent = latestRender();
53
52
  };
package/src/emotion.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import type { Distillery } from './engine';
9
9
  import { contentHash64 } from './hash';
10
10
  import { flattenTemplate, isHandleLike, type TemplateSlice } from './nesting';
11
+ import type { StyleSink } from './sink';
11
12
  import {
12
13
  globalStylesFromFlattened,
13
14
  pendingHandleFromFlattened,
@@ -21,13 +22,9 @@ export {
21
22
  type CreateDomSinkOptions,
22
23
  type DocumentLike,
23
24
  type StyleElementLike,
25
+ type StyleParentLike,
24
26
  } from './dom_sink';
25
-
26
- /** Receives stylesheet invalidation from Emotion compat modules. */
27
- export interface StyleSink {
28
- /** Marks the sink dirty. `render` produces the current stylesheet when it flushes. */
29
- invalidate(render: () => string): void;
30
- }
27
+ export type { StyleSink } from './sink';
31
28
 
32
29
  /** Shared sink for Emotion compat modules on one registry. */
33
30
  export interface CreateEmotionOptions {
package/src/engine.ts CHANGED
@@ -19,12 +19,14 @@ import {
19
19
  type StyleNameResolverOptions,
20
20
  } from './names';
21
21
  import { renderStyles, type RenderStylesOptions } from './runtime';
22
+ import type { StyleSink } from './sink';
22
23
  import {
23
24
  createStyleModuleWithEnvironment,
24
25
  type PrimitiveStyleAuthoringApi,
25
26
  readableVarOwnersFromEnvironment,
26
27
  type ResolvedStyles,
27
28
  type StyleAuthoringApi,
29
+ type StyleHandle,
28
30
  StyleRegistry,
29
31
  type StylesModule,
30
32
  type StylesObject,
@@ -39,6 +41,30 @@ import {
39
41
  type ValuesOf,
40
42
  } from './theme';
41
43
 
44
+ /** Sink, render options, and warning sink for {@link Distillery.liveCollection}. */
45
+ export interface LiveCollectionOptions {
46
+ /** Invalidated whenever the collection grows. Nothing is written before the first handle. */
47
+ sink?: StyleSink;
48
+ /** Options forwarded to `renderStyles` by `css()`. */
49
+ render?: RenderStylesOptions;
50
+ /** Warning sink for no-op handles. Used only when the distillery has `dev: true`. */
51
+ warn?: (message: string) => void;
52
+ }
53
+
54
+ /**
55
+ * Readable collection filled by one live render.
56
+ *
57
+ * Readable only: compact names depend on the complete collected set and need two passes.
58
+ */
59
+ export interface LiveCollection {
60
+ /** Underlying readable artifact collector. Mutations here (e.g. `use(globalModule)`, `useThemeVar`) also invalidate the sink. */
61
+ readonly collector: StylesCollector;
62
+ /** Collects `handles` and returns their readable class names, space-separated. */
63
+ readonly resolveClassName: (...handles: StyleHandle[]) => string;
64
+ /** Renders the CSS collected so far. */
65
+ readonly css: () => string;
66
+ }
67
+
42
68
  /** One component library's binding: environment, registry, and pre-bound operations. Destructure-safe (no `this`). */
43
69
  export interface Distillery<
44
70
  TTokens = unknown,
@@ -79,6 +105,8 @@ export interface Distillery<
79
105
  names?: StyleNameMode,
80
106
  options?: Pick<StylesCollectorOptions, 'warn'>
81
107
  ) => StylesCollector;
108
+ /** Readable collection for one live render; keeps `sink` current as handles resolve. */
109
+ readonly liveCollection: (options?: LiveCollectionOptions) => LiveCollection;
82
110
  /** Emits collected CSS for this environment. */
83
111
  readonly renderStyles: (
84
112
  collector: StylesCollector,
@@ -135,6 +163,19 @@ export const createDistillery = <const TTheme extends ThemeTree>(
135
163
  sharedVars ?? new Set()
136
164
  );
137
165
 
166
+ const artifactCollector = (
167
+ names: StyleNameMode,
168
+ collectorOptions?: Pick<StylesCollectorOptions, 'warn'>
169
+ ): StylesCollector =>
170
+ new StylesCollector({
171
+ target: 'artifact',
172
+ names,
173
+ registry,
174
+ prefix,
175
+ dev,
176
+ ...(collectorOptions?.warn ? { warn: collectorOptions.warn } : {}),
177
+ });
178
+
138
179
  return {
139
180
  environment,
140
181
  tokens,
@@ -160,15 +201,7 @@ export const createDistillery = <const TTheme extends ThemeTree>(
160
201
  ({ css: style, tokens: moduleTokens }) =>
161
202
  factory({ style, tokens: moduleTokens })
162
203
  ),
163
- artifactCollector: (names, options) =>
164
- new StylesCollector({
165
- target: 'artifact',
166
- names,
167
- registry,
168
- prefix,
169
- dev,
170
- ...(options?.warn ? { warn: options.warn } : {}),
171
- }),
204
+ artifactCollector,
172
205
  stylesheetCollector: (names = 'readable', options) => {
173
206
  const collector = new StylesCollector({
174
207
  target: 'stylesheet',
@@ -183,6 +216,23 @@ export const createDistillery = <const TTheme extends ThemeTree>(
183
216
  }
184
217
  return collector;
185
218
  },
219
+ liveCollection: ({ sink, render, warn } = {}) => {
220
+ const collector = artifactCollector('readable', warn ? { warn } : {});
221
+ const css = (): string =>
222
+ renderStyles(environment, collector, undefined, render);
223
+ if (sink) {
224
+ collector.subscribe(() => sink.invalidate(css));
225
+ }
226
+ return {
227
+ collector,
228
+ resolveClassName: (...handles) =>
229
+ collector
230
+ .useHandles(handles)
231
+ .map((handle) => handle.readableName)
232
+ .join(' '),
233
+ css,
234
+ };
235
+ },
186
236
  renderStyles: (collector, resolver, renderOptions) =>
187
237
  renderStyles(environment, collector, resolver, renderOptions),
188
238
  createNameResolver: (resolverOptions) =>
package/src/index.ts CHANGED
@@ -5,7 +5,20 @@
5
5
  * 2.0.
6
6
  */
7
7
 
8
- export { createDistillery, type Distillery } from './engine';
8
+ export {
9
+ createDistillery,
10
+ type Distillery,
11
+ type LiveCollection,
12
+ type LiveCollectionOptions,
13
+ } from './engine';
14
+ export {
15
+ createDomSink,
16
+ type CreateDomSinkOptions,
17
+ type DocumentLike,
18
+ type StyleElementLike,
19
+ type StyleParentLike,
20
+ } from './dom_sink';
21
+ export type { StyleSink } from './sink';
9
22
  export {
10
23
  type DistilleryEnvironment,
11
24
  type DistilleryOptions,
package/src/sink.ts ADDED
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ * or more contributor license agreements. Licensed under the Elastic License
4
+ * 2.0; you may not use this file except in compliance with the Elastic License
5
+ * 2.0.
6
+ */
7
+
8
+ /** Receives stylesheet invalidation from a live collection or Emotion compat modules. */
9
+ export interface StyleSink {
10
+ /** Marks the sink dirty. `render` produces the current stylesheet when it flushes. */
11
+ invalidate(render: () => string): void;
12
+ }
13
+
14
+ /** Minimal interface for a DOM `<style>` element. */
15
+ export interface StyleElementLike {
16
+ /** Current stylesheet text, or `null` before the first flush. */
17
+ textContent: string | null;
18
+ }
19
+
20
+ /** Node that receives a managed `<style>` element, e.g. `document.head` or a `ShadowRoot`. */
21
+ export interface StyleParentLike {
22
+ // Method syntax (bivariant) so DOM `Node.appendChild<T extends Node>` assigns.
23
+ /** Appends the managed style element. */
24
+ appendChild(node: StyleElementLike): void;
25
+ }