-
-
Notifications
You must be signed in to change notification settings - Fork 389
feature: QrCode component
#4394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
093f004
9e7fcc8
77d2341
74a2a03
9e55df2
57969d4
dbcbfee
65706e9
7a68496
c1e75a1
b2ebed6
80cc49a
172623c
0192508
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| 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 | ||
|
|
||
| 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.). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). | ||
| 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`). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
| 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'] { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 If you were to apply apply One of two things needs to happen:
I'd have real concern for number 2 as it may unintentionally break this pattern: 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. |
||
| } | ||
| } | ||
| 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>; | ||
| } |
| 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>; | ||
| } |
| 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>; | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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'; |
| 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, | ||
| }); |
| 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>>(); |
There was a problem hiding this comment.
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.