<p align="center">
 <img width="400" src="https://github.com/paulsputer/svelte-gym/assets/4686906/b06068e8-bdbd-4efa-9155-6ef15f5023c5" />
</p>

# Svelte Gym

**Rapidly create, exercise, and share Svelte component states via URL-encoded permalinks.**

## Why Svelte Gym?

Developing and testing components should be seamless. Svelte Gym provides a playground environment to exercise your components and ensure they respond correctly to various inputs and constraints.

**Key Features:**

- **URL-Driven State:** Every control and property is reflected in the URL. Share a link, share a state.
- **Visual Regression Ready:** Deterministic URLs make it easy to use tools like [BackstopJS](https://github.com/garris/BackstopJS) to detect visual regressions.
- **LLM Friendly:** The URL structure provides a context-rich, text-based representation of your component's state, making it ideal for AI-assisted development and debugging.

### Use Cases

- **Responsive Testing:** How does the component respond when the parent element resizes?
- **Style Verification:** Is the font size on the parent element respected?
- **Edge Cases:** Does text overflow expectedly? Is bad data (null, NaN, undefined) handled gracefully?
- **Collaboration:** Share a specific component state with a colleague or an LLM to reproduce a bug.

## Getting Started

### Installation

```bash
npm install -D svelte-gym
# or
pnpm install -D svelte-gym
# or
bun install -D svelte-gym
```

### Svelte 4 Support

Svelte Gym is written in Svelte 5. If you are still using Svelte 4, you can import the Svelte 4-compatible versions of the components from the `/v4` subpath:

```svelte
<script lang="ts">
	import { TestHarness, restoreProps, GymCheckbox, GymTextbox } from 'svelte-gym/v4';
	// ... rest of your code ...
</script>

<TestHarness>
	<!-- Use slots instead of snippets for Svelte 4 -->
	<div slot="componentToTest">
		<MyComponent {...props} />
	</div>

	<ul slot="controls">
		<li><GymCheckbox bind:props name="isActive" /></li>
	</ul>
</TestHarness>
```

### Basic Usage (Svelte 5)

Create a `+page.svelte` route for your component (e.g., `src/routes/gym/my-component/+page.svelte`):

```svelte
<script lang="ts">
	import { TestHarness, restoreProps, GymCheckbox, GymTextbox } from 'svelte-gym';
	import MyComponent from '$lib/MyComponent.svelte';

	// 1. Define your component properties
	let props = $state({
		label: 'Hello World',
		isActive: true,
		count: 0
	});

	// 2. Restore properties from URL parameters automatically
	// This allows the URL to drive the component state
	restoreProps(props);
</script>

<!-- 3. Wrap your component in the TestHarness -->
<TestHarness>
	{#snippet componentToTest()}
		<MyComponent {...props} />
	{/snippet}

	<!-- 4. Add controls to manipulate props -->
	{#snippet controls()}
		<ul>
			<li><GymCheckbox bind:props name="isActive" /></li>
			<li><GymTextbox bind:props name="label" /></li>
		</ul>
	{/snippet}
</TestHarness>
```

## AI / LLM Workflow & Best Practices

Svelte Gym is designed to be "interpreter-friendly." To ensure the best results and visual parity when using LLMs for component development:

### 1. Minimal `componentToTest` Snippet

The `componentToTest` snippet should **only** contain the component being tested.

> [!IMPORTANT]
> **Avoid wrapping the component in extra `div` or `section` tags.** This ensures that the component's layout and styles are tested in isolation, matching how it will appear when deployed.

**Correct:**

```svelte
{#snippet componentToTest()}
	<MyComponent {...props} />
{/snippet}
```

**Incorrect:**

```svelte
{#snippet componentToTest()}
    <div class="wrapper"> <!-- ❌ DON'T DO THIS -->
        <MyComponent {...props} />
    </div>
```

### 2. Use `Gym*` Input Components

Always use the built-in `Gym` prefixed input components (e.g., `GymSlider`, `GymTextbox`, `GymCheckbox`) in the `controls` snippet.

> [!IMPORTANT]
> **Do not use standard HTML inputs or other custom components.** `Gym*` inputs are specifically designed to handle and test edge cases like `null`, `undefined`, `NaN`, and `Infinity`, which are critical for robust component testing and are not supported by standard inputs.

### 3. URL-Driven State & Reproductions

1.  **Describe the Issue:** "My component breaks when the label is too long."
2.  **Generate a Reproduction:** The LLM can generate a Svelte Gym URL with a long label string encoded in the parameters.
    - _Example:_ `http://localhost:5173/gym/button?label=Super+Long+Label+That+Breaks+Layout`
3.  **Fix and Verify:** You can verify the fix visually, and the URL serves as a regression test case.

## API Reference

### `TestHarness`

The main wrapper for your component playground.

**Props:**

- `maxWidth` (number, optional): Maximum width constraint for the test area.
- `maxHeight` (number, optional): Maximum height constraint for the test area.
- `maxFontSize` (number, optional): Maximum font size for the test area.

**Snippets:**

- `componentToTest`: Place the component you want to test here.
- `controls`: Place `Gym*` controls here to modify `props`.

### `restoreProps(props)`

Synchronizes the URL search parameters with your local `props` object. This must be called in your component's `<script>` section.

### Formatting Controls

You can group your controls into semantic sections in the sidebar by wrapping them in HTML `<article>` tags. The first header element (`<h1>` through `<h6>`) inside the article will be automatically styled as the section's title.

```svelte
{#snippet controls()}
	<article>
		<h1>Basic Settings</h1>
		<ul>
			<li><GymCheckbox bind:props name="isActive" /></li>
			<li><GymTextbox bind:props name="label" /></li>
		</ul>
	</article>

	<article>
		<h1>Advanced Settings</h1>
		<ul>
			<li><GymSlider bind:props name="count" min={0} max={10} /></li>
		</ul>
	</article>
{/snippet}
```

### Controls (`Gym*` Components)

All controls support `bind:props` and a `name` attribute corresponding to the property key in `props`.

- `GymCheckbox`: Boolean toggle.
- `GymTextbox`: String input.
- `GymSlider`: Numeric slider (requires `min`, `max`).
- `GymDropdown`: Select from a list of options.
- `GymRadioGroup`: Radio button group.
- `GymLog`: Displays a log of events (passed as an array of strings).

### JSON Path Support

Svelte Gym supports nested properties using dot notation. This is useful for complex state objects.

```svelte
<script lang="ts">
	let props = $state({
		config: {
			theme: {
				mode: 'dark'
			}
		}
	});
</script>
```

In your controls:

```svelte
<GymDropdown bind:props name="config.theme.mode" options={['light', 'dark']} />
```

## Support

<a href="https://www.buymeacoffee.com/sveltegym" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

## ESLint Plugin

Svelte Gym ships with an ESLint plugin (`eslint-plugin-svelte-gym`) to help catch common test harness setup mistakes at lint time.

### Installation

```bash
# The plugin is included in the svelte-gym package
npm install -D eslint-plugin-svelte-gym
```

Or reference it locally if using the source:

```json
{
	"devDependencies": {
		"eslint-plugin-svelte-gym": "file:./eslint-plugin"
	}
}
```

### ESLint Configuration

Add to your `.eslintrc.cjs`:

```js
module.exports = {
	overrides: [
		{
			files: ['*.svelte'],
			plugins: ['svelte-gym'],
			rules: {
				'svelte-gym/require-restore-props': 'warn',
				'svelte-gym/no-duplicate-prop-names': 'warn',
				'svelte-gym/require-props-state': 'error',
				'svelte-gym/single-component-in-test': 'error'
			}
		}
	]
};
```

### Rules

#### `svelte-gym/require-restore-props` ⚠️

Warns when a file imports `TestHarness` but never calls `restoreProps()`. Without this call, URL parameters won't be restored into component state.

**Correct:**

```svelte
<script>
	import { TestHarness, restoreProps } from 'svelte-gym';
	let props = $state({ label: 'Hello' });
	restoreProps(props);
</script>
```

**Incorrect:**

```svelte
<script>
	import { TestHarness } from 'svelte-gym'; // ⚠️ Missing restoreProps
	let props = $state({ label: 'Hello' });
</script>
```

#### `svelte-gym/no-duplicate-prop-names` ⚠️

Warns when multiple `Gym*` components use the same `name` prop. Duplicate names cause permalink parameter collisions.

**Incorrect:**

```svelte
<GymTextbox bind:props name="label" />
<GymSlider bind:props name="label" />
<!-- ⚠️ Duplicate "label" -->
```

#### `svelte-gym/require-props-state` ❌

Errors when `restoreProps(props)` is called but `props` was not declared with `$state()`. Without `$state()`, restored values won't trigger Svelte reactivity.

**Correct:**

```svelte
<script>
	let props = $state({ count: 0 }); // ✅ Uses $state
	restoreProps(props);
</script>
```

**Incorrect:**

```svelte
<script>
	let props = { count: 0 }; // ❌ Not reactive
	restoreProps(props);
</script>
```

#### `svelte-gym/single-component-in-test` ❌

Errors when the `componentToTest` snippet contains more than one element, or when its single element is a plain HTML element (not a component). This prevents wrapper `<div>` or `<section>` tags that make things look correct in the harness but behave differently in production.

**Correct:**

```svelte
{#snippet componentToTest()}
	<MyComponent {...props} />
{/snippet}
```

**Incorrect:**

```svelte
{#snippet componentToTest()}
	<div class="wrapper">
		<!-- ❌ HTML wrapper -->
		<MyComponent {...props} />
	</div>
{/snippet}
```

## Known Issues

- **Type definitions generated as `Record<string, never>`:** If your published package emits broken `.d.ts` files where all component props are typed as `Record<string, never>`, ensure your `peerDependencies.svelte` is set to `"^5.0.0"` (not `">=3.0.0"`). The `@sveltejs/package` type generator uses this field to select the correct type shims — a range that intersects with Svelte 3 causes it to use legacy shims that don't understand `$props()`. See [sveltejs/kit#12972](https://github.com/sveltejs/kit/issues/12972) for details.

## License

MIT
