|
| 1 | +# Component playground |
| 2 | + |
| 3 | +Component docs pages can embed an interactive playground (see the [button page](https://minimaldesign.github.io/mCSS/components/button)): toggle the available modifiers, watch the element update live, and copy the exact markup. It's powered by a single Preact island configured entirely from MDX, so adding a playground to a page is a config block, not a new component. |
| 4 | + |
| 5 | +## Files |
| 6 | + |
| 7 | +| File | Role | |
| 8 | +| ---- | ---- | |
| 9 | +| `src/components/_Playground.jsx` | The island. Generic, config-driven, no per-component code. | |
| 10 | +| `src/styles/site/component.playground.css` | Site-only styles, registered unlayered in `src/styles/_global.css`. | |
| 11 | +| `src/content/components/button.mdx` | Reference usage (the pilot page). | |
| 12 | + |
| 13 | +All props must be serializable: they cross the Astro island boundary (`client:visible`), so functions can't be passed. That constraint shapes the whole API. |
| 14 | + |
| 15 | +## Template |
| 16 | + |
| 17 | +The `template` prop is an HTML string with placeholders. One substitution pass renders both the live preview and the generated code, so the two can never drift. |
| 18 | + |
| 19 | +- `{classes}` becomes `baseClasses` plus the classes contributed by active controls. |
| 20 | +- `{attrs}` becomes the active boolean attributes, with a leading space (empty when none). |
| 21 | +- `{name}` becomes the value of the text control or snippet toggle named `name`. |
| 22 | + |
| 23 | +Pass it as a quoted JS string so MDX doesn't parse `{classes}` as an expression: |
| 24 | + |
| 25 | +```jsx |
| 26 | +import Playground from "../../components/_Playground.jsx"; |
| 27 | + |
| 28 | +<Playground |
| 29 | + client:visible |
| 30 | + template={'<button class="{classes}"{attrs}>{label}</button>'} |
| 31 | + baseClasses="bt" |
| 32 | + controls={[/* see below */]} |
| 33 | +/> |
| 34 | +``` |
| 35 | + |
| 36 | +Text control values are HTML-escaped before substitution (they're user-typed). The template itself is trusted, it's authored in MDX like the rest of the page. |
| 37 | + |
| 38 | +## Controls |
| 39 | + |
| 40 | +The `controls` prop is an array of groups, each `{ heading, items }`. Groups become grid columns: 2 on mobile, 4 from the `--md` breakpoint (768px) up. A flat array of controls also works: consecutive ungrouped controls collect into one implicit group. |
| 41 | + |
| 42 | +Control types: |
| 43 | + |
| 44 | +| Control | Behavior | |
| 45 | +| ------- | -------- | |
| 46 | +| `{ type: "select", name, options: [{ label, value }], default }` | Mutually exclusive modifier group. Renders as radio buttons up to 4 options (`RADIO_MAX_OPTIONS` in `_Playground.jsx`), as a `<select>` above that. | |
| 47 | +| `{ type: "checkbox", name, label, value: "bt-outline" }` | Boolean modifier: adds `value` to `{classes}` when checked. | |
| 48 | +| `{ type: "checkbox", name, label, attr: "disabled" }` | Boolean attribute: adds `attr` to `{attrs}` when checked. | |
| 49 | +| `{ type: "checkbox", name, label, snippet: "icon" }` | Substitutes the named snippet (or an empty string) for `{name}`. | |
| 50 | +| `{ type: "text", name, label, default }` | Free text, HTML-escaped before substitution in both preview and code. | |
| 51 | + |
| 52 | +Control `label`s are optional: skip them when a group `heading` already says the same thing. |
| 53 | + |
| 54 | +Select option fields, beyond `label` and `value`: |
| 55 | + |
| 56 | +- `value` is the option's state key and doubles as the class it contributes to `{classes}`. When they differ, set an explicit `class` (usually `""`), e.g. the Avatar page's initials-vs-image options. |
| 57 | +- `attr` contributes an attribute string to `{attrs}` while selected, e.g. the Feature Grid page's `col-lg="3"` options. |
| 58 | +- `snippet` names a key in the snippets map. It requires the control to declare `snippet: "<placeholder>"`; the selected option's markup fills that placeholder, e.g. the Notice page's per-type icon. |
| 59 | + |
| 60 | +## Snippets and dark surfaces |
| 61 | + |
| 62 | +- `snippets={{ icon: { preview: mailSvgRaw, code: "<svg>[…]</svg> " } }}` gives snippets different markup for the live preview (the real SVG, imported with `?raw`) and the code panel (the abbreviated form used across the docs). |
| 63 | +- Snippets referenced by a control substitute only while that control is active. Snippets no control consumes substitute `{name}` statically, for constant markup like the Social Media page's icons. |
| 64 | +- An option or checkbox control can set `previewSurface: "dark"` to render the preview on a dark surface while active, which keeps `.bt-white` and friends visible. |
| 65 | + |
| 66 | +## Example |
| 67 | + |
| 68 | +The button pilot, abbreviated: |
| 69 | + |
| 70 | +```jsx |
| 71 | +<Playground |
| 72 | + client:visible |
| 73 | + template={'<button class="{classes}"{attrs}>{icon}{label}</button>'} |
| 74 | + baseClasses="bt" |
| 75 | + snippets={{ icon: { preview: mail, code: "<svg>[…]</svg> " } }} |
| 76 | + controls={[ |
| 77 | + { heading: "Size", items: [ |
| 78 | + { type: "select", name: "size", default: "", options: [ |
| 79 | + { label: "Default", value: "" }, |
| 80 | + { label: "Medium", value: "bt-md" }, |
| 81 | + { label: "Large", value: "bt-lg" }, |
| 82 | + ]}, |
| 83 | + ]}, |
| 84 | + { heading: "Modifiers", items: [ |
| 85 | + { type: "checkbox", name: "outline", label: "Outline", value: "bt-outline", default: false }, |
| 86 | + { type: "checkbox", name: "icon", label: "Icon", snippet: "icon", default: false }, |
| 87 | + ]}, |
| 88 | + { heading: "Content", items: [ |
| 89 | + { type: "text", name: "label", label: "Label", default: "Button" }, |
| 90 | + { type: "checkbox", name: "disabled", label: "Disabled", attr: "disabled", default: false }, |
| 91 | + ]}, |
| 92 | + ]} |
| 93 | +/> |
| 94 | +``` |
| 95 | + |
| 96 | +## Not built yet (on purpose) |
| 97 | + |
| 98 | +- A `variant` control selecting among named templates, e.g. `<button>` vs `<a role="button">`. |
| 99 | +- A CSS output tab next to the HTML one. |
| 100 | +- Per-page column overrides. If a page ever needs them, the plan is a `columns` prop setting custom properties consumed by the CSS, not runtime media queries. |
0 commit comments