Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 69 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"devDependencies": {
"@studiometa/oxlint-config": "0.2.0",
"@vitest/coverage-v8": "4.0.18",
"happy-dom": "^20.8.3",
"oxlint": "1.52.0",
"vitest": "4.0.18"
}
Expand Down
121 changes: 121 additions & 0 deletions packages/playground-preview/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# @studiometa/playground-preview

A lightweight web component to embed [`@studiometa/playground`](https://github.com/studiometa/playground) previews anywhere — framework-agnostic, zero configuration.

## Installation

```bash
npm install @studiometa/playground-preview
```

## Usage

### Auto-registration

Import the package to automatically register the `<playground-preview>` custom element:

```js
import '@studiometa/playground-preview';
```

### Manual registration

Import the class and register it yourself with a custom tag name:

```js
import { PlaygroundPreview } from '@studiometa/playground-preview/element';

customElements.define('my-playground', PlaygroundPreview);
```

### Short content via attributes

```html
<playground-preview
html="<h1>Hello</h1>"
script="console.log('hi')"
css="h1 { color: red }"
></playground-preview>
```

### Long content via `<script>` children

For longer code snippets, use `<script>` elements with custom `type` attributes. The browser won't execute them, and the component reads their `textContent`:

```html
<playground-preview height="80vh" theme="dark">
<script type="playground/html">
<div class="flex items-center gap-4">
<h1>Hello World</h1>
<p>Some longer content here...</p>
</div>
</script>

<script type="playground/script">
import { Base, createApp } from '@studiometa/js-toolkit';

class App extends Base {
static config = { name: 'App' };
mounted() { console.log('mounted!'); }
}

export default createApp(App);
</script>

<script type="playground/css">
@import "tailwindcss";
h1 { @apply text-4xl font-bold; }
</script>
</playground-preview>
```

When both attributes and `<script>` children are provided for the same language, children take precedence.

## Attributes

| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `html` | `string` | `""` | HTML content |
| `script` | `string` | `""` | JavaScript content |
| `css` | `string` | `""` | CSS content |
| `base-url` | `string` | `https://studiometa-playground.pages.dev` | Playground instance URL |
| `height` | `string` | `60vh` | Container height |
| `zoom` | `number` | `0.9` | Initial iframe scale |
| `theme` | `string` | `auto` | `dark`, `light`, or `auto` (uses `prefers-color-scheme`) |
| `no-controls` | `boolean` | `false` | Hide zoom/reload/open controls |
| `header` | `string` | — | Passed through to the playground URL |

## Controls

When `no-controls` is not set, the component displays a toolbar with:

- **Zoom in / out / reset** — adjust the iframe scale
- **Reload** — re-creates the iframe
- **Open in new window** — opens the full playground with editors enabled

## Theming

The component uses Shadow DOM for style encapsulation. You can customize its appearance via CSS custom properties:

```css
playground-preview {
--pg-bg: #f4f4f5;
--pg-bg-dark: #27272a;
--pg-controls-bg: rgba(0, 0, 0, 0.55);
--pg-controls-bg-hover: rgba(0, 0, 0, 0.75);
--pg-controls-color: #fff;
--pg-border-color: #e4e4e7;
--pg-border-color-dark: #3f3f46;
--pg-border-radius: 8px;
--pg-loader-color: #a1a1aa;
--pg-transition-duration: 200ms;
}
```

## Lazy loading

The iframe is only created when the component scrolls into the viewport (using `IntersectionObserver` with a `100px` root margin). A spinner is displayed while the iframe loads.

## License

MIT
56 changes: 56 additions & 0 deletions packages/playground-preview/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@studiometa/playground-preview",
"version": "0.3.1",
"type": "module",
"description": "A web component to embed @studiometa/playground previews anywhere",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/studiometa/playground.git"
},
"keywords": [
"playground",
"web-component",
"custom-element",
"preview",
"embed",
"code"
],
"author": "Studio Meta <agence@studiometa.fr> (https://www.studiometa.fr/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/studiometa/playground/issues"
},
"homepage": "https://github.com/studiometa/playground#readme",
"files": [
"dist/"
],
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./element": {
"types": "./dist/element.d.ts",
"import": "./dist/element.js"
}
},
"scripts": {
"prebuild": "rm -rf dist",
"build": "node scripts/build.js && tsc --build tsconfig.build.json",
"watch": "node scripts/watch.js"
},
"devDependencies": {
"esbuild-wasm": "^0.24.2",
"fflate": "^0.8.2",
"typescript": "5.8.3"
},
"engines": {
"node": ">=20.11.0"
}
}
31 changes: 31 additions & 0 deletions packages/playground-preview/scripts/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { resolve } from 'node:path';
import esbuild from 'esbuild-wasm';

const root = resolve(import.meta.dirname, '..');

const start = performance.now();
console.log('Building...');

const { errors, warnings } = await esbuild.build({
entryPoints: [
resolve(root, 'src/index.ts'),
resolve(root, 'src/element.ts'),
],
write: true,
outdir: resolve(root, 'dist'),
target: 'es2022',
format: 'esm',
sourcemap: true,
bundle: true,
});

for (const error of errors) {
console.error(error);
}

for (const warning of warnings) {
console.log(warning);
}

const duration = (performance.now() - start).toFixed(2);
console.log(`Done building in ${duration}ms!`);
Loading
Loading