diff --git a/.claude/skills/create-component/SKILL.md b/.claude/skills/create-component/SKILL.md deleted file mode 100644 index cfca3edaf4..0000000000 --- a/.claude/skills/create-component/SKILL.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -name: create-component -description: Scaffold a new Skeleton component. ---- - -Inform the user this skill is not yet available. diff --git a/.claude/skills/create-functional-component/SKILL.md b/.claude/skills/create-functional-component/SKILL.md new file mode 100644 index 0000000000..c50d2a3ecd --- /dev/null +++ b/.claude/skills/create-functional-component/SKILL.md @@ -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 ` + + + +{#if element} + {@render element(attributes)} +{:else} + +{/if} diff --git a/packages/skeleton-svelte/src/components/qr-code/anatomy/frame.svelte b/packages/skeleton-svelte/src/components/qr-code/anatomy/frame.svelte new file mode 100644 index 0000000000..7385b8d92c --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/anatomy/frame.svelte @@ -0,0 +1,27 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} + + {@render children?.()} + +{/if} diff --git a/packages/skeleton-svelte/src/components/qr-code/anatomy/overlay.svelte b/packages/skeleton-svelte/src/components/qr-code/anatomy/overlay.svelte new file mode 100644 index 0000000000..c7c21e0cb4 --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/anatomy/overlay.svelte @@ -0,0 +1,27 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/qr-code/anatomy/pattern.svelte b/packages/skeleton-svelte/src/components/qr-code/anatomy/pattern.svelte new file mode 100644 index 0000000000..04ab79de7c --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/anatomy/pattern.svelte @@ -0,0 +1,25 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} + +{/if} diff --git a/packages/skeleton-svelte/src/components/qr-code/anatomy/root-context.svelte b/packages/skeleton-svelte/src/components/qr-code/anatomy/root-context.svelte new file mode 100644 index 0000000000..dff0e4f1ec --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/anatomy/root-context.svelte @@ -0,0 +1,20 @@ + + + + +{@render children(qrCode)} diff --git a/packages/skeleton-svelte/src/components/qr-code/anatomy/root-provider.svelte b/packages/skeleton-svelte/src/components/qr-code/anatomy/root-provider.svelte new file mode 100644 index 0000000000..1706311265 --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/anatomy/root-provider.svelte @@ -0,0 +1,30 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/qr-code/anatomy/root.svelte b/packages/skeleton-svelte/src/components/qr-code/anatomy/root.svelte new file mode 100644 index 0000000000..15d24759f8 --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/anatomy/root.svelte @@ -0,0 +1,37 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/qr-code/index.ts b/packages/skeleton-svelte/src/components/qr-code/index.ts new file mode 100644 index 0000000000..5b36b1b949 --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/index.ts @@ -0,0 +1,9 @@ +export type { QrCodeDownloadTriggerProps } from './anatomy/download-trigger.svelte'; +export type { QrCodeFrameProps } from './anatomy/frame.svelte'; +export type { QrCodeOverlayProps } from './anatomy/overlay.svelte'; +export type { QrCodePatternProps } from './anatomy/pattern.svelte'; +export type { QrCodeRootProps } from './anatomy/root.svelte'; +export type { QrCodeRootContextProps } from './anatomy/root-context.svelte'; +export type { QrCodeRootProviderProps } from './anatomy/root-provider.svelte'; +export { QrCode } from './modules/anatomy.js'; +export { useQrCode } from './modules/provider.svelte.js'; diff --git a/packages/skeleton-svelte/src/components/qr-code/modules/anatomy.ts b/packages/skeleton-svelte/src/components/qr-code/modules/anatomy.ts new file mode 100644 index 0000000000..7ba164452e --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/modules/anatomy.ts @@ -0,0 +1,16 @@ +import DownloadTrigger from '../anatomy/download-trigger.svelte'; +import Frame from '../anatomy/frame.svelte'; +import Overlay from '../anatomy/overlay.svelte'; +import Pattern from '../anatomy/pattern.svelte'; +import RootContext from '../anatomy/root-context.svelte'; +import RootProvider from '../anatomy/root-provider.svelte'; +import Root from '../anatomy/root.svelte'; + +export const QrCode = Object.assign(Root, { + Provider: RootProvider, + Context: RootContext, + DownloadTrigger: DownloadTrigger, + Frame: Frame, + Pattern: Pattern, + Overlay: Overlay, +}); diff --git a/packages/skeleton-svelte/src/components/qr-code/modules/provider.svelte.ts b/packages/skeleton-svelte/src/components/qr-code/modules/provider.svelte.ts new file mode 100644 index 0000000000..91603bd118 --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/modules/provider.svelte.ts @@ -0,0 +1,8 @@ +import { connect, machine, type Api, type Props } from '@zag-js/qr-code'; +import { normalizeProps, useMachine, type PropTypes } from '@zag-js/svelte'; + +export function useQrCode(props: Props | (() => Props)): () => Api { + const service = useMachine(machine, props); + const qrCode = $derived(connect(service, normalizeProps)); + return () => qrCode; +} diff --git a/packages/skeleton-svelte/src/components/qr-code/modules/root-context.ts b/packages/skeleton-svelte/src/components/qr-code/modules/root-context.ts new file mode 100644 index 0000000000..56b8c65645 --- /dev/null +++ b/packages/skeleton-svelte/src/components/qr-code/modules/root-context.ts @@ -0,0 +1,4 @@ +import { createContext } from '../../../internal/create-context.js'; +import type { useQrCode } from './provider.svelte.js'; + +export const RootContext = createContext>(); diff --git a/packages/skeleton-svelte/src/index.ts b/packages/skeleton-svelte/src/index.ts index f6b95832bf..d701981872 100644 --- a/packages/skeleton-svelte/src/index.ts +++ b/packages/skeleton-svelte/src/index.ts @@ -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'; diff --git a/packages/skeleton-svelte/test/components/qr-code.svelte b/packages/skeleton-svelte/test/components/qr-code.svelte new file mode 100644 index 0000000000..4d5a9fbeef --- /dev/null +++ b/packages/skeleton-svelte/test/components/qr-code.svelte @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/packages/skeleton-svelte/test/components/qr-code.test.ts b/packages/skeleton-svelte/test/components/qr-code.test.ts new file mode 100644 index 0000000000..995f18fd58 --- /dev/null +++ b/packages/skeleton-svelte/test/components/qr-code.test.ts @@ -0,0 +1,41 @@ +import QrCode from './qr-code.svelte'; +import { describe, expect, it } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +describe('QrCode', () => { + describe('Root', () => { + it('renders', async () => { + render(QrCode); + await expect.element(page.getByTestId('root')).toBeInTheDocument(); + }); + }); + + describe('Frame', () => { + it('renders', async () => { + render(QrCode); + await expect.element(page.getByTestId('frame')).toBeInTheDocument(); + }); + }); + + describe('Pattern', () => { + it('renders', async () => { + render(QrCode); + await expect.element(page.getByTestId('pattern')).toBeInTheDocument(); + }); + }); + + describe('Overlay', () => { + it('renders', async () => { + render(QrCode); + await expect.element(page.getByTestId('overlay')).toBeInTheDocument(); + }); + }); + + describe('DownloadTrigger', () => { + it('renders', async () => { + render(QrCode); + await expect.element(page.getByTestId('download-trigger')).toBeInTheDocument(); + }); + }); +}); diff --git a/playgrounds/skeleton-react/src/routes/components/qr-code/index.tsx b/playgrounds/skeleton-react/src/routes/components/qr-code/index.tsx new file mode 100644 index 0000000000..a70379331d --- /dev/null +++ b/playgrounds/skeleton-react/src/routes/components/qr-code/index.tsx @@ -0,0 +1,20 @@ +import { QrCode } from '@skeletonlabs/skeleton-react'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/components/qr-code/')({ + component: Page, +}); + +function Page() { + return ( + + + + + Overlay + + Download + + + ); +} diff --git a/playgrounds/skeleton-svelte/src/routes/components/qr-code/+page.svelte b/playgrounds/skeleton-svelte/src/routes/components/qr-code/+page.svelte new file mode 100644 index 0000000000..995a581575 --- /dev/null +++ b/playgrounds/skeleton-svelte/src/routes/components/qr-code/+page.svelte @@ -0,0 +1,11 @@ + + + + + + + Overlay + Download + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea417d453b..7f0112fb02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,6 +195,9 @@ catalogs: '@zag-js/progress': specifier: 1.40.0 version: 1.40.0 + '@zag-js/qr-code': + specifier: 1.40.0 + version: 1.40.0 '@zag-js/radio-group': specifier: 1.40.0 version: 1.40.0 @@ -613,6 +616,9 @@ importers: '@zag-js/progress': specifier: 'catalog:' version: 1.40.0 + '@zag-js/qr-code': + specifier: 'catalog:' + version: 1.40.0 '@zag-js/radio-group': specifier: 'catalog:' version: 1.40.0 @@ -737,6 +743,9 @@ importers: '@zag-js/progress': specifier: 'catalog:' version: 1.40.0 + '@zag-js/qr-code': + specifier: 'catalog:' + version: 1.40.0 '@zag-js/radio-group': specifier: 'catalog:' version: 1.40.0 @@ -4022,6 +4031,9 @@ packages: '@zag-js/progress@1.40.0': resolution: {integrity: sha512-V61a5CHEs8suevQVS+/1ENj1RDVYNOUUTawK6uriCA6Ol59xe30DmF+eV6Y9miM7L/pN3YjZRq9uEDJMXXK32g==} + '@zag-js/qr-code@1.40.0': + resolution: {integrity: sha512-xD37tVrQ46CeqVLqkSm61kURoJ4Z/uOFcB8z7Hu3UX+1OFTfkhgrns6iLUneoRjO3hsqQaTaVkxVOQeLYWb+wA==} + '@zag-js/radio-group@1.40.0': resolution: {integrity: sha512-sFJCdyOKzQC9hylSP19R71yv44by/C78D9EHfsxQJtvOgDv9E+h13NNX4n9wWyubC20xftlxkja8sNT5NfJKUw==} @@ -5834,6 +5846,9 @@ packages: proxy-compare@3.0.1: resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} + proxy-memoize@3.0.1: + resolution: {integrity: sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -6651,6 +6666,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uqr@0.1.2: + resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -9805,6 +9823,16 @@ snapshots: '@zag-js/types': 1.40.0 '@zag-js/utils': 1.40.0 + '@zag-js/qr-code@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + proxy-memoize: 3.0.1 + uqr: 0.1.2 + '@zag-js/radio-group@1.40.0': dependencies: '@zag-js/anatomy': 1.40.0 @@ -12004,6 +12032,10 @@ snapshots: proxy-compare@3.0.1: {} + proxy-memoize@3.0.1: + dependencies: + proxy-compare: 3.0.1 + punycode@2.3.1: optional: true @@ -12873,6 +12905,8 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uqr@0.1.2: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e0148ecb27..4d7e101d24 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -67,6 +67,7 @@ catalog: '@zag-js/pagination': 1.40.0 '@zag-js/popover': 1.40.0 '@zag-js/progress': 1.40.0 + '@zag-js/qr-code': 1.40.0 '@zag-js/radio-group': 1.40.0 '@zag-js/rating-group': 1.40.0 '@zag-js/react': 1.40.0 diff --git a/sites/skeleton.dev/src/content/component-types/react/qr-code.json b/sites/skeleton.dev/src/content/component-types/react/qr-code.json new file mode 100644 index 0000000000..a6bd0e92aa --- /dev/null +++ b/sites/skeleton.dev/src/content/component-types/react/qr-code.json @@ -0,0 +1,127 @@ +{ + "name": "qr-code", + "types": [ + { + "name": "QrCodeRootProps", + "props": [ + { + "name": "element", + "type": "((attributes: HTMLAttributes<\"div\">) => Element) | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeRootProviderProps", + "props": [ + { + "name": "value", + "type": "Api", + "typeKind": "primitive", + "optional": false, + "JSDoc": { + "description": null, + "tags": [] + } + }, + { + "name": "element", + "type": "((attributes: HTMLAttributes<\"div\">) => Element) | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeRootContextProps", + "props": [ + { + "name": "children", + "type": "(qrCode: Api) => ReactNode", + "typeKind": "function", + "optional": false, + "JSDoc": { + "description": null, + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeDownloadTriggerProps", + "props": [ + { + "name": "element", + "type": "((attributes: HTMLAttributes<\"button\">) => Element) | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeFrameProps", + "props": [ + { + "name": "element", + "type": "((attributes: HTMLAttributes<\"svg\">) => Element) | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodePatternProps", + "props": [ + { + "name": "element", + "type": "((attributes: HTMLAttributes<\"path\">) => Element) | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeOverlayProps", + "props": [ + { + "name": "element", + "type": "((attributes: HTMLAttributes<\"div\">) => Element) | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + } + ] +} diff --git a/sites/skeleton.dev/src/content/component-types/svelte/qr-code.json b/sites/skeleton.dev/src/content/component-types/svelte/qr-code.json new file mode 100644 index 0000000000..bff597cbac --- /dev/null +++ b/sites/skeleton.dev/src/content/component-types/svelte/qr-code.json @@ -0,0 +1,127 @@ +{ + "name": "qr-code", + "types": [ + { + "name": "QrCodeRootProps", + "props": [ + { + "name": "element", + "type": "Snippet<[HTMLAttributes<\"div\">]> | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeRootProviderProps", + "props": [ + { + "name": "value", + "type": "() => Api", + "typeKind": "function", + "optional": false, + "JSDoc": { + "description": null, + "tags": [] + } + }, + { + "name": "element", + "type": "Snippet<[HTMLAttributes<\"div\">]> | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeRootContextProps", + "props": [ + { + "name": "children", + "type": "Snippet<[() => Api]>", + "typeKind": "function", + "optional": false, + "JSDoc": { + "description": null, + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeDownloadTriggerProps", + "props": [ + { + "name": "element", + "type": "Snippet<[HTMLAttributes<\"button\">]> | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeFrameProps", + "props": [ + { + "name": "element", + "type": "Snippet<[HTMLAttributes<\"svg\">]> | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodePatternProps", + "props": [ + { + "name": "element", + "type": "Snippet<[HTMLAttributes<\"path\">]> | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + }, + { + "name": "QrCodeOverlayProps", + "props": [ + { + "name": "element", + "type": "Snippet<[HTMLAttributes<\"div\">]> | undefined", + "typeKind": "function", + "optional": true, + "JSDoc": { + "description": "Render the element yourself", + "tags": [] + } + } + ], + "metadata": {} + } + ] +}