@theme-registry/refract 0.1.7 → 0.1.9

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.
@@ -146,6 +146,89 @@ export interface UsageDescriptor {
146
146
  /** The real recipe identities in this format, so a downstream agent never guesses a name. */
147
147
  readonly recipes: readonly UsageRecipe[];
148
148
  }
149
+ /** How one recipe is marked up in a preview document. `tag` omitted ⇒ the preview picks a sensible element. */
150
+ export interface PreviewMarkup {
151
+ /** The element to render (`"button"`, `"div"`, …). Omit to let the preview choose by group name. */
152
+ readonly tag?: string;
153
+ /** Attributes to set on it — for CSS, `{ class: "<the real class list>" }`. */
154
+ readonly attrs: Readonly<Record<string, string>>;
155
+ }
156
+ /**
157
+ * How to render a **human-facing** preview of an adapter's built output — the audience-flipped sibling
158
+ * of {@link UsageDescriptor} (which serves the machine-facing `llms.txt` guide). Feeds `preview.html`.
159
+ *
160
+ * The build layer can always render *token* plates on its own (they come from the format-neutral DTCG
161
+ * export), so this describes only the half it cannot know: whether the emitted artifacts are **loadable
162
+ * by a browser as-is**, and if so, in what order and with what markup. That is true for CSS and, today,
163
+ * nothing else — SCSS needs compiling, styled-components emits JS modules, JSON has no rendered form.
164
+ * Those adapters return an empty `stylesheets` with an honest {@link PreviewDescriptor.unavailable}
165
+ * message, and the page degrades to tokens-only instead of rendering unstyled boxes.
166
+ *
167
+ * It is passed the **normalized emit plan and the real emitted file names**, because both answers
168
+ * depend on them: `split` has a load-order contract, `subsystem`/`components` name files through a
169
+ * user-supplied function (so names can never be re-derived from the plan alone), and `components`
170
+ * emits self-contained merged rules keyed by a different class than the composition list every other
171
+ * mode uses.
172
+ */
173
+ export interface PreviewDescriptor {
174
+ /**
175
+ * Emitted file names that a browser can load as-is, **in load order** (e.g. `split` puts its
176
+ * variables file first). Empty ⇒ no live rendering is possible for this format; say why in
177
+ * {@link unavailable}. The build layer intersects this with the files actually written, so a
178
+ * stale name can never produce a page pointing at a missing artifact.
179
+ */
180
+ readonly stylesheets: readonly string[];
181
+ /** Mark up one recipe. Omit ⇒ recipes are listed by name only, never rendered. */
182
+ readonly markup?: (recipe: UsageRecipe) => PreviewMarkup | undefined;
183
+ /**
184
+ * Optional grouping key per recipe, used for page layout only — `subsystem` mode groups by
185
+ * subsystem, `components` mode by the file each component was written to. Omit ⇒ one flat section.
186
+ */
187
+ readonly groupBy?: (recipe: UsageRecipe) => string | undefined;
188
+ /**
189
+ * The attribute to set on `<html>` to force an appearance mode (CSS emits `:root[data-theme="…"]`,
190
+ * so the CSS adapter answers `"data-theme"`). Absent ⇒ the preview renders no mode toggle, because
191
+ * it would have no way to switch. The theme's own `prefers-color-scheme` blocks still apply.
192
+ */
193
+ readonly modeAttribute?: string;
194
+ /** Why a live render isn't possible, in the author's own words. Shown when `stylesheets` is empty. */
195
+ readonly unavailable?: string;
196
+ /** Caveats worth surfacing on the page even when rendering does work (e.g. "`variables: false` — …"). */
197
+ readonly notes?: readonly string[];
198
+ /**
199
+ * A token path → the name the emitted output gives it (`colors.brand` → `--dt-colors-brand`).
200
+ * Shown beside each value, so a reader can go straight from the specimen to the thing they type.
201
+ * Return `undefined` for a path that mints no name.
202
+ */
203
+ readonly tokenName?: (path: string) => string | undefined;
204
+ /**
205
+ * The states a recipe actually declares (`["hover", "focus", "disabled"]`), so the page can render
206
+ * a state matrix instead of one at-rest specimen. Only states that also answer to
207
+ * {@link statePinClass} are rendered — a state nobody can pin can't be shown at rest.
208
+ */
209
+ readonly states?: (recipe: UsageRecipe) => readonly string[];
210
+ /**
211
+ * The class that forces `state` to render at rest. CSS states are pseudo-classes (`:hover`) and
212
+ * attribute selectors (`[disabled]`) that markup cannot switch on, so the adapter emits a parallel
213
+ * pinnable rule (see {@link statePinCss}) and names its class here.
214
+ */
215
+ readonly statePinClass?: (state: string) => string | undefined;
216
+ /**
217
+ * Extra stylesheet the page inlines to make {@link statePinClass} work. Kept out of the emitted
218
+ * theme on purpose: these rules exist only to drive a specimen sheet and must never reach a
219
+ * consumer's bundle.
220
+ */
221
+ readonly statePinCss?: string;
222
+ /**
223
+ * A composed recipe's identity broken into its parts — each emitted class plus the recipe address
224
+ * it came from (`undefined` ⇒ this rule-set's own delta). Rendering the joined string without
225
+ * saying why it's two classes reads as a bug.
226
+ */
227
+ readonly composition?: (recipe: UsageRecipe) => ReadonlyArray<{
228
+ readonly className: string;
229
+ readonly from?: string;
230
+ }> | undefined;
231
+ }
149
232
  /** The Model-derived context bound alongside the Model. */
150
233
  export interface RenderContext<TBreakpoint extends string = string> {
151
234
  /** Core-built media descriptor (breakpoints → `@media` builder) for responsive rules. */
@@ -186,6 +269,14 @@ export interface BoundSpec<TUnit = string> {
186
269
  * that has neither refract nor its skills. Optional: `defineAdapter` supplies a generic default
187
270
  * (recipe identities via `recipeName`); an adapter overrides it to add format-specific import prose. */
188
271
  describeUsage?(): UsageDescriptor;
272
+ /**
273
+ * Describe how to render a human-facing `preview.html` of this emit — see {@link PreviewDescriptor}.
274
+ * Receives the normalized plan and the file names `emit(plan)` actually produced.
275
+ *
276
+ * **Unlike `describeUsage`, this has NO default**: an absent implementation means "I don't know how
277
+ * to show this in a browser", and the preview falls back to token plates only. That's the correct
278
+ * behavior for a third-party adapter that has never heard of previews, so the contract stays additive. */
279
+ describePreview?(plan: NormalizedEmit, files: readonly string[]): PreviewDescriptor;
189
280
  /**
190
281
  * Build-time emit: self-contained files (adapter vendors its own runtime helpers). `plan` is the
191
282
  * normalized output-shape directive (§9); omitted ⇒ `single`/`theme.css` (back-compat). The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theme-registry/refract",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "The framework-agnostic, format-neutral theme compiler core — build a ThemeModel from a RawTheme, then lower it with any adapter package (CSS, SCSS, JSON, styled-components).",
5
5
  "author": {
6
6
  "name": "Petyo Stoyanov",
@@ -57,7 +57,7 @@
57
57
  "./package.json": "./package.json"
58
58
  },
59
59
  "peerDependencies": {
60
- "typescript": ">=5.0.0"
60
+ "typescript": ">=5.0.0 <6"
61
61
  },
62
62
  "peerDependenciesMeta": {
63
63
  "typescript": {
@@ -30,6 +30,16 @@ onto the theme. You author an `AdapterSpec<TUnit>` and wrap it with `defineAdapt
30
30
  - **Optional:** `renderToken`, aggregator overrides (`renderAllRecipes`/`…Variables`/`renderAll`),
31
31
  `extend(theme)` (attach runtime glue), `emit(plan?)` (build-time files — switch on `plan.type`
32
32
  and throw a clear error for modes you don't support).
33
+ - **Optional self-description** (each feeds an opt-in build artifact; both are additive, so an
34
+ adapter that implements neither still works):
35
+ - `describeUsage()` → the machine-facing `guide` (`llms.txt` + `manifest.json`). `defineAdapter`
36
+ supplies a generic default; override it to add format-specific consumption prose.
37
+ - `describePreview(plan, files)` → the human-facing `preview.html`. **No default.** Implement it
38
+ only if a browser can load your emitted files *as-is*; otherwise return
39
+ `{ stylesheets: [], unavailable: "why not" }` and the preview renders token plates plus your
40
+ explanation. Take the file names from the `files` argument — in `subsystem`/`components` mode
41
+ `filename` is a user function, so names can't be re-derived from the plan. See
42
+ **docs/extending.md → Writing `describePreview`**.
33
43
 
34
44
  `defineAdapter(spec)` supplies the default aggregators as pure Model walks — override only if the
35
45
  full document isn't a flat concatenation.
@@ -34,8 +34,11 @@ export default defineConfig({
34
34
  ```
35
35
 
36
36
  `ThemeConfig` = `{ raw, targets, media?, units?, baseFontSize? }`. Each `EmitTarget` =
37
- `{ adapter, outDir, name?, emit?, helpers? }`. Resolution order: `theme.config.ts` → `.mjs` →
38
- `.js` (a `.ts` config lazy-loads the optional `typescript` peer).
37
+ `{ adapter, outDir, name?, emit?, helpers?, guide?, preview? }`. Resolution order: `theme.config.ts` → `.mjs` →
38
+ `.js` (a `.ts` config lazy-loads the optional `typescript` peer — it must be **5.x**; a bare
39
+ `npm i -D typescript` resolves to 7.x, whose main entry doesn't expose the compiler API, and the
40
+ build then fails with `Cannot read properties of undefined (reading 'ESNext')`. A `.mjs`/`.js`
41
+ config never loads it).
39
42
 
40
43
  ## Emit modes (`emit` on a target)
41
44
 
@@ -63,6 +66,41 @@ tree-shake, so the CSS-only modes are redundant).
63
66
 
64
67
  Run via `npx refract <cmd>`. `outDir` resolves relative to the config file's directory.
65
68
 
69
+ ## Emitted extras (`guide` / `preview` on a target)
70
+
71
+ Both are **off by default** and land inside the target's `outDir`, so they travel with any
72
+ distribution form. A build with neither set is byte-identical to before.
73
+
74
+ - `guide: true` → `llms.txt` + `manifest.json` — the **machine-facing** consumption guide (real
75
+ class names / export ids / token paths). Object form: `{ packageName?, llmsFile?, manifestFile? }`.
76
+ - `preview: true` → `preview.html` — the **human-facing** rendered specimen. Object form:
77
+ `{ file?, title?, inline? }`; `inline` defaults **true** (one self-contained shareable file), set
78
+ it `false` to emit relative `<link>`s that reflect a rebuilt stylesheet on refresh.
79
+
80
+ **What `preview` shows depends on the adapter, and you should say so rather than over-promise.**
81
+ Token plates (colours, type ramp, spacing, radii, shadows, breakpoints) render from the
82
+ format-neutral token export, so **every** adapter gets them. *Live* recipe plates need output a
83
+ browser can load as-is — **CSS only** today. An SCSS / styled-components / JSON target still renders
84
+ every token and lists every recipe by its real identity, and states why it can't render them live.
85
+ If a user wants a live design-review page from an SC or SCSS theme, tell them to **add a CSS target
86
+ to the same config** — same recipes through the same core, so it's a faithful specimen.
87
+
88
+ The page is a **style guide**, not a token dump: sticky section rail, colour families as contiguous
89
+ ladders with live WCAG contrast readouts on each `text` pairing, the type ramp set in its own sizes,
90
+ spacing as a measure *and* an applied inset (there is no `padding` token — spacing IS the padding
91
+ scale), a **state matrix** per recipe, an **appearance-mode diff** of the tokens that actually carry
92
+ an override, **composition** broken into its parts, and a prose specimen for the bare elements the
93
+ `globals` subsystem themes. Sections appear only when the theme has tokens of that kind.
94
+
95
+ It follows the target's `emit` mode (variables load before styles in `split`; plates group by
96
+ subsystem or by component file), gains a light/dark toggle when the theme declares `modes`, and
97
+ frame-width buttons when it declares `breakpoints`. A theme with **no recipes** (e.g. straight out of
98
+ `refract create`) renders its tokens and says explicitly that recipes are missing.
99
+
100
+ **State rules never reach the shipped stylesheet.** A CSS pseudo-class can't be triggered from
101
+ markup, so the adapter emits parallel pinnable rules (`.cls.rfp-s-hover`) that are inlined into
102
+ `preview.html` only. If a user asks why those classes aren't in `theme.css`, that's why.
103
+
66
104
  ## Dark-mode strategy (differs by target)
67
105
 
68
106
  - **CSS / SCSS / JSON** — dark rides the *referenced token's own `modes`* (see **theme-foundations**);
@@ -54,6 +54,11 @@ For AI-assisted consumers, refract can emit a **self-documenting guide** into `o
54
54
  the build target (see **build-config**). Because the guide sits in `outDir`, it travels with any
55
55
  distribution form, package specifier or not.
56
56
 
57
+ Its human-facing sibling is `preview: true`, which writes a `preview.html` specimen into the same
58
+ folder — token plates for any adapter, plus live recipe plates when the output is browser-loadable
59
+ (CSS). Reach for it when a person needs to *see* the theme (design review, a handoff, a sanity check
60
+ after a retune) rather than consume it.
61
+
57
62
  While *authoring* against a live project, the **`@theme-registry/refract-mcp`** server exposes the
58
63
  same information as **live tools + resources** (schema 1) — `getClass` / `resolveToken` / `renderRecipe`
59
64
  and a `refract://manifest.json` resource — answering against the real compiled theme. Prefer it when