Skip to content

Commit 396c428

Browse files
Merge pull request #24 from minimaldesign/components-cleanup
Add interactive component playground (Button pilot) plus docs cleanup
2 parents 6c4f894 + c6afb0b commit 396c428

31 files changed

Lines changed: 1197 additions & 207 deletions

agents/components.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
- **PascalCase** for all component filenames: `ComponentName.astro`, `ComponentName.jsx`.
2626
- **`_` prefix** for internal/private components (not part of the public Astro component library; used only in blog posts or docs): `_ColorPickerOklch.jsx`, `_GridDemo.jsx`.
27+
- **`_Playground.jsx`** is the generic, config-driven playground island for component docs pages (template string with `{classes}`/`{attrs}`/`{name}` placeholders, grouped control schema). Authoring API is documented in `docs/playground.md`; reuse it instead of building per-component demo islands.
2728

2829
## Layouts
2930

dist/css/component.field.css

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,19 @@ xxl: 1920px
5353

5454
/* Native validation: reveal the error message once the control has been
5555
interacted with and is invalid (:user-invalid, not :invalid, so pristine
56-
forms don't light up red). The border color comes from elements.form.css. */
57-
.field:has(:user-invalid) .field_error {
56+
forms don't light up red). The border color comes from elements.form.css.
57+
.is-invalid forces the same presentation from markup, for server-side
58+
validation results, tests, and demos. */
59+
:is(.field:has(:user-invalid),.field.is-invalid) .field_error {
5860
display: block;
5961
}
6062

61-
.field:has(:user-invalid):has(.field_error) .field_hint {
63+
:is(.field:has(:user-invalid),.field.is-invalid):has(.field_error) .field_hint {
6264
display: none;
6365
}
6466

67+
.field.is-invalid :is(input, select, textarea) {
68+
border-color: var(--input-border-color-invalid);
69+
}
70+
6571
}

dist/mcss.css

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3752,14 +3752,20 @@ details {
37523752

37533753
/* Native validation: reveal the error message once the control has been
37543754
interacted with and is invalid (:user-invalid, not :invalid, so pristine
3755-
forms don't light up red). The border color comes from elements.form.css. */
3756-
.field:has(:user-invalid) .field_error {
3755+
forms don't light up red). The border color comes from elements.form.css.
3756+
.is-invalid forces the same presentation from markup, for server-side
3757+
validation results, tests, and demos. */
3758+
:is(.field:has(:user-invalid),.field.is-invalid) .field_error {
37573759
display: block;
37583760
}
37593761

3760-
.field:has(:user-invalid):has(.field_error) .field_hint {
3762+
:is(.field:has(:user-invalid),.field.is-invalid):has(.field_error) .field_hint {
37613763
display: none;
37623764
}
3765+
3766+
.field.is-invalid :is(input, select, textarea) {
3767+
border-color: var(--input-border-color-invalid);
3768+
}
37633769
}
37643770

37653771
@layer components{

dist/mcss.min.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/playground.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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

Comments
 (0)