Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
6 changes: 0 additions & 6 deletions .claude/skills/create-component/SKILL.md

This file was deleted.

73 changes: 73 additions & 0 deletions .claude/skills/create-functional-component/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
name: create-functional-component
description: Create Skeleton functional components in both React and Svelte packages using the repository's anatomy/modules conventions.
---

# Create Functional Component

Scaffold a new framework component in both Skeleton packages using the existing anatomy/modules authoring style.

## Where things live

- React components: [packages/skeleton-react/src/components/](packages/skeleton-react/src/components/)
- Svelte components: [packages/skeleton-svelte/src/components/](packages/skeleton-svelte/src/components/)
- React component tests: [packages/skeleton-react/test/components/](packages/skeleton-react/test/components/)
- Svelte component tests: [packages/skeleton-svelte/test/components/](packages/skeleton-svelte/test/components/)
- Public package exports: [packages/skeleton-react/src/index.ts](packages/skeleton-react/src/index.ts), [packages/skeleton-svelte/src/index.ts](packages/skeleton-svelte/src/index.ts)
- Reference components to model after:
- Static/non-machine: [app-bar](packages/skeleton-react/src/components/app-bar/index.ts), [app-bar](packages/skeleton-svelte/src/components/app-bar/index.ts)
- Machine-backed: [accordion](packages/skeleton-react/src/components/accordion/index.ts), [accordion](packages/skeleton-svelte/src/components/accordion/index.ts)

## File destination rules

Recommend structure based on component complexity, explain the reasoning, accept overrides.

- **Simple/static component** → create `anatomy/*`, `modules/anatomy.ts`, and `index.ts` in both framework folders.
- **Machine-backed component** → add provider/context modules in both frameworks:
- React: `modules/provider.ts`, context files as needed.
- Svelte: `modules/provider.svelte.ts`, context files as needed.
- **Always mirror APIs** across React and Svelte (part names, prop interface names, namespace members).

## Conventions (from app-bar / accordion / dialog)

- Folder is kebab-case; exported namespace is PascalCase (`app-bar` -> `AppBar`).
- Keep anatomy/modules split:
- `anatomy/*` = renderable parts
- `modules/*` = context/providers/namespace composition
- `index.ts` = type exports + namespace export
- Use `PropsWithElement` + `HTMLAttributes` in part prop interfaces.
- Build attributes with `mergeProps` and render with element override fallback.
- React anatomy files are `.tsx`; `index.ts` type exports reference `.jsx` paths.
- Svelte anatomy files are `.svelte`; interfaces are in `<script lang="ts" module>`.
- Namespace composition uses `Object.assign(Root, { ...parts })` in `modules/anatomy.ts`.
- For static parts, include `data-scope` and `data-part` attributes.

## Prompt flow
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks awesome. Only bit of feedback is perhaps prompt users for a Zag documentation URL. Then notify the LLM here how to convert to the machine-friendly version:

You might even try and include the Github source for the component using a similar pattern, so it can have a deep level of understanding of how this operates.

Just to make this an explicit part of the user<->llm transaction upfront.


Ask one at a time, confirm each, write nothing until all are answered and the user confirms.

1. **Component name** — kebab-case folder and PascalCase namespace. Recommend both, accept overrides.
2. **Type** — static/simple or machine-backed (Zag). This resolves required module files.
3. **Parts list** — root-only or full anatomy list (`root`, `trigger`, `content`, etc.).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Step 3 and 4 feels like something the LLM could likely suggest for us if provided the documentation mentioned above. Just accept user overrides.

4. **Elements per part** — default HTML tag for each part (`div`, `button`, `header`, etc.).
5. **Exports** — whether to include `Provider`, `Context`, and `useX` public exports.
6. **Confirm** — show planned file tree for both frameworks and namespace API before writing.

## After scaffolding

1. Add new component re-exports to [packages/skeleton-react/src/index.ts](packages/skeleton-react/src/index.ts) and [packages/skeleton-svelte/src/index.ts](packages/skeleton-svelte/src/index.ts).
2. Keep export ordering consistent with neighboring component entries.
3. Add tests in both frameworks:

- React fixture: `packages/skeleton-react/test/components/<component>.tsx`
- React suite: `packages/skeleton-react/test/components/<component>.test.tsx`
- Svelte fixture: `packages/skeleton-svelte/test/components/<component>.svelte`
- Svelte suite: `packages/skeleton-svelte/test/components/<component>.test.ts`

4. For each anatomy part, include a render assertion (`toBeInTheDocument`).
5. If the component should be previewable in the playgrounds, add matching demo entries in both frameworks and regenerate the React route manifest if a new route was added.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing prompts the user if playground should be created, so might be worth being a default on, user opt-out?

6. If requested, trigger `/create-doc` using the new component slug for framework docs scaffolding.

## Final summary

List: files created in each framework, namespace members exported, provider/context/hook files added (if any), top-level index exports added, and test files/coverage added (including prop delegation).
79 changes: 79 additions & 0 deletions .claude/skills/create-playground-entry/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
name: create-playground-entry
description: Create or update Skeleton React and Svelte playground component pages for a given component, including route generation steps.
---

# Create Playground Entry

Create playground pages for a component in one or both playground apps.

Use this when a component needs a hands-on demo page under `components/<slug>` in:

- [playgrounds/skeleton-react/](playgrounds/skeleton-react/)
- [playgrounds/skeleton-svelte/](playgrounds/skeleton-svelte/)

## Where things live

- React playground routes: [playgrounds/skeleton-react/src/routes/components/](playgrounds/skeleton-react/src/routes/components/)
- Svelte playground routes: [playgrounds/skeleton-svelte/src/routes/components/](playgrounds/skeleton-svelte/src/routes/components/)
- React route shell and nav list source: [playgrounds/skeleton-react/src/routes/\_\_root.tsx](playgrounds/skeleton-react/src/routes/__root.tsx)
- Svelte route shell and nav list source: [playgrounds/skeleton-svelte/src/routes/+layout.svelte](playgrounds/skeleton-svelte/src/routes/+layout.svelte)

## Important behavior

- Both playgrounds auto-build the component nav from filesystem globs in their root layouts.
- Adding a route file in the right folder is enough to make it appear in the sidebar.

## File destination rules

Recommend destination from component slug and target framework(s), explain why, then accept overrides.

- Component slug `my-component`:
- React file: `playgrounds/skeleton-react/src/routes/components/my-component/index.tsx`
- Svelte file: `playgrounds/skeleton-svelte/src/routes/components/my-component/+page.svelte`
- Usually create both files for framework components.
- If the component exists in only one framework, create only that framework's playground page.

## Conventions (from existing playground entries)

- Route folder is kebab-case and mirrors component slug.
- React files:
- Include `'use client';`
- Import component(s) from `@skeletonlabs/skeleton-react`
- Export `Route` with `createFileRoute('/components/<slug>/')`
- Define `function Page()` and return demo JSX
- Svelte files:
- Start with `<script lang="ts">`
- Import component(s) from `@skeletonlabs/skeleton-svelte`
- Exported route is filesystem-based (`+page.svelte`), no explicit route helper
- Use matching demo structure with idiomatic Svelte syntax
- Demo scope should be aligned across frameworks for parity.

Reference files:

- Simple: [playgrounds/skeleton-react/src/routes/components/accordion/index.tsx](playgrounds/skeleton-react/src/routes/components/accordion/index.tsx), [playgrounds/skeleton-svelte/src/routes/components/accordion/+page.svelte](playgrounds/skeleton-svelte/src/routes/components/accordion/+page.svelte)
- Multi-section: [playgrounds/skeleton-react/src/routes/components/carousel/index.tsx](playgrounds/skeleton-react/src/routes/components/carousel/index.tsx), [playgrounds/skeleton-svelte/src/routes/components/carousel/+page.svelte](playgrounds/skeleton-svelte/src/routes/components/carousel/+page.svelte)

## Prompt flow

Ask one at a time, confirm each answer, write nothing until all are answered and the user confirms.

1. **Component slug** - kebab-case route segment (for example `floating-panel`).
Copy link
Copy Markdown
Contributor

@endigo9740 endigo9740 Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When folks are using LLMs they're going to likely use natural language and write "Floating Panel" or "floating panel" or " I want to add a floating panel component". So allow any input, but let the LLM coerce to specific kabab format here. Again user confirmation is good.

Same deal for the other skill btw.

2. **Framework targets** - `react`, `svelte`, or both (recommend both when the component exists in both packages).
3. **Demo scope** - minimal single example or multi-section showcase.
4. **Feature coverage** - list the states/variants/interactions to include.
5. **Parity level** - strict parity or framework-idiomatic differences.
6. **Confirm** - show planned files and summarize route/codegen actions before writing.

## After scaffolding

1. Ensure the route files compile in each target playground.
2. Regenerate React route tree after new or renamed React route files:
- `pnpm --filter @skeletonlabs/playground-skeleton-react exec tsr generate`
3. If requested, run checks:
- React: `pnpm --filter @skeletonlabs/playground-skeleton-react run check`
- Svelte: `pnpm --filter @skeletonlabs/playground-skeleton-svelte run check`

## Final summary

List: framework targets, files created/updated, whether React route generation was run, and any intentional parity differences between React and Svelte demos.
19 changes: 19 additions & 0 deletions packages/skeleton-common/src/components/qr-code.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[data-scope='qr-code'] {
&[data-part='root'] {
display: grid;
gap: --spacing(2);
}
&[data-part='frame'] {
Copy link
Copy Markdown
Contributor

@endigo9740 endigo9740 Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to include an explicit example of modifying the color of the frame/pattern parts, as they require different and specific properties.

Maybe brand color for the frame, brand contrast for the pattern.

background-color: var(--color-surface-50);
}
&[data-part='pattern'] {
fill: var(--color-surface-950);
}
&[data-part='overlay'] {
background-color: var(--color-surface-900-100);
color: var(--color-surface-100-900);
}
&[data-part='download-trigger'] {
@apply btn preset-filled;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was meaning to talk to you about the use of presets in our components. While I think btn is fine for adding form, I think presets may have gotchas. The reason being is preset classes don't "swap" like the used to, they override.

If you were to apply apply class="preset-outlined-primary-500" on this button as a user, you would expect a transparent background with a border edge of the colors defined. But what you get instead is a filled button with an outline added.

One of two things needs to happen:

  1. We need to prune presets from the stylesheet and keep those in user-land only. A breaking change, but my recommendation.
  2. We need to add an implicit bg-transparent on the outline presets.

I'd have real concern for number 2 as it may unintentionally break this pattern:

preset-tonal preset-outlined-surface-200-800

This would result in a tonal fill but outlined edge. Aka what we used to call "ghost" styling in v2. But if we add an explicit transparent bg, it may render this unviable.

}
}
1 change: 1 addition & 0 deletions packages/skeleton-common/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@import './components/navigation.css';
@import './components/pagination.css';
@import './components/progress.css';
@import './components/qr-code.css';
@import './components/rating-group.css';
@import './components/segmented-control.css';
@import './components/slider.css';
Expand Down
1 change: 1 addition & 0 deletions packages/skeleton-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@zag-js/pagination": "catalog:",
"@zag-js/popover": "catalog:",
"@zag-js/progress": "catalog:",
"@zag-js/qr-code": "catalog:",
"@zag-js/radio-group": "catalog:",
"@zag-js/rating-group": "catalog:",
"@zag-js/react": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { HTMLAttributes } from '../../../internal/html-attributes.js';
import type { PropsWithElement } from '../../../internal/props-with-element.js';
import type { DownloadTriggerProps } from '@zag-js/qr-code';
import { RootContext } from '../modules/root-context.js';
import { mergeProps } from '@zag-js/react';
import { use } from 'react';

export interface QrCodeDownloadTriggerProps extends DownloadTriggerProps, PropsWithElement<'button'>, HTMLAttributes<'button'> {}

export default function DownloadTrigger(props: QrCodeDownloadTriggerProps) {
const qrCode = use(RootContext);

const { mimeType, fileName, quality, element, children, ...rest } = props;

const attributes = mergeProps(qrCode.getDownloadTriggerProps({ mimeType, fileName, quality }), rest);

return element ? element(attributes) : <button {...attributes}>{children}</button>;
}
17 changes: 17 additions & 0 deletions packages/skeleton-react/src/components/qr-code/anatomy/frame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { HTMLAttributes } from '../../../internal/html-attributes.js';
import type { PropsWithElement } from '../../../internal/props-with-element.js';
import { RootContext } from '../modules/root-context.js';
import { mergeProps } from '@zag-js/react';
import { use } from 'react';

export interface QrCodeFrameProps extends PropsWithElement<'svg'>, HTMLAttributes<'svg'> {}

export default function Frame(props: QrCodeFrameProps) {
const qrCode = use(RootContext);

const { element, children, ...rest } = props;

const attributes = mergeProps(qrCode.getFrameProps(), rest);

return element ? element(attributes) : <svg {...attributes}>{children}</svg>;
}
17 changes: 17 additions & 0 deletions packages/skeleton-react/src/components/qr-code/anatomy/overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { HTMLAttributes } from '../../../internal/html-attributes.js';
import type { PropsWithElement } from '../../../internal/props-with-element.js';
import { RootContext } from '../modules/root-context.js';
import { mergeProps } from '@zag-js/react';
import { use } from 'react';

export interface QrCodeOverlayProps extends PropsWithElement<'div'>, HTMLAttributes<'div'> {}

export default function Overlay(props: QrCodeOverlayProps) {
const qrCode = use(RootContext);

const { element, children, ...rest } = props;

const attributes = mergeProps(qrCode.getOverlayProps(), rest);

return element ? element(attributes) : <div {...attributes}>{children}</div>;
}
17 changes: 17 additions & 0 deletions packages/skeleton-react/src/components/qr-code/anatomy/pattern.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { HTMLAttributes } from '../../../internal/html-attributes.js';
import type { PropsWithElement } from '../../../internal/props-with-element.js';
import { RootContext } from '../modules/root-context.js';
import { mergeProps } from '@zag-js/react';
import { use } from 'react';

export interface QrCodePatternProps extends PropsWithElement<'path'>, HTMLAttributes<'path'> {}

export default function Pattern(props: QrCodePatternProps) {
const qrCode = use(RootContext);

const { element, ...rest } = props;

const attributes = mergeProps(qrCode.getPatternProps(), rest);

return element ? element(attributes) : <path {...attributes} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ReactNode } from 'react';
import { use } from 'react';
import type { useQrCode } from '../modules/provider.js';
import { RootContext as RootContext_ } from '../modules/root-context.js';

export interface QrCodeRootContextProps {
children: (qrCode: ReturnType<typeof useQrCode>) => ReactNode;
}

export default function RootContext(props: QrCodeRootContextProps) {
const qrCode = use(RootContext_);

const { children } = props;

return children(qrCode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { HTMLAttributes } from '../../../internal/html-attributes.js';
import type { PropsWithElement } from '../../../internal/props-with-element.js';
import type { useQrCode } from '../modules/provider.js';
import { RootContext } from '../modules/root-context.js';
import { mergeProps } from '@zag-js/react';

export interface QrCodeRootProviderProps extends PropsWithElement<'div'>, HTMLAttributes<'div', 'id' | 'dir'> {
value: ReturnType<typeof useQrCode>;
}

export default function RootProvider(props: QrCodeRootProviderProps) {
const { element, children, value: qrCode, ...rest } = props;

const attributes = mergeProps(qrCode.getRootProps(), rest);

return (
<RootContext.Provider value={qrCode}>{element ? element(attributes) : <div {...attributes}>{children}</div>}</RootContext.Provider>
);
}
21 changes: 21 additions & 0 deletions packages/skeleton-react/src/components/qr-code/anatomy/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { HTMLAttributes } from '../../../internal/html-attributes.js';
import type { PropsWithElement } from '../../../internal/props-with-element.js';
import { splitProps, type Props } from '@zag-js/qr-code';
import { mergeProps } from '@zag-js/react';
import { useQrCode } from '../modules/provider.js';
import { RootContext } from '../modules/root-context.js';

export interface QrCodeRootProps extends Omit<Props, 'id'>, PropsWithElement<'div'>, HTMLAttributes<'div', 'id' | 'dir' | 'defaultValue'> {}

export default function Root(props: QrCodeRootProps) {
const [qrCodeProps, componentProps] = splitProps(props);
const { element, children, ...rest } = componentProps;

const qrCode = useQrCode(qrCodeProps);

const attributes = mergeProps(qrCode.getRootProps(), rest);

return (
<RootContext.Provider value={qrCode}>{element ? element(attributes) : <div {...attributes}>{children}</div>}</RootContext.Provider>
);
}
9 changes: 9 additions & 0 deletions packages/skeleton-react/src/components/qr-code/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type { QrCodeDownloadTriggerProps } from './anatomy/download-trigger.jsx';
export type { QrCodeFrameProps } from './anatomy/frame.jsx';
export type { QrCodeOverlayProps } from './anatomy/overlay.jsx';
export type { QrCodePatternProps } from './anatomy/pattern.jsx';
export type { QrCodeRootProps } from './anatomy/root.jsx';
export type { QrCodeRootContextProps } from './anatomy/root-context.jsx';
export type { QrCodeRootProviderProps } from './anatomy/root-provider.jsx';
export { QrCode } from './modules/anatomy.js';
export { useQrCode } from './modules/provider.js';
16 changes: 16 additions & 0 deletions packages/skeleton-react/src/components/qr-code/modules/anatomy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import DownloadTrigger from '../anatomy/download-trigger.jsx';
import Frame from '../anatomy/frame.jsx';
import Overlay from '../anatomy/overlay.jsx';
import Pattern from '../anatomy/pattern.jsx';
import RootContext from '../anatomy/root-context.jsx';
import RootProvider from '../anatomy/root-provider.jsx';
import Root from '../anatomy/root.jsx';

export const QrCode = Object.assign(Root, {
Provider: RootProvider,
Context: RootContext,
DownloadTrigger: DownloadTrigger,
Frame: Frame,
Pattern: Pattern,
Overlay: Overlay,
});
12 changes: 12 additions & 0 deletions packages/skeleton-react/src/components/qr-code/modules/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { connect, machine, type Api, type Props } from '@zag-js/qr-code';
import { normalizeProps, useMachine, type PropTypes } from '@zag-js/react';
import { useId } from 'react';

export function useQrCode(props: Omit<Props, 'id'> = {}): Api<PropTypes> {
const service = useMachine(machine, {
id: useId(),
...props,
});

return connect(service, normalizeProps);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createContext } from '../../../internal/create-context.js';
import type { useQrCode } from './provider.js';

export const RootContext = createContext<ReturnType<typeof useQrCode>>();
1 change: 1 addition & 0 deletions packages/skeleton-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './components/pagination/index.js';
export * from './components/popover/index.js';
export * from './components/portal/index.js';
export * from './components/progress/index.js';
export * from './components/qr-code/index.js';
export * from './components/rating-group/index.js';
export * from './components/segmented-control/index.js';
export * from './components/slider/index.js';
Expand Down
Loading