@elastic/distillate 0.0.0 → 0.1.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.
- package/LICENSE.txt +93 -0
- package/NOTICE.txt +30 -0
- package/README.md +142 -0
- package/THIRD_PARTY_LICENSES.md +768 -0
- package/dist/cjs/collector.js +420 -0
- package/dist/cjs/collector.js.map +1 -0
- package/dist/cjs/css_scan.js +186 -0
- package/dist/cjs/css_scan.js.map +1 -0
- package/dist/cjs/dom_sink.js +40 -0
- package/dist/cjs/dom_sink.js.map +1 -0
- package/dist/cjs/emotion.js +189 -0
- package/dist/cjs/emotion.js.map +1 -0
- package/dist/cjs/engine.js +79 -0
- package/dist/cjs/engine.js.map +1 -0
- package/dist/cjs/environment.js +9 -0
- package/dist/cjs/environment.js.map +1 -0
- package/dist/cjs/hash.js +86 -0
- package/dist/cjs/hash.js.map +1 -0
- package/dist/cjs/idents.js +23 -0
- package/dist/cjs/idents.js.map +1 -0
- package/dist/cjs/index.js +42 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/instance.js +34 -0
- package/dist/cjs/instance.js.map +1 -0
- package/dist/cjs/local_vars.js +100 -0
- package/dist/cjs/local_vars.js.map +1 -0
- package/dist/cjs/minify.js +77 -0
- package/dist/cjs/minify.js.map +1 -0
- package/dist/cjs/names.js +77 -0
- package/dist/cjs/names.js.map +1 -0
- package/dist/cjs/nesting.js +406 -0
- package/dist/cjs/nesting.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/runtime.js +257 -0
- package/dist/cjs/runtime.js.map +1 -0
- package/dist/cjs/styles/authoring.js +169 -0
- package/dist/cjs/styles/authoring.js.map +1 -0
- package/dist/cjs/styles/declarations.js +139 -0
- package/dist/cjs/styles/declarations.js.map +1 -0
- package/dist/cjs/styles/index.js +32 -0
- package/dist/cjs/styles/index.js.map +1 -0
- package/dist/cjs/styles/module.js +233 -0
- package/dist/cjs/styles/module.js.map +1 -0
- package/dist/cjs/styles/registry.js +187 -0
- package/dist/cjs/styles/registry.js.map +1 -0
- package/dist/cjs/styles/types.js +9 -0
- package/dist/cjs/styles/types.js.map +1 -0
- package/dist/cjs/testing.js +37 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/cjs/theme.js +249 -0
- package/dist/cjs/theme.js.map +1 -0
- package/dist/cjs/tokens/contextual_var.js +39 -0
- package/dist/cjs/tokens/contextual_var.js.map +1 -0
- package/dist/cjs/tokens/index.js +24 -0
- package/dist/cjs/tokens/index.js.map +1 -0
- package/dist/cjs/tokens/kind.js +14 -0
- package/dist/cjs/tokens/kind.js.map +1 -0
- package/dist/cjs/tokens/light_dark.js +42 -0
- package/dist/cjs/tokens/light_dark.js.map +1 -0
- package/dist/cjs/tokens/scale_token.js +28 -0
- package/dist/cjs/tokens/scale_token.js.map +1 -0
- package/dist/cjs/tokens/theme_token.js +28 -0
- package/dist/cjs/tokens/theme_token.js.map +1 -0
- package/dist/cjs/var_invariant.js +115 -0
- package/dist/cjs/var_invariant.js.map +1 -0
- package/dist/collector.d.ts +84 -0
- package/dist/collector.d.ts.map +1 -0
- package/dist/collector.js +416 -0
- package/dist/collector.js.map +1 -0
- package/dist/css_scan.d.ts +57 -0
- package/dist/css_scan.d.ts.map +1 -0
- package/dist/css_scan.js +180 -0
- package/dist/css_scan.js.map +1 -0
- package/dist/dom_sink.d.ts +27 -0
- package/dist/dom_sink.d.ts.map +1 -0
- package/dist/dom_sink.js +36 -0
- package/dist/dom_sink.js.map +1 -0
- package/dist/emotion.d.ts +47 -0
- package/dist/emotion.d.ts.map +1 -0
- package/dist/emotion.js +184 -0
- package/dist/emotion.js.map +1 -0
- package/dist/engine.d.ts +41 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +75 -0
- package/dist/engine.js.map +1 -0
- package/dist/environment.d.ts +43 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +8 -0
- package/dist/environment.js.map +1 -0
- package/dist/hash.d.ts +2 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +82 -0
- package/dist/hash.js.map +1 -0
- package/dist/idents.d.ts +9 -0
- package/dist/idents.d.ts.map +1 -0
- package/dist/idents.js +19 -0
- package/dist/idents.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/instance.d.ts +16 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +30 -0
- package/dist/instance.js.map +1 -0
- package/dist/local_vars.d.ts +75 -0
- package/dist/local_vars.d.ts.map +1 -0
- package/dist/local_vars.js +92 -0
- package/dist/local_vars.js.map +1 -0
- package/dist/minify.d.ts +11 -0
- package/dist/minify.d.ts.map +1 -0
- package/dist/minify.js +73 -0
- package/dist/minify.js.map +1 -0
- package/dist/names.d.ts +39 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +70 -0
- package/dist/names.js.map +1 -0
- package/dist/nesting.d.ts +59 -0
- package/dist/nesting.d.ts.map +1 -0
- package/dist/nesting.js +400 -0
- package/dist/nesting.js.map +1 -0
- package/dist/runtime.d.ts +36 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +252 -0
- package/dist/runtime.js.map +1 -0
- package/dist/styles/authoring.d.ts +59 -0
- package/dist/styles/authoring.d.ts.map +1 -0
- package/dist/styles/authoring.js +155 -0
- package/dist/styles/authoring.js.map +1 -0
- package/dist/styles/declarations.d.ts +10 -0
- package/dist/styles/declarations.d.ts.map +1 -0
- package/dist/styles/declarations.js +132 -0
- package/dist/styles/declarations.js.map +1 -0
- package/dist/styles/index.d.ts +7 -0
- package/dist/styles/index.d.ts.map +1 -0
- package/dist/styles/index.js +12 -0
- package/dist/styles/index.js.map +1 -0
- package/dist/styles/module.d.ts +5 -0
- package/dist/styles/module.d.ts.map +1 -0
- package/dist/styles/module.js +229 -0
- package/dist/styles/module.js.map +1 -0
- package/dist/styles/registry.d.ts +48 -0
- package/dist/styles/registry.d.ts.map +1 -0
- package/dist/styles/registry.js +182 -0
- package/dist/styles/registry.js.map +1 -0
- package/dist/styles/types.d.ts +211 -0
- package/dist/styles/types.d.ts.map +1 -0
- package/dist/styles/types.js +8 -0
- package/dist/styles/types.js.map +1 -0
- package/dist/testing.d.ts +10 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +31 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme.d.ts +85 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +240 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens/contextual_var.d.ts +35 -0
- package/dist/tokens/contextual_var.d.ts.map +1 -0
- package/dist/tokens/contextual_var.js +33 -0
- package/dist/tokens/contextual_var.js.map +1 -0
- package/dist/tokens/index.d.ts +5 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +11 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/tokens/kind.d.ts +2 -0
- package/dist/tokens/kind.d.ts.map +1 -0
- package/dist/tokens/kind.js +10 -0
- package/dist/tokens/kind.js.map +1 -0
- package/dist/tokens/light_dark.d.ts +20 -0
- package/dist/tokens/light_dark.d.ts.map +1 -0
- package/dist/tokens/light_dark.js +37 -0
- package/dist/tokens/light_dark.js.map +1 -0
- package/dist/tokens/scale_token.d.ts +20 -0
- package/dist/tokens/scale_token.d.ts.map +1 -0
- package/dist/tokens/scale_token.js +23 -0
- package/dist/tokens/scale_token.js.map +1 -0
- package/dist/tokens/theme_token.d.ts +20 -0
- package/dist/tokens/theme_token.d.ts.map +1 -0
- package/dist/tokens/theme_token.js +23 -0
- package/dist/tokens/theme_token.js.map +1 -0
- package/dist/var_invariant.d.ts +25 -0
- package/dist/var_invariant.d.ts.map +1 -0
- package/dist/var_invariant.js +108 -0
- package/dist/var_invariant.js.map +1 -0
- package/docs/concepts/authoring.md +115 -0
- package/docs/concepts/collection.md +66 -0
- package/docs/concepts/distillery.md +47 -0
- package/docs/concepts/index.md +18 -0
- package/docs/concepts/naming-and-output.md +123 -0
- package/docs/concepts/ordering.md +61 -0
- package/docs/concepts/single-copy.md +103 -0
- package/docs/concepts/tokens.md +99 -0
- package/docs/docset.yml +53 -0
- package/docs/examples/01-stylesheet.ts +20 -0
- package/docs/examples/02-artifact.ts +26 -0
- package/docs/examples/03-variants.ts +40 -0
- package/docs/examples/04-emotion.ts +33 -0
- package/docs/examples/05-var-invariant.ts +31 -0
- package/docs/examples/06-payload.ts +85 -0
- package/docs/examples/fixture.ts +22 -0
- package/docs/getting-started/index.md +14 -0
- package/docs/getting-started/installation.md +30 -0
- package/docs/getting-started/quick-start.md +61 -0
- package/docs/getting-started/your-first-stylesheet.md +57 -0
- package/docs/guides/compact-artifacts.md +87 -0
- package/docs/guides/emotion-migration.md +60 -0
- package/docs/guides/index.md +17 -0
- package/docs/guides/name-collisions.md +37 -0
- package/docs/guides/non-css-surfaces.md +73 -0
- package/docs/guides/react-renderer.md +158 -0
- package/docs/guides/theming.md +107 -0
- package/docs/guides/var-invariant.md +24 -0
- package/docs/guides/vs-emotion.md +61 -0
- package/docs/index.md +73 -0
- package/docs/logo.svg +7 -0
- package/docs/playground/index.html +12 -0
- package/docs/playground/package.json +23 -0
- package/docs/playground/src/app/app.tsx +109 -0
- package/docs/playground/src/app/css_panel.tsx +119 -0
- package/docs/playground/src/app/editor_panel.tsx +87 -0
- package/docs/playground/src/app/index.ts +8 -0
- package/docs/playground/src/app/monaco_css_editor.tsx +84 -0
- package/docs/playground/src/app/monaco_source_editor.tsx +132 -0
- package/docs/playground/src/app/preview_panel.tsx +99 -0
- package/docs/playground/src/app/tokens_panel.tsx +117 -0
- package/docs/playground/src/app/types.ts +10 -0
- package/docs/playground/src/examples/snippets.test.ts +28 -0
- package/docs/playground/src/examples/snippets.ts +418 -0
- package/docs/playground/src/lib/compile.test.ts +157 -0
- package/docs/playground/src/lib/compile.ts +242 -0
- package/docs/playground/src/lib/demo_environment.test.ts +123 -0
- package/docs/playground/src/lib/demo_environment.ts +140 -0
- package/docs/playground/src/lib/editor_lib.test.ts +39 -0
- package/docs/playground/src/lib/editor_lib.ts +142 -0
- package/docs/playground/src/lib/format_css.test.ts +41 -0
- package/docs/playground/src/lib/format_css.ts +71 -0
- package/docs/playground/src/main.tsx +25 -0
- package/docs/playground/src/setup/chrome.test.ts +47 -0
- package/docs/playground/src/setup/chrome.ts +365 -0
- package/docs/playground/src/setup/monaco_env.ts +64 -0
- package/docs/playground/tsconfig.json +22 -0
- package/docs/playground/vite.config.ts +37 -0
- package/docs/playground/vitest.config.ts +35 -0
- package/docs/playground.md +28 -0
- package/docs/reference/api/emotion.md +372 -0
- package/docs/reference/api/index-1.md +2180 -0
- package/docs/reference/api/index.md +9 -0
- package/docs/reference/api/testing.md +92 -0
- package/docs/reference/api.md +12 -0
- package/docs/reference/entry-points.md +125 -0
- package/docs/reference/environment.md +60 -0
- package/docs/reference/index.md +11 -0
- package/docs/reference/okf-map.md +92 -0
- package/package.json +127 -4
- package/src/collector.ts +526 -0
- package/src/css_scan.ts +263 -0
- package/src/dom_sink.ts +65 -0
- package/src/emotion.ts +311 -0
- package/src/engine.ts +191 -0
- package/src/environment.ts +56 -0
- package/src/hash.ts +111 -0
- package/src/idents.ts +21 -0
- package/src/index.ts +93 -0
- package/src/instance.ts +49 -0
- package/src/local_vars.ts +220 -0
- package/src/minify.ts +87 -0
- package/src/names.ts +129 -0
- package/src/nesting.ts +596 -0
- package/src/runtime.ts +428 -0
- package/src/styles/authoring.ts +244 -0
- package/src/styles/declarations.ts +172 -0
- package/src/styles/index.ts +58 -0
- package/src/styles/module.ts +396 -0
- package/src/styles/registry.ts +261 -0
- package/src/styles/types.ts +283 -0
- package/src/testing.ts +48 -0
- package/src/theme.ts +454 -0
- package/src/tokens/contextual_var.ts +72 -0
- package/src/tokens/index.ts +17 -0
- package/src/tokens/kind.ts +13 -0
- package/src/tokens/light_dark.ts +58 -0
- package/src/tokens/scale_token.ts +41 -0
- package/src/tokens/theme_token.ts +42 -0
- package/src/var_invariant.ts +147 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
navigation_title: Distillate vs CSS-in-JS
|
|
3
|
+
description: When to use Distillate over Emotion, CSS Modules, static extraction, or Tailwind.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Distillate vs CSS-in-JS
|
|
7
|
+
|
|
8
|
+
Distillate is a **component library authoring** tool. Emotion, CSS Modules, Tailwind, and vanilla-extract are primarily **application** tools. The audiences overlap, but the core problems differ.
|
|
9
|
+
|
|
10
|
+
## The positioning
|
|
11
|
+
|
|
12
|
+
Use Distillate when you need **both a public stylesheet and compact self-contained artifact payloads** from the same authored source — and the library must carry no design opinions of its own.
|
|
13
|
+
|
|
14
|
+
Use Emotion (or similar) when you are styling an **application directly** and have no need to export tree-shaken CSS alongside HTML.
|
|
15
|
+
|
|
16
|
+
## Distillate vs Emotion
|
|
17
|
+
|
|
18
|
+
Emotion generates class names at runtime by hashing template content and injecting `<style>` elements. Distillate records declarations once at module-construction time and emits CSS on demand through a collector.
|
|
19
|
+
|
|
20
|
+
| | Emotion | Distillate |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| Primary audience | Application developers | Component library authors |
|
|
23
|
+
| Style authoring | `css\`...\`` at render time | `createStyleModule` once at module load |
|
|
24
|
+
| Runtime variation | New template → new class | CSS custom properties (`vars`, theme tokens) |
|
|
25
|
+
| Output modes | DOM injection | Stylesheet or compact artifact |
|
|
26
|
+
| Tree-shaking | No — all styles inject eagerly | Artifact collector drops unreached entries |
|
|
27
|
+
| Typed tokens | No | `lightDark`, `cq`, `vars` |
|
|
28
|
+
| Object styles | `css({ color: 'red' })` | Not supported |
|
|
29
|
+
| Keyframes | `keyframes\`...\`` | Not supported |
|
|
30
|
+
| `styled.*` | Yes (`@emotion/styled`) | No |
|
|
31
|
+
| Bundle constraint | Works in any bundle | One copy per runtime required |
|
|
32
|
+
|
|
33
|
+
The `@elastic/distillate/emotion` entry point is a drop-in surface (`css` / `cx` / `injectGlobal`) over the same registry. It lets you keep existing call sites while gaining the collector and artifact-export path. See [Migrate from Emotion](emotion-migration.md).
|
|
34
|
+
|
|
35
|
+
## When to use Distillate
|
|
36
|
+
|
|
37
|
+
- You ship a component library that exports **both** a loadable stylesheet (for apps) and self-contained HTML payloads (emails, AI reply cards, SVG renders, exported reports).
|
|
38
|
+
- You want typed design-token references (`tokens.colors.ink`) with pruning — unused tokens drop out of artifact CSS.
|
|
39
|
+
- You need the same styles in two name modes: human-readable for development, short-identifier compact when HTML and CSS travel together.
|
|
40
|
+
- You want to assert at test time that every `var(--...)` in emitted CSS has a matching declaration (`assertVarRefsHaveDeclarations`).
|
|
41
|
+
|
|
42
|
+
## When not to use Distillate
|
|
43
|
+
|
|
44
|
+
- **You need per-render dynamic class names.** Modules are static after construction. Put variation on CSS custom properties, not new templates per render.
|
|
45
|
+
- **You are building an application, not a library.** Distillate's artifact-export path has no value when styles never leave the host app.
|
|
46
|
+
- **You rely on `styled.*`, keyframes, `@supports`, or object styles.** These are unsupported.
|
|
47
|
+
- **Your bundle cannot guarantee a single copy at runtime.** See [Single-copy invariant](../concepts/single-copy.md). For plugin-host architectures (Kibana-style), the platform must expose Distillate as a shared dependency.
|
|
48
|
+
|
|
49
|
+
## Distillate vs CSS Modules
|
|
50
|
+
|
|
51
|
+
CSS Modules scope class names per file with a bundler plugin. They do not tree-shake unused declarations, have no typed token system, and cannot emit compact payloads. Choose CSS Modules for simple scoped styles in an application; choose Distillate for a library that needs typed tokens and artifact export.
|
|
52
|
+
|
|
53
|
+
## Distillate vs static extraction (Linaria / vanilla-extract)
|
|
54
|
+
|
|
55
|
+
Static extraction tools run at build time and produce real CSS files with zero runtime cost. The tradeoff: they cannot follow an actual render to determine which handles were reached. Distillate's artifact collection is a **runtime** side effect of resolving class names, which is the correct model when the emitted CSS must exactly match the rendered HTML tree.
|
|
56
|
+
|
|
57
|
+
Vanilla-extract offers typed token support similar to `createThemeContract`. Choose vanilla-extract if build-time extraction is a hard requirement or if runtime is forbidden (edge/serverless cold-start sensitive). Choose Distillate if artifact export or runtime reachability collection is required.
|
|
58
|
+
|
|
59
|
+
## Distillate vs Tailwind
|
|
60
|
+
|
|
61
|
+
Tailwind generates utility classes from a predefined scale. Distillate emits authored property blocks from tagged templates. They are not competing tools — a library can use Distillate internally while a host application uses Tailwind. The key difference: Tailwind cannot produce a compact, tree-shaken CSS payload tied to a specific render tree.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
navigation_title: Distillate
|
|
3
|
+
description: Theme-agnostic CSS style engine with typed authoring, reachability collection, and readable or compact output.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Distillate
|
|
7
|
+
|
|
8
|
+
{{pkg}} is a CSS style engine for component libraries. You author styles once, against your own tokens. The engine emits a public stylesheet with stable names, or a tree-shaken compact payload for HTML that leaves the app.
|
|
9
|
+
|
|
10
|
+
One runtime dependency, for `emotion`-style syntax: [stylis](https://github.com/thysultan/stylis.js) 4.4.0. No brand is baked in.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createDistillery, cq, lightDark } from '@elastic/distillate';
|
|
14
|
+
|
|
15
|
+
const distillery = createDistillery({
|
|
16
|
+
prefix: 'eui',
|
|
17
|
+
themeScope: '.eui-view',
|
|
18
|
+
theme: {
|
|
19
|
+
colors: { ink: lightDark('#111', '#eee') }, // theme variable
|
|
20
|
+
gap: cq('8px', '2cqi'), // inlines as `8px`
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const demo = distillery.createStyleModule('demo', ({ css, tokens }) => ({
|
|
25
|
+
root: css`
|
|
26
|
+
color: ${tokens.colors.ink};
|
|
27
|
+
padding: ${tokens.gap};
|
|
28
|
+
`,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
distillery.renderStyles(distillery.stylesheetCollector()); // default readable classes: `.demo-root`
|
|
32
|
+
|
|
33
|
+
const collector = distillery.artifactCollector('compact'); // compact classes and vars: `.a`, `--a`
|
|
34
|
+
collector.use(demo.handles.root);
|
|
35
|
+
distillery.renderStyles(collector);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
1. **Bind.** `createDistillery` takes a prefix and a nested theme tree; it derives `tokens` and `themeVars`.
|
|
41
|
+
2. **Author.** Tagged templates (`css`, `rule`, `media`, `variants`) record every token and var they touch.
|
|
42
|
+
3. **Collect.** A `StylesCollector` keeps the entries a render actually reached. Unused variants and unread custom properties drop out.
|
|
43
|
+
4. **Emit.** `renderStyles` writes CSS in readable or compact names, for a full stylesheet or a single artifact.
|
|
44
|
+
|
|
45
|
+
```mermaid
|
|
46
|
+
flowchart LR
|
|
47
|
+
Env[DistilleryEnvironment] --> Distillery[createDistillery]
|
|
48
|
+
Distillery --> Modules[Style modules]
|
|
49
|
+
Modules --> Collector[StylesCollector]
|
|
50
|
+
Collector --> CSS[renderStyles]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Why Distillate
|
|
54
|
+
|
|
55
|
+
Distillate is for **component library authors** who need the same authored styles to work two ways: a public stylesheet with stable names for host applications, and a compact self-contained CSS payload for HTML that leaves the app (emails, AI reply cards, SVG renders, exported reports).
|
|
56
|
+
|
|
57
|
+
- **Two outputs from one source.** Readable names for apps; compact names for payloads where HTML and CSS travel together.
|
|
58
|
+
- **Reachability, not static analysis.** Collection runs during a real render — class-name resolution is the side effect that builds the collected set. Unused entries and unread theme tokens drop out automatically.
|
|
59
|
+
- **Your tokens.** `tokens` is the tree you passed in. The engine never invents a palette.
|
|
60
|
+
- **Emotion-shaped opt-in.** `@elastic/distillate/emotion` is `css` / `cx` / `injectGlobal` over the same registry for incremental adoption.
|
|
61
|
+
- **Testable CSS.** `@elastic/distillate/testing` asserts every `var(...)` has a matching declaration.
|
|
62
|
+
|
|
63
|
+
See [Distillate vs CSS-in-JS](guides/vs-emotion.md) for a side-by-side comparison with Emotion, CSS Modules, vanilla-extract, and Tailwind.
|
|
64
|
+
|
|
65
|
+
## Next
|
|
66
|
+
|
|
67
|
+
- [Install](getting-started/installation.md) and [quick start](getting-started/quick-start.md)
|
|
68
|
+
- [The output matrix](concepts/naming-and-output.md)
|
|
69
|
+
- [Declare and select variations](guides/theming.md)
|
|
70
|
+
- [Read values outside CSS](guides/non-css-surfaces.md)
|
|
71
|
+
- [Integrate with a React renderer](guides/react-renderer.md)
|
|
72
|
+
- [Playground](https://elastic.github.io/distillate/playground/)
|
|
73
|
+
- [API reference](reference/index.md)
|
package/docs/logo.svg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M25 31H16V10C16 5.02923 20.0292 1 25 1V31Z" fill="#F04E98"/>
|
|
3
|
+
<path d="M1 16C1 24.2846 7.71538 31 16 31L16 16L1 16Z" fill="#1BA9F5"/>
|
|
4
|
+
<path d="M31 25C31 21.6863 28.3137 19 25 19C21.6863 19 19 21.6863 19 25C19 28.3137 21.6863 31 25 31C28.3137 31 31 28.3137 31 25Z" fill="#00BFB3"/>
|
|
5
|
+
<path d="M25 31C21.6863 31 19 28.3137 19 25C19 21.6863 21.6863 19 25 19V31Z" fill="#FF957D"/>
|
|
6
|
+
<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" fill="#FEC514"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Distillate playground</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elastic/distillate-playground",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"test": "vitest run"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"monaco-editor": "^0.56.0",
|
|
12
|
+
"react": "^19.1.1",
|
|
13
|
+
"react-dom": "^19.1.1",
|
|
14
|
+
"sucrase": "^3.35.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^19.1.12",
|
|
18
|
+
"@types/react-dom": "^19.1.9",
|
|
19
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
20
|
+
"vite": "^6.3.6",
|
|
21
|
+
"vitest": "^4.1.11"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
import { type ReactNode, useEffect, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { defaultSnippet, snippetById } from '../examples/snippets';
|
|
11
|
+
import {
|
|
12
|
+
type CompileError,
|
|
13
|
+
compilePlayground,
|
|
14
|
+
type CompileSuccess,
|
|
15
|
+
type PlaygroundMode,
|
|
16
|
+
} from '../lib/compile';
|
|
17
|
+
import { ui } from '../setup/chrome';
|
|
18
|
+
|
|
19
|
+
import { CssPanel } from './css_panel';
|
|
20
|
+
import { EditorPanel } from './editor_panel';
|
|
21
|
+
import { PreviewPanel } from './preview_panel';
|
|
22
|
+
import { TokensPanel } from './tokens_panel';
|
|
23
|
+
import type { CssTab, PreviewTheme } from './types';
|
|
24
|
+
|
|
25
|
+
const COMPILE_DEBOUNCE_MS = 150;
|
|
26
|
+
const { handles: h } = ui;
|
|
27
|
+
|
|
28
|
+
export const App = (): ReactNode => {
|
|
29
|
+
const [mode, setMode] = useState<PlaygroundMode>('emotion');
|
|
30
|
+
const [theme, setTheme] = useState<PreviewTheme>('light');
|
|
31
|
+
const [cssTab, setCssTab] = useState<CssTab>('readable');
|
|
32
|
+
const [snippetId, setSnippetId] = useState(defaultSnippet.id);
|
|
33
|
+
const [sources, setSources] = useState<Record<PlaygroundMode, string>>(
|
|
34
|
+
() => ({ ...defaultSnippet.source })
|
|
35
|
+
);
|
|
36
|
+
const [display, setDisplay] = useState<CompileSuccess | null>(null);
|
|
37
|
+
const [error, setError] = useState<CompileError | null>(null);
|
|
38
|
+
|
|
39
|
+
const source = sources[mode];
|
|
40
|
+
|
|
41
|
+
const onSourceChange = (next: string): void =>
|
|
42
|
+
setSources((current) => ({ ...current, [mode]: next }));
|
|
43
|
+
|
|
44
|
+
const onSnippetChange = (id: string): void => {
|
|
45
|
+
const snippet = snippetById(id);
|
|
46
|
+
if (!snippet) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
setSnippetId(snippet.id);
|
|
50
|
+
setSources({ ...snippet.source });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const onReset = (): void => {
|
|
54
|
+
const snippet = snippetById(snippetId);
|
|
55
|
+
if (!snippet) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
onSourceChange(snippet.source[mode]);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const timer = window.setTimeout(() => {
|
|
63
|
+
const result = compilePlayground(source, mode);
|
|
64
|
+
if (result.ok) {
|
|
65
|
+
setDisplay(result);
|
|
66
|
+
setError(null);
|
|
67
|
+
} else {
|
|
68
|
+
setError(result.error);
|
|
69
|
+
}
|
|
70
|
+
}, COMPILE_DEBOUNCE_MS);
|
|
71
|
+
return () => window.clearTimeout(timer);
|
|
72
|
+
}, [source, mode]);
|
|
73
|
+
|
|
74
|
+
const onModeChange = (next: PlaygroundMode): void => setMode(next);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className={h.shell.readableName}>
|
|
78
|
+
<header className={h.top.readableName}>
|
|
79
|
+
<a className={h.brand.readableName} href="../">
|
|
80
|
+
Distillate
|
|
81
|
+
</a>
|
|
82
|
+
<h1>Playground</h1>
|
|
83
|
+
</header>
|
|
84
|
+
<div className={h.layout.readableName}>
|
|
85
|
+
<div className={h.dockSource.readableName}>
|
|
86
|
+
<EditorPanel
|
|
87
|
+
{...{
|
|
88
|
+
mode,
|
|
89
|
+
snippetId,
|
|
90
|
+
source,
|
|
91
|
+
onModeChange,
|
|
92
|
+
onReset,
|
|
93
|
+
onSnippetChange,
|
|
94
|
+
onSourceChange,
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
<TokensPanel {...{ theme }} />
|
|
98
|
+
</div>
|
|
99
|
+
<div className={h.dockOutput.readableName}>
|
|
100
|
+
<PreviewPanel
|
|
101
|
+
{...{ display, error, theme }}
|
|
102
|
+
onThemeChange={setTheme}
|
|
103
|
+
/>
|
|
104
|
+
<CssPanel {...{ cssTab, display }} onCssTabChange={setCssTab} />
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
import { lazy, type ReactNode, Suspense, useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
import type { CompileSuccess } from '../lib/compile';
|
|
11
|
+
import { formatCss } from '../lib/format_css';
|
|
12
|
+
import { cx, ui } from '../setup/chrome';
|
|
13
|
+
|
|
14
|
+
import type { CssTab } from './types';
|
|
15
|
+
|
|
16
|
+
const { handles: h } = ui;
|
|
17
|
+
|
|
18
|
+
const MonacoCssEditor = lazy(async () => {
|
|
19
|
+
const { MonacoCssEditor: Editor } = await import('./monaco_css_editor');
|
|
20
|
+
return { default: Editor };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export interface CssPanelProps {
|
|
24
|
+
readonly display: CompileSuccess | null;
|
|
25
|
+
readonly cssTab: CssTab;
|
|
26
|
+
readonly onCssTabChange: (tab: CssTab) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cssForTab = (display: CompileSuccess | null, cssTab: CssTab): string => {
|
|
30
|
+
if (!display) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
if (cssTab === 'readable') {
|
|
34
|
+
return display.readableCss;
|
|
35
|
+
}
|
|
36
|
+
if (cssTab === 'compact') {
|
|
37
|
+
return display.compactCss;
|
|
38
|
+
}
|
|
39
|
+
return display.artifactCss;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const formatBytes = (bytes: number): string =>
|
|
43
|
+
`${bytes.toLocaleString('en-US')} B`;
|
|
44
|
+
|
|
45
|
+
const savedLabel = (display: CompileSuccess): string => {
|
|
46
|
+
const { stylesheetBytes, artifactBytes } = display;
|
|
47
|
+
if (stylesheetBytes === 0) {
|
|
48
|
+
return '0% smaller';
|
|
49
|
+
}
|
|
50
|
+
const pct = Math.round((1 - artifactBytes / stylesheetBytes) * 100);
|
|
51
|
+
if (pct === 0) {
|
|
52
|
+
return 'same size';
|
|
53
|
+
}
|
|
54
|
+
if (pct < 0) {
|
|
55
|
+
return `${Math.abs(pct)}% larger`;
|
|
56
|
+
}
|
|
57
|
+
return `${pct}% smaller`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const CssPanel = ({
|
|
61
|
+
display,
|
|
62
|
+
cssTab,
|
|
63
|
+
onCssTabChange,
|
|
64
|
+
}: CssPanelProps): ReactNode => {
|
|
65
|
+
const cssText = cssForTab(display, cssTab);
|
|
66
|
+
const formatted = useMemo(() => formatCss(cssText), [cssText]);
|
|
67
|
+
const value = formatted || '/* No CSS emitted yet. */';
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<section className={cx(h.panel, h.dockCss)}>
|
|
71
|
+
<div className={h.toolbar.readableName}>
|
|
72
|
+
<h2>Generated CSS</h2>
|
|
73
|
+
<div
|
|
74
|
+
className={h.segmented.readableName}
|
|
75
|
+
role="group"
|
|
76
|
+
aria-label="CSS output">
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
aria-pressed={cssTab === 'readable'}
|
|
80
|
+
onClick={() => onCssTabChange('readable')}>
|
|
81
|
+
Readable
|
|
82
|
+
</button>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
aria-pressed={cssTab === 'compact'}
|
|
86
|
+
onClick={() => onCssTabChange('compact')}>
|
|
87
|
+
Compact
|
|
88
|
+
</button>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
aria-pressed={cssTab === 'artifact'}
|
|
92
|
+
onClick={() => onCssTabChange('artifact')}>
|
|
93
|
+
Artifact
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
{display ? (
|
|
98
|
+
<p className={h.cssStats.readableName}>
|
|
99
|
+
<span>Full stylesheet {formatBytes(display.stylesheetBytes)}</span>
|
|
100
|
+
<span aria-hidden="true">·</span>
|
|
101
|
+
<span>Compact artifact {formatBytes(display.artifactBytes)}</span>
|
|
102
|
+
<span aria-hidden="true">·</span>
|
|
103
|
+
<span>{savedLabel(display)}</span>
|
|
104
|
+
<a href="../guides/compact-artifacts">How this is measured</a>
|
|
105
|
+
</p>
|
|
106
|
+
) : null}
|
|
107
|
+
<Suspense
|
|
108
|
+
fallback={
|
|
109
|
+
<div
|
|
110
|
+
className={h.cssOut.readableName}
|
|
111
|
+
data-pg-monaco=""
|
|
112
|
+
aria-busy="true"
|
|
113
|
+
/>
|
|
114
|
+
}>
|
|
115
|
+
<MonacoCssEditor {...{ value }} />
|
|
116
|
+
</Suspense>
|
|
117
|
+
</section>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
import { type ChangeEvent, lazy, type ReactNode, Suspense } from 'react';
|
|
9
|
+
|
|
10
|
+
import { snippetCatalog } from '../examples/snippets';
|
|
11
|
+
import type { PlaygroundMode } from '../lib/compile';
|
|
12
|
+
import { cx, ui } from '../setup/chrome';
|
|
13
|
+
|
|
14
|
+
const { handles: h } = ui;
|
|
15
|
+
|
|
16
|
+
const MonacoSourceEditor = lazy(async () => {
|
|
17
|
+
const { MonacoSourceEditor: Editor } = await import('./monaco_source_editor');
|
|
18
|
+
return { default: Editor };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export interface EditorPanelProps {
|
|
22
|
+
readonly mode: PlaygroundMode;
|
|
23
|
+
readonly snippetId: string;
|
|
24
|
+
readonly source: string;
|
|
25
|
+
readonly onModeChange: (mode: PlaygroundMode) => void;
|
|
26
|
+
readonly onReset: () => void;
|
|
27
|
+
readonly onSnippetChange: (id: string) => void;
|
|
28
|
+
readonly onSourceChange: (source: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const EditorPanel = ({
|
|
32
|
+
mode,
|
|
33
|
+
snippetId,
|
|
34
|
+
source,
|
|
35
|
+
onModeChange,
|
|
36
|
+
onReset,
|
|
37
|
+
onSnippetChange,
|
|
38
|
+
onSourceChange,
|
|
39
|
+
}: EditorPanelProps): ReactNode => {
|
|
40
|
+
return (
|
|
41
|
+
<section className={cx(h.panel, h.editor)}>
|
|
42
|
+
<div className={h.toolbar.readableName}>
|
|
43
|
+
<div
|
|
44
|
+
className={h.segmented.readableName}
|
|
45
|
+
role="group"
|
|
46
|
+
aria-label="Authoring mode">
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
aria-pressed={mode === 'emotion'}
|
|
50
|
+
onClick={() => onModeChange('emotion')}>
|
|
51
|
+
Emotion css
|
|
52
|
+
</button>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
aria-pressed={mode === 'native'}
|
|
56
|
+
onClick={() => onModeChange('native')}>
|
|
57
|
+
Native module
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
<label className={h.snippetSelect.readableName}>
|
|
61
|
+
<span>Example</span>
|
|
62
|
+
<select
|
|
63
|
+
value={snippetId}
|
|
64
|
+
aria-label="Example"
|
|
65
|
+
onChange={(event: ChangeEvent<HTMLSelectElement>) => {
|
|
66
|
+
onSnippetChange(event.target.value);
|
|
67
|
+
}}>
|
|
68
|
+
{snippetCatalog.map((snippet) => (
|
|
69
|
+
<option key={snippet.id} value={snippet.id}>
|
|
70
|
+
{snippet.label}
|
|
71
|
+
</option>
|
|
72
|
+
))}
|
|
73
|
+
</select>
|
|
74
|
+
</label>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
className={h.ghost.readableName}
|
|
78
|
+
onClick={onReset}>
|
|
79
|
+
Reset
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
<Suspense fallback={<div data-pg-monaco="" aria-busy="true" />}>
|
|
83
|
+
<MonacoSourceEditor {...{ mode, source, onSourceChange }} />
|
|
84
|
+
</Suspense>
|
|
85
|
+
</section>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export { App } from './app';
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
import 'monaco-editor/languages/definitions/css/register.js';
|
|
9
|
+
|
|
10
|
+
import { type ReactNode, useEffect, useRef } from 'react';
|
|
11
|
+
import { editor, Uri } from 'monaco-editor/editor.js';
|
|
12
|
+
|
|
13
|
+
import { ui } from '../setup/chrome';
|
|
14
|
+
import { CSS_THEME, ensureMonaco } from '../setup/monaco_env';
|
|
15
|
+
|
|
16
|
+
const { handles: h } = ui;
|
|
17
|
+
|
|
18
|
+
const MODEL_URI = 'file:///playground.css';
|
|
19
|
+
|
|
20
|
+
interface MonacoCssEditorProps {
|
|
21
|
+
readonly value: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const MonacoCssEditor = ({ value }: MonacoCssEditorProps): ReactNode => {
|
|
25
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const host = hostRef.current;
|
|
30
|
+
if (!host) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const initialValue = value;
|
|
34
|
+
ensureMonaco();
|
|
35
|
+
const uri = Uri.parse(MODEL_URI);
|
|
36
|
+
const existing = editor.getModel(uri);
|
|
37
|
+
const model = existing ?? editor.createModel(initialValue, 'css', uri);
|
|
38
|
+
if (existing && existing.getValue() !== initialValue) {
|
|
39
|
+
existing.setValue(initialValue);
|
|
40
|
+
}
|
|
41
|
+
const instance = editor.create(host, {
|
|
42
|
+
model,
|
|
43
|
+
theme: CSS_THEME,
|
|
44
|
+
readOnly: true,
|
|
45
|
+
domReadOnly: true,
|
|
46
|
+
automaticLayout: true,
|
|
47
|
+
minimap: { enabled: false },
|
|
48
|
+
fontSize: 13,
|
|
49
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
|
|
50
|
+
lineHeight: 20,
|
|
51
|
+
tabSize: 2,
|
|
52
|
+
insertSpaces: true,
|
|
53
|
+
scrollBeyondLastLine: false,
|
|
54
|
+
wordWrap: 'on',
|
|
55
|
+
padding: { top: 12, bottom: 12 },
|
|
56
|
+
fixedOverflowWidgets: true,
|
|
57
|
+
ariaLabel: 'Generated CSS',
|
|
58
|
+
renderLineHighlight: 'none',
|
|
59
|
+
overviewRulerLanes: 0,
|
|
60
|
+
hideCursorInOverviewRuler: true,
|
|
61
|
+
contextmenu: false,
|
|
62
|
+
links: false,
|
|
63
|
+
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
|
|
64
|
+
});
|
|
65
|
+
editorRef.current = instance;
|
|
66
|
+
return () => {
|
|
67
|
+
instance.dispose();
|
|
68
|
+
editorRef.current = null;
|
|
69
|
+
model.dispose();
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const instance = editorRef.current;
|
|
75
|
+
if (!instance || instance.getValue() === value) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
instance.setValue(value);
|
|
79
|
+
}, [value]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div ref={hostRef} className={h.cssOut.readableName} data-pg-monaco="" />
|
|
83
|
+
);
|
|
84
|
+
};
|