diff --git a/.changeset/plenty-birds-bet.md b/.changeset/plenty-birds-bet.md new file mode 100644 index 0000000000..69583f1659 --- /dev/null +++ b/.changeset/plenty-birds-bet.md @@ -0,0 +1,8 @@ +--- +"@skeletonlabs/skeleton-common": minor +"@skeletonlabs/skeleton-svelte": minor +"@skeletonlabs/skeleton-react": minor +--- + +feature: `Floating Panel` component + \ No newline at end of file diff --git a/packages/skeleton-common/src/classes/floating-panel.ts b/packages/skeleton-common/src/classes/floating-panel.ts new file mode 100644 index 0000000000..68ad457431 --- /dev/null +++ b/packages/skeleton-common/src/classes/floating-panel.ts @@ -0,0 +1,16 @@ +import { defineSkeletonClasses } from '../internal/define-skeleton-classes.js' with { type: 'macro' }; + +export const classesFloatingPanel = defineSkeletonClasses({ + trigger: '', + positioner: '', + content: 'card overflow-hidden shadow-lg border border-surface-300-700', + dragTrigger: '', + header: 'px-4 py-2 grid grid-cols-[1fr_auto] gap-2 items-center bg-surface-200-800 overflow-y-hidden', + title: 'flex justify-start items-center gap-2 whitespace-nowrap', + control: 'flex gap-1', + stageTrigger: 'btn-icon hover:preset-tonal', + closeTrigger: 'btn-icon hover:preset-tonal', + body: 'h-full bg-surface-100-900 p-4 overflow-y-auto', + // TODO: Remove explicit data-[axis=n]:top-0 when https://github.com/chakra-ui/zag/pull/2863 is merged and released + resizeTrigger: 'data-[axis*=n]:h-2 data-[axis*=s]:h-2 data-[axis*=e]:w-2 data-[axis*=w]:w-2 data-[axis=n]:top-0', +}); diff --git a/packages/skeleton-common/src/index.ts b/packages/skeleton-common/src/index.ts index 33a4ff6202..ad96d4ae1d 100644 --- a/packages/skeleton-common/src/index.ts +++ b/packages/skeleton-common/src/index.ts @@ -6,6 +6,7 @@ export * from './classes/combobox.js'; export * from './classes/date-picker.js'; export * from './classes/dialog.js'; export * from './classes/file-upload.js'; +export * from './classes/floating-panel.js'; export * from './classes/listbox.js'; export * from './classes/menu.js'; export * from './classes/navigation.js'; diff --git a/packages/skeleton-react/package.json b/packages/skeleton-react/package.json index bded9d74e8..c4b3f6ea12 100644 --- a/packages/skeleton-react/package.json +++ b/packages/skeleton-react/package.json @@ -38,6 +38,7 @@ "@zag-js/date-picker": "catalog:", "@zag-js/dialog": "catalog:", "@zag-js/file-upload": "catalog:", + "@zag-js/floating-panel": "catalog:", "@zag-js/listbox": "catalog:", "@zag-js/menu": "catalog:", "@zag-js/pagination": "catalog:", diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/body.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/body.tsx new file mode 100644 index 0000000000..f846a03952 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/body.tsx @@ -0,0 +1,24 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelBodyProps extends PropsWithElement<'div'>, HTMLAttributes<'div'> {} + +export default function Body(props: FloatingPanelBodyProps) { + const floatingPanel = use(RootContext); + + const { element, children, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getBodyProps(), + { + className: classesFloatingPanel.body, + }, + rest, + ); + + return element ? element(attributes) :
{children}
; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/close-trigger.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/close-trigger.tsx new file mode 100644 index 0000000000..8a3d31bc8c --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/close-trigger.tsx @@ -0,0 +1,24 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelCloseTriggerProps extends PropsWithElement<'button'>, HTMLAttributes<'button'> {} + +export default function CloseTrigger(props: FloatingPanelCloseTriggerProps) { + const floatingPanel = use(RootContext); + + const { element, children, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getCloseTriggerProps(), + { + className: classesFloatingPanel.closeTrigger, + }, + rest, + ); + + return element ? element(attributes) : ; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/content.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/content.tsx new file mode 100644 index 0000000000..fe6863ed11 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/content.tsx @@ -0,0 +1,24 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelContentProps extends PropsWithElement<'div'>, HTMLAttributes<'div'> {} + +export default function Content(props: FloatingPanelContentProps) { + const floatingPanel = use(RootContext); + + const { element, children, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getContentProps(), + { + className: classesFloatingPanel.content, + }, + rest, + ); + + return element ? element(attributes) :
{children}
; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/control.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/control.tsx new file mode 100644 index 0000000000..d7ff0cb40e --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/control.tsx @@ -0,0 +1,24 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelControlProps extends PropsWithElement<'div'>, HTMLAttributes<'div'> {} + +export default function Control(props: FloatingPanelControlProps) { + const floatingPanel = use(RootContext); + + const { element, children, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getControlProps(), + { + className: classesFloatingPanel.control, + }, + rest, + ); + + return element ? element(attributes) :
{children}
; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/drag-trigger.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/drag-trigger.tsx new file mode 100644 index 0000000000..260528c25a --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/drag-trigger.tsx @@ -0,0 +1,24 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelDragTriggerProps extends PropsWithElement<'div'>, HTMLAttributes<'div'> {} + +export default function DragTrigger(props: FloatingPanelDragTriggerProps) { + const floatingPanel = use(RootContext); + + const { element, children, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getDragTriggerProps(), + { + className: classesFloatingPanel.dragTrigger, + }, + rest, + ); + + return element ? element(attributes) :
{children}
; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/header.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/header.tsx new file mode 100644 index 0000000000..b42021191e --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/header.tsx @@ -0,0 +1,24 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelHeaderProps extends PropsWithElement<'div'>, HTMLAttributes<'div'> {} + +export default function Header(props: FloatingPanelHeaderProps) { + const floatingPanel = use(RootContext); + + const { element, children, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getHeaderProps(), + { + className: classesFloatingPanel.header, + }, + rest, + ); + + return element ? element(attributes) :
{children}
; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/positioner.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/positioner.tsx new file mode 100644 index 0000000000..b7eab06c65 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/positioner.tsx @@ -0,0 +1,24 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelPositionerProps extends PropsWithElement<'div'>, HTMLAttributes<'div'> {} + +export default function Positioner(props: FloatingPanelPositionerProps) { + const floatingPanel = use(RootContext); + + const { element, children, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getPositionerProps(), + { + className: classesFloatingPanel.positioner, + }, + rest, + ); + + return element ? element(attributes) :
{children}
; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/resize-trigger.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/resize-trigger.tsx new file mode 100644 index 0000000000..4e4a967e45 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/resize-trigger.tsx @@ -0,0 +1,26 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { splitResizeTriggerProps, type ResizeTriggerProps } from '@zag-js/floating-panel'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelResizeTriggerProps extends ResizeTriggerProps, PropsWithElement<'div'>, HTMLAttributes<'div'> {} + +export default function ResizeTrigger(props: FloatingPanelResizeTriggerProps) { + const floatingPanel = use(RootContext); + + const [resizeTriggerProps, componentProps] = splitResizeTriggerProps(props); + const { element, children, ...rest } = componentProps; + + const attributes = mergeProps( + floatingPanel.getResizeTriggerProps(resizeTriggerProps as ResizeTriggerProps), + { + className: classesFloatingPanel.resizeTrigger, + }, + rest, + ); + + return element ? element(attributes) :
{children}
; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/root-context.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/root-context.tsx new file mode 100644 index 0000000000..99c2db88b2 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/root-context.tsx @@ -0,0 +1,15 @@ +import type { useFloatingPanel } from '../modules/provider.js'; +import { RootContext as RootContext_ } from '../modules/root-context.js'; +import { type ReactNode, use } from 'react'; + +export interface FloatingPanelRootContextProps { + children: (floatingPanel: ReturnType) => ReactNode; +} + +export default function RootContext(props: FloatingPanelRootContextProps) { + const floatingPanel = use(RootContext_); + + const { children } = props; + + return children(floatingPanel); +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/root-provider.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/root-provider.tsx new file mode 100644 index 0000000000..1462720c85 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/root-provider.tsx @@ -0,0 +1,13 @@ +import type { useFloatingPanel } from '../modules/provider.js'; +import { RootContext } from '../modules/root-context.js'; +import { type PropsWithChildren } from 'react'; + +export interface FloatingPanelRootProviderProps extends PropsWithChildren { + value: ReturnType; +} + +export default function RootProvider(props: FloatingPanelRootProviderProps) { + const { children, value: floatingPanel } = props; + + return {children}; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/root.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/root.tsx new file mode 100644 index 0000000000..36aa9d2de7 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/root.tsx @@ -0,0 +1,15 @@ +import { useFloatingPanel } from '../modules/provider.js'; +import { RootContext } from '../modules/root-context.js'; +import { type Props, splitProps } from '@zag-js/floating-panel'; +import { type PropsWithChildren } from 'react'; + +export interface FloatingPanelRootProps extends PropsWithChildren, Omit {} + +export default function Root(props: FloatingPanelRootProps) { + const [floatingPanelProps, componentProps] = splitProps(props); + const { children } = componentProps; + + const floatingPanel = useFloatingPanel(floatingPanelProps); + + return {children}; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/stage-trigger.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/stage-trigger.tsx new file mode 100644 index 0000000000..05a76b4eb5 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/stage-trigger.tsx @@ -0,0 +1,25 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { type StageTriggerProps } from '@zag-js/floating-panel'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelStageTriggerProps extends StageTriggerProps, PropsWithElement<'button'>, HTMLAttributes<'button'> {} + +export default function StageTrigger(props: FloatingPanelStageTriggerProps) { + const floatingPanel = use(RootContext); + + const { element, children, stage, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getStageTriggerProps({ stage }), + { + className: classesFloatingPanel.stageTrigger, + }, + rest, + ); + + return element ? element(attributes) : ; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/title.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/title.tsx new file mode 100644 index 0000000000..2fff7f3ad0 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/title.tsx @@ -0,0 +1,24 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelTitleProps extends PropsWithElement<'div'>, HTMLAttributes<'div'> {} + +export default function Title(props: FloatingPanelTitleProps) { + const floatingPanel = use(RootContext); + + const { element, children, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getTitleProps(), + { + className: classesFloatingPanel.title, + }, + rest, + ); + + return element ? element(attributes) :
{children}
; +} diff --git a/packages/skeleton-react/src/components/floating-panel/anatomy/trigger.tsx b/packages/skeleton-react/src/components/floating-panel/anatomy/trigger.tsx new file mode 100644 index 0000000000..e0a3b651ca --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/anatomy/trigger.tsx @@ -0,0 +1,24 @@ +import { RootContext } from '../modules/root-context.js'; +import type { HTMLAttributes } from '@/internal/html-attributes.js'; +import type { PropsWithElement } from '@/internal/props-with-element.js'; +import { classesFloatingPanel } from '@skeletonlabs/skeleton-common'; +import { mergeProps } from '@zag-js/react'; +import { use } from 'react'; + +export interface FloatingPanelTriggerProps extends PropsWithElement<'button'>, HTMLAttributes<'button'> {} + +export default function Trigger(props: FloatingPanelTriggerProps) { + const floatingPanel = use(RootContext); + + const { element, children, ...rest } = props; + + const attributes = mergeProps( + floatingPanel.getTriggerProps(), + { + className: classesFloatingPanel.trigger, + }, + rest, + ); + + return element ? element(attributes) : ; +} diff --git a/packages/skeleton-react/src/components/floating-panel/index.ts b/packages/skeleton-react/src/components/floating-panel/index.ts new file mode 100644 index 0000000000..37cd75579f --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/index.ts @@ -0,0 +1,16 @@ +export type { FloatingPanelBodyProps } from './anatomy/body.jsx'; +export type { FloatingPanelCloseTriggerProps } from './anatomy/close-trigger.jsx'; +export type { FloatingPanelContentProps } from './anatomy/content.jsx'; +export type { FloatingPanelControlProps } from './anatomy/control.jsx'; +export type { FloatingPanelDragTriggerProps } from './anatomy/drag-trigger.jsx'; +export type { FloatingPanelHeaderProps } from './anatomy/header.jsx'; +export type { FloatingPanelPositionerProps } from './anatomy/positioner.jsx'; +export type { FloatingPanelResizeTriggerProps } from './anatomy/resize-trigger.jsx'; +export type { FloatingPanelRootProps } from './anatomy/root.jsx'; +export type { FloatingPanelRootContextProps } from './anatomy/root-context.jsx'; +export type { FloatingPanelRootProviderProps } from './anatomy/root-provider.jsx'; +export type { FloatingPanelStageTriggerProps } from './anatomy/stage-trigger.jsx'; +export type { FloatingPanelTitleProps } from './anatomy/title.jsx'; +export type { FloatingPanelTriggerProps } from './anatomy/trigger.jsx'; +export { FloatingPanel } from './modules/anatomy.js'; +export { useFloatingPanel } from './modules/provider.js'; diff --git a/packages/skeleton-react/src/components/floating-panel/modules/anatomy.ts b/packages/skeleton-react/src/components/floating-panel/modules/anatomy.ts new file mode 100644 index 0000000000..22b192c13d --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/modules/anatomy.ts @@ -0,0 +1,30 @@ +import Body from '../anatomy/body.jsx'; +import CloseTrigger from '../anatomy/close-trigger.jsx'; +import Content from '../anatomy/content.jsx'; +import Control from '../anatomy/control.jsx'; +import DragTrigger from '../anatomy/drag-trigger.jsx'; +import Header from '../anatomy/header.jsx'; +import Positioner from '../anatomy/positioner.jsx'; +import ResizeTrigger from '../anatomy/resize-trigger.jsx'; +import RootContext from '../anatomy/root-context.jsx'; +import RootProvider from '../anatomy/root-provider.jsx'; +import Root from '../anatomy/root.jsx'; +import StageTrigger from '../anatomy/stage-trigger.jsx'; +import Title from '../anatomy/title.jsx'; +import Trigger from '../anatomy/trigger.jsx'; + +export const FloatingPanel = Object.assign(Root, { + Provider: RootProvider, + Context: RootContext, + Trigger: Trigger, + Positioner: Positioner, + Content: Content, + DragTrigger: DragTrigger, + Header: Header, + Title: Title, + Control: Control, + StageTrigger: StageTrigger, + CloseTrigger: CloseTrigger, + Body: Body, + ResizeTrigger: ResizeTrigger, +}); diff --git a/packages/skeleton-react/src/components/floating-panel/modules/provider.ts b/packages/skeleton-react/src/components/floating-panel/modules/provider.ts new file mode 100644 index 0000000000..0ad22e2963 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/modules/provider.ts @@ -0,0 +1,11 @@ +import { type Api, connect, machine, type Props } from '@zag-js/floating-panel'; +import { normalizeProps, useMachine, type PropTypes } from '@zag-js/react'; +import { useId } from 'react'; + +export function useFloatingPanel(props: Omit = {}): Api { + const service = useMachine(machine, { + id: useId(), + ...props, + }); + return connect(service, normalizeProps); +} diff --git a/packages/skeleton-react/src/components/floating-panel/modules/root-context.ts b/packages/skeleton-react/src/components/floating-panel/modules/root-context.ts new file mode 100644 index 0000000000..e8f8f89900 --- /dev/null +++ b/packages/skeleton-react/src/components/floating-panel/modules/root-context.ts @@ -0,0 +1,4 @@ +import type { useFloatingPanel } from './provider.js'; +import { createContext } from '@/internal/create-context.js'; + +export const RootContext = createContext>(); diff --git a/packages/skeleton-react/src/index.ts b/packages/skeleton-react/src/index.ts index c47913103c..dd95233e25 100644 --- a/packages/skeleton-react/src/index.ts +++ b/packages/skeleton-react/src/index.ts @@ -6,6 +6,7 @@ export * from './components/combobox/index.js'; export * from './components/date-picker/index.js'; export * from './components/dialog/index.js'; export * from './components/file-upload/index.js'; +export * from './components/floating-panel/index.js'; export * from './components/listbox/index.js'; export * from './components/menu/index.js'; export * from './components/navigation/index.js'; diff --git a/packages/skeleton-react/test/components/floating-panel/index.test.tsx b/packages/skeleton-react/test/components/floating-panel/index.test.tsx new file mode 100644 index 0000000000..e03bc83b2a --- /dev/null +++ b/packages/skeleton-react/test/components/floating-panel/index.test.tsx @@ -0,0 +1,87 @@ +import Test from './test.jsx'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +describe('FloatingPanel', () => { + describe('Trigger', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('trigger')).toBeInTheDocument(); + }); + }); + + describe('Positioner', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('positioner')).toBeInTheDocument(); + }); + }); + + describe('Content', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('content')).toBeInTheDocument(); + }); + }); + + describe('Header', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('header')).toBeInTheDocument(); + }); + }); + + describe('Body', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('body')).toBeInTheDocument(); + }); + }); + + describe('Title', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('title')).toBeInTheDocument(); + }); + }); + + describe('DragTrigger', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('drag-trigger')).toBeInTheDocument(); + }); + }); + + describe('ResizeTrigger', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('resize-trigger')).toBeInTheDocument(); + }); + }); + + describe('StageTrigger', () => { + it('renders minimized trigger', () => { + render(); + expect(screen.getByTestId('stage-trigger-minimized')).toBeInTheDocument(); + }); + + it('renders maximized trigger', () => { + render(); + expect(screen.getByTestId('stage-trigger-maximized')).toBeInTheDocument(); + }); + }); + + describe('CloseTrigger', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('close-trigger')).toBeInTheDocument(); + }); + }); + + describe('Control', () => { + it('renders', () => { + render(); + expect(screen.getByTestId('control')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/skeleton-react/test/components/floating-panel/test.tsx b/packages/skeleton-react/test/components/floating-panel/test.tsx new file mode 100644 index 0000000000..f7fd2c44cd --- /dev/null +++ b/packages/skeleton-react/test/components/floating-panel/test.tsx @@ -0,0 +1,24 @@ +import { FloatingPanel } from '@/index.js'; + +export default function Test() { + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/skeleton-svelte/package.json b/packages/skeleton-svelte/package.json index e156c2fb63..467ad351da 100644 --- a/packages/skeleton-svelte/package.json +++ b/packages/skeleton-svelte/package.json @@ -39,6 +39,7 @@ "@zag-js/date-picker": "catalog:", "@zag-js/dialog": "catalog:", "@zag-js/file-upload": "catalog:", + "@zag-js/floating-panel": "catalog:", "@zag-js/listbox": "catalog:", "@zag-js/menu": "catalog:", "@zag-js/pagination": "catalog:", diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/body.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/body.svelte new file mode 100644 index 0000000000..f3766d1809 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/body.svelte @@ -0,0 +1,36 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/close-trigger.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/close-trigger.svelte new file mode 100644 index 0000000000..0d6df62047 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/close-trigger.svelte @@ -0,0 +1,36 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} + +{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/content.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/content.svelte new file mode 100644 index 0000000000..c29cfa511a --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/content.svelte @@ -0,0 +1,36 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/control.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/control.svelte new file mode 100644 index 0000000000..59c28d8019 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/control.svelte @@ -0,0 +1,36 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/drag-trigger.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/drag-trigger.svelte new file mode 100644 index 0000000000..17e16af6ea --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/drag-trigger.svelte @@ -0,0 +1,36 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/header.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/header.svelte new file mode 100644 index 0000000000..29ee5e0bb3 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/header.svelte @@ -0,0 +1,36 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/positioner.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/positioner.svelte new file mode 100644 index 0000000000..e28c338e92 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/positioner.svelte @@ -0,0 +1,36 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/resize-trigger.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/resize-trigger.svelte new file mode 100644 index 0000000000..3da7412f8b --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/resize-trigger.svelte @@ -0,0 +1,40 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/root-context.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/root-context.svelte new file mode 100644 index 0000000000..4b2c550a57 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/root-context.svelte @@ -0,0 +1,20 @@ + + + + +{@render children(floatingPanel)} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/root-provider.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/root-provider.svelte new file mode 100644 index 0000000000..847cd0944e --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/root-provider.svelte @@ -0,0 +1,20 @@ + + + + +{@render children?.()} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/root.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/root.svelte new file mode 100644 index 0000000000..4eeaffb844 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/root.svelte @@ -0,0 +1,27 @@ + + + + +{@render children?.()} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/stage-trigger.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/stage-trigger.svelte new file mode 100644 index 0000000000..143ca5f17c --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/stage-trigger.svelte @@ -0,0 +1,37 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} + +{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/title.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/title.svelte new file mode 100644 index 0000000000..068c478c23 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/title.svelte @@ -0,0 +1,36 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/anatomy/trigger.svelte b/packages/skeleton-svelte/src/components/floating-panel/anatomy/trigger.svelte new file mode 100644 index 0000000000..11b31f87d2 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/anatomy/trigger.svelte @@ -0,0 +1,36 @@ + + + + +{#if element} + {@render element(attributes)} +{:else} + +{/if} diff --git a/packages/skeleton-svelte/src/components/floating-panel/index.ts b/packages/skeleton-svelte/src/components/floating-panel/index.ts new file mode 100644 index 0000000000..7c0a87d3a1 --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/index.ts @@ -0,0 +1,16 @@ +export type { FloatingPanelBodyProps } from './anatomy/body.svelte'; +export type { FloatingPanelCloseTriggerProps } from './anatomy/close-trigger.svelte'; +export type { FloatingPanelContentProps } from './anatomy/content.svelte'; +export type { FloatingPanelControlProps } from './anatomy/control.svelte'; +export type { FloatingPanelDragTriggerProps } from './anatomy/drag-trigger.svelte'; +export type { FloatingPanelHeaderProps } from './anatomy/header.svelte'; +export type { FloatingPanelPositionerProps } from './anatomy/positioner.svelte'; +export type { FloatingPanelResizeTriggerProps } from './anatomy/resize-trigger.svelte'; +export type { FloatingPanelRootProps } from './anatomy/root.svelte'; +export type { FloatingPanelRootContextProps } from './anatomy/root-context.svelte'; +export type { FloatingPanelRootProviderProps } from './anatomy/root-provider.svelte'; +export type { FloatingPanelStageTriggerProps } from './anatomy/stage-trigger.svelte'; +export type { FloatingPanelTitleProps } from './anatomy/title.svelte'; +export type { FloatingPanelTriggerProps } from './anatomy/trigger.svelte'; +export { FloatingPanel } from './modules/anatomy.js'; +export { useFloatingPanel } from './modules/provider.svelte.js'; diff --git a/packages/skeleton-svelte/src/components/floating-panel/modules/anatomy.ts b/packages/skeleton-svelte/src/components/floating-panel/modules/anatomy.ts new file mode 100644 index 0000000000..e52530015b --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/modules/anatomy.ts @@ -0,0 +1,30 @@ +import Body from '../anatomy/body.svelte'; +import CloseTrigger from '../anatomy/close-trigger.svelte'; +import Content from '../anatomy/content.svelte'; +import Control from '../anatomy/control.svelte'; +import DragTrigger from '../anatomy/drag-trigger.svelte'; +import Header from '../anatomy/header.svelte'; +import Positioner from '../anatomy/positioner.svelte'; +import ResizeTrigger from '../anatomy/resize-trigger.svelte'; +import RootContext from '../anatomy/root-context.svelte'; +import RootProvider from '../anatomy/root-provider.svelte'; +import Root from '../anatomy/root.svelte'; +import StageTrigger from '../anatomy/stage-trigger.svelte'; +import Title from '../anatomy/title.svelte'; +import Trigger from '../anatomy/trigger.svelte'; + +export const FloatingPanel = Object.assign(Root, { + Provider: RootProvider, + Context: RootContext, + Trigger: Trigger, + Positioner: Positioner, + Content: Content, + DragTrigger: DragTrigger, + Header: Header, + Title: Title, + Control: Control, + StageTrigger: StageTrigger, + CloseTrigger: CloseTrigger, + Body: Body, + ResizeTrigger: ResizeTrigger, +}); diff --git a/packages/skeleton-svelte/src/components/floating-panel/modules/provider.svelte.ts b/packages/skeleton-svelte/src/components/floating-panel/modules/provider.svelte.ts new file mode 100644 index 0000000000..8a6c1d68ef --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/modules/provider.svelte.ts @@ -0,0 +1,8 @@ +import { type Api, connect, machine, type Props } from '@zag-js/floating-panel'; +import { normalizeProps, useMachine, type PropTypes } from '@zag-js/svelte'; + +export function useFloatingPanel(props: Props | (() => Props)): () => Api { + const service = useMachine(machine, props); + const floatingPanel = $derived(connect(service, normalizeProps)); + return () => floatingPanel; +} diff --git a/packages/skeleton-svelte/src/components/floating-panel/modules/root-context.ts b/packages/skeleton-svelte/src/components/floating-panel/modules/root-context.ts new file mode 100644 index 0000000000..887ef787fc --- /dev/null +++ b/packages/skeleton-svelte/src/components/floating-panel/modules/root-context.ts @@ -0,0 +1,4 @@ +import type { useFloatingPanel } from './provider.svelte.js'; +import { createContext } from '@/internal/create-context.js'; + +export const RootContext = createContext>(); diff --git a/packages/skeleton-svelte/src/index.ts b/packages/skeleton-svelte/src/index.ts index c47913103c..dd95233e25 100644 --- a/packages/skeleton-svelte/src/index.ts +++ b/packages/skeleton-svelte/src/index.ts @@ -6,6 +6,7 @@ export * from './components/combobox/index.js'; export * from './components/date-picker/index.js'; export * from './components/dialog/index.js'; export * from './components/file-upload/index.js'; +export * from './components/floating-panel/index.js'; export * from './components/listbox/index.js'; export * from './components/menu/index.js'; export * from './components/navigation/index.js'; diff --git a/packages/skeleton-svelte/test/components/floating-panel/index.test.ts b/packages/skeleton-svelte/test/components/floating-panel/index.test.ts new file mode 100644 index 0000000000..37bf3419d4 --- /dev/null +++ b/packages/skeleton-svelte/test/components/floating-panel/index.test.ts @@ -0,0 +1,87 @@ +import Test from './test.svelte'; +import { render, screen } from '@testing-library/svelte'; +import { describe, expect, it } from 'vitest'; + +describe('FloatingPanel', () => { + describe('Trigger', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('trigger')).toBeInTheDocument(); + }); + }); + + describe('Positioner', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('positioner')).toBeInTheDocument(); + }); + }); + + describe('Content', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('content')).toBeInTheDocument(); + }); + }); + + describe('Header', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('header')).toBeInTheDocument(); + }); + }); + + describe('Body', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('body')).toBeInTheDocument(); + }); + }); + + describe('Title', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('title')).toBeInTheDocument(); + }); + }); + + describe('DragTrigger', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('drag-trigger')).toBeInTheDocument(); + }); + }); + + describe('ResizeTrigger', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('resize-trigger')).toBeInTheDocument(); + }); + }); + + describe('StageTrigger', () => { + it('renders minimized trigger', () => { + render(Test); + expect(screen.getByTestId('stage-trigger-minimized')).toBeInTheDocument(); + }); + + it('renders maximized trigger', () => { + render(Test); + expect(screen.getByTestId('stage-trigger-maximized')).toBeInTheDocument(); + }); + }); + + describe('CloseTrigger', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('close-trigger')).toBeInTheDocument(); + }); + }); + + describe('Control', () => { + it('renders', () => { + render(Test); + expect(screen.getByTestId('control')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/skeleton-svelte/test/components/floating-panel/test.svelte b/packages/skeleton-svelte/test/components/floating-panel/test.svelte new file mode 100644 index 0000000000..4ca365123f --- /dev/null +++ b/packages/skeleton-svelte/test/components/floating-panel/test.svelte @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/playgrounds/skeleton-react/app/components/floating-panel/page.tsx b/playgrounds/skeleton-react/app/components/floating-panel/page.tsx new file mode 100644 index 0000000000..ec82022429 --- /dev/null +++ b/playgrounds/skeleton-react/app/components/floating-panel/page.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; +import { XIcon, MinusIcon, MaximizeIcon, MinimizeIcon } from 'lucide-react'; + +export default function Page() { + return ( + + Open Panel + + + + + + Floating Panel + + + + + + + + + + + + + + + + + +

This is a floating panel that can be dragged, resized, minimized, and maximized.

+

Try dragging from the header or resizing from the bottom-right corner.

+ +
+
+
+
+
+ ); +} diff --git a/playgrounds/skeleton-svelte/src/routes/components/floating-panel/+page.svelte b/playgrounds/skeleton-svelte/src/routes/components/floating-panel/+page.svelte new file mode 100644 index 0000000000..4b7d642621 --- /dev/null +++ b/playgrounds/skeleton-svelte/src/routes/components/floating-panel/+page.svelte @@ -0,0 +1,38 @@ + + + + Open Panel + + + + + + Floating Panel + + + + + + + + + + + + + + + + + +

This is a floating panel that can be dragged, resized, minimized, and maximized.

+

Try dragging from the header or resizing from the bottom-right corner.

+ +
+
+
+
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af68393221..951d919dca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,6 +159,9 @@ catalogs: '@zag-js/file-upload': specifier: 1.29.0 version: 1.29.0 + '@zag-js/floating-panel': + specifier: 1.29.0 + version: 1.29.0 '@zag-js/listbox': specifier: 1.29.0 version: 1.29.0 @@ -538,6 +541,9 @@ importers: '@zag-js/file-upload': specifier: 'catalog:' version: 1.29.0 + '@zag-js/floating-panel': + specifier: 'catalog:' + version: 1.29.0 '@zag-js/listbox': specifier: 'catalog:' version: 1.29.0 @@ -656,6 +662,9 @@ importers: '@zag-js/file-upload': specifier: 'catalog:' version: 1.29.0 + '@zag-js/floating-panel': + specifier: 'catalog:' + version: 1.29.0 '@zag-js/listbox': specifier: 'catalog:' version: 1.29.0 @@ -3119,6 +3128,9 @@ packages: '@zag-js/file-utils@1.29.0': resolution: {integrity: sha512-T9etdsDPrCYTUQmy6/D6Tz2AteK6FVRfJRecxG3OJ7OVv1F6B4zH2SCJgpwnqJj6NBKjXLK1rVYuGzphJXNZ4g==} + '@zag-js/floating-panel@1.29.0': + resolution: {integrity: sha512-7jT0h3jvOdKjEdBFhXb6vdUmeZTt5o5+d+mEdRCWWm5be35TdkenJw5FoRlzS6CF3e0Xhm7iROl86x2hM9c2Zw==} + '@zag-js/focus-trap@1.29.0': resolution: {integrity: sha512-eZCKUT7VJzCHCeIyCdX9hG+c5a4uMnHLA0CBDGFsB69On5wQ3v8N5febbdpEFvJMfOrf93HVK1cNHCZ+rvvQNA==} @@ -3655,9 +3667,6 @@ packages: resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} engines: {node: '>=18'} - devalue@5.4.2: - resolution: {integrity: sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==} - devalue@5.5.0: resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} @@ -4141,10 +4150,6 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -5237,10 +5242,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - smol-toml@1.4.2: - resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==} - engines: {node: '>= 18'} - smol-toml@1.5.2: resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} engines: {node: '>= 18'} @@ -6156,7 +6157,7 @@ snapshots: hast-util-from-html: 2.0.3 hast-util-to-text: 4.0.2 import-meta-resolve: 4.2.0 - js-yaml: 4.1.0 + js-yaml: 4.1.1 mdast-util-definitions: 6.0.0 rehype-raw: 7.0.0 rehype-stringify: 10.0.1 @@ -6165,7 +6166,7 @@ snapshots: remark-rehype: 11.1.2 remark-smartypants: 3.0.2 shiki: 3.15.0 - smol-toml: 1.4.2 + smol-toml: 1.5.2 unified: 11.0.5 unist-util-remove-position: 5.0.0 unist-util-visit: 5.0.0 @@ -7613,7 +7614,7 @@ snapshots: '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.4.2 + devalue: 5.5.0 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -7633,7 +7634,7 @@ snapshots: '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.4.2 + devalue: 5.5.0 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -8223,6 +8224,17 @@ snapshots: dependencies: '@zag-js/i18n-utils': 1.29.0 + '@zag-js/floating-panel@1.29.0': + dependencies: + '@zag-js/anatomy': 1.29.0 + '@zag-js/core': 1.29.0 + '@zag-js/dom-query': 1.29.0 + '@zag-js/popper': 1.29.0 + '@zag-js/rect-utils': 1.29.0 + '@zag-js/store': 1.29.0 + '@zag-js/types': 1.29.0 + '@zag-js/utils': 1.29.0 + '@zag-js/focus-trap@1.29.0': dependencies: '@zag-js/dom-query': 1.29.0 @@ -8887,8 +8899,6 @@ snapshots: dependencies: base-64: 1.0.0 - devalue@5.4.2: {} - devalue@5.5.0: {} devlop@1.1.0: @@ -9456,10 +9466,6 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -10905,8 +10911,6 @@ snapshots: slash@3.0.0: {} - smol-toml@1.4.2: {} - smol-toml@1.5.2: {} source-map-js@1.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5f2ac8f0a2..7f4bd2b632 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -55,6 +55,7 @@ catalog: '@zag-js/date-picker': 1.29.0 '@zag-js/dialog': 1.29.0 '@zag-js/file-upload': 1.29.0 + '@zag-js/floating-panel': 1.29.0 '@zag-js/listbox': 1.29.0 '@zag-js/menu': 1.29.0 '@zag-js/pagination': 1.29.0 diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/anchor-position.tsx b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/anchor-position.tsx new file mode 100644 index 0000000000..706066cdfc --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/anchor-position.tsx @@ -0,0 +1,53 @@ +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; +import { GripVerticalIcon, XIcon, MinusIcon, MaximizeIcon, MinimizeIcon } from 'lucide-react'; + +export default function AnchorPosition() { + return ( +
+ { + if (!ctx.triggerRect) return { x: 0, y: 0 }; + return { + x: ctx.triggerRect.x + ctx.triggerRect.width / 2, + y: ctx.triggerRect.y + ctx.triggerRect.height / 2, + }; + }} + > + Open Panel + + + + + + + + Anchored Panel + + + + + + + + + + + + + + + + + + +

This panel is centered in the viewport using getAnchorPosition.

+

The position is calculated based on the boundary rectangle.

+
+ +
+
+
+
+
+ ); +} diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/controlled.tsx b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/controlled.tsx new file mode 100644 index 0000000000..629576b3c9 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/controlled.tsx @@ -0,0 +1,81 @@ +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; +import { GripVerticalIcon, XIcon, MinusIcon, MaximizeIcon, MinimizeIcon } from 'lucide-react'; +import { useState } from 'react'; + +export default function Controlled() { + const [open, setOpen] = useState(false); + const [size, setSize] = useState({ + width: 400, + height: 300, + }); + + return ( + <> +
+ + + +
+ + setOpen(details.open)} + size={size} + onSizeChange={(details) => setSize(details.size)} + > + + + + + + + + Controlled Panel + + + + + + + + + + + + + + + + + + +

This panel's open state and size are controlled via the inputs above.

+

Try changing the values or resizing/closing the panel to see the inputs update.

+
+ +
+
+
+
+ + ); +} diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/default.tsx b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/default.tsx new file mode 100644 index 0000000000..dc527b88b6 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/default.tsx @@ -0,0 +1,45 @@ +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; +import { GripVerticalIcon, MinimizeIcon, XIcon, MinusIcon, MaximizeIcon } from 'lucide-react'; + +export default function Default() { + return ( + + Open Panel + + + + + + + + Floating Panel + + + + + + + + + + + + + + + + + + +

+ This is a floating panel that can be dragged, resized, minimized, and maximized. Try dragging from the header or resizing + from the bottom-right corner. +

+
+ +
+
+
+
+ ); +} diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/dir.tsx b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/dir.tsx new file mode 100644 index 0000000000..5df5374815 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/dir.tsx @@ -0,0 +1,43 @@ +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; +import { GripVerticalIcon, XIcon, MinusIcon, MaximizeIcon, MinimizeIcon } from 'lucide-react'; + +export default function Dir() { + return ( + + Open Panel + + + + + + + + Floating Panel + + + + + + + + + + + + + + + + + + +

This is a floating panel with right-to-left (RTL) direction.

+

You can drag it from the header or resize it from the bottom-right corner.

+
+ +
+
+
+
+ ); +} diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/disable-dragging.tsx b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/disable-dragging.tsx new file mode 100644 index 0000000000..a0af80b9d7 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/disable-dragging.tsx @@ -0,0 +1,43 @@ +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; +import { GripVerticalIcon, XIcon, MinusIcon, MaximizeIcon, MinimizeIcon } from 'lucide-react'; + +export default function DisableDragging() { + return ( + + Open Panel + + + + + + + + Fixed Position Panel + + + + + + + + + + + + + + + + + + +

This panel cannot be dragged - the position is fixed.

+

However, it can still be resized from the bottom-right corner.

+
+ +
+
+
+
+ ); +} diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/disable-resizing.tsx b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/disable-resizing.tsx new file mode 100644 index 0000000000..39f6523344 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/disable-resizing.tsx @@ -0,0 +1,43 @@ +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; +import { GripVerticalIcon, XIcon, MinusIcon, MaximizeIcon, MinimizeIcon } from 'lucide-react'; + +export default function DisableResizing() { + return ( + + Open Panel + + + + + + + + Fixed Size Panel + + + + + + + + + + + + + + + + + + +

This panel cannot be resized.

+

Try dragging the edges - they won't respond.

+
+ +
+
+
+
+ ); +} diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/resize-triggers.tsx b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/resize-triggers.tsx new file mode 100644 index 0000000000..81dbb70b41 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/resize-triggers.tsx @@ -0,0 +1,52 @@ +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; +import { GripVerticalIcon, MinimizeIcon, XIcon, MinusIcon, MaximizeIcon } from 'lucide-react'; + +export default function Default() { + return ( + + Open Panel + + + + + + + + Floating Panel + + + + + + + + + + + + + + + + + + +

+ This is a floating panel that can be dragged, resized, minimized, and maximized. Try dragging from the header or resizing + from the bottom-right corner. +

+
+ + + + + + + + +
+
+
+
+ ); +} diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/size-constraints.tsx b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/size-constraints.tsx new file mode 100644 index 0000000000..b080be4552 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/react/size-constraints.tsx @@ -0,0 +1,43 @@ +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; +import { GripVerticalIcon, MinimizeIcon, XIcon, MinusIcon, MaximizeIcon } from 'lucide-react'; + +export default function SizeConstraints() { + return ( + + Open Panel + + + + + + + + Floating Panel + + + + + + + + + + + + + + + + + + +

This panel has size constraints applied: minimum 300x200 pixels and maximum 900x600 pixels.

+

Try resizing from the bottom-right corner - the panel will respect these boundaries.

+
+ +
+
+
+
+ ); +} diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/anchor-position.svelte b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/anchor-position.svelte new file mode 100644 index 0000000000..6601af37b0 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/anchor-position.svelte @@ -0,0 +1,51 @@ + + +
+ { + if (!ctx.triggerRect) return { x: 0, y: 0 }; + return { + x: ctx.triggerRect.x + ctx.triggerRect.width / 2, + y: ctx.triggerRect.y + ctx.triggerRect.height / 2, + }; + }} + > + Open Panel + + + + + + + + Anchored Panel + + + + + + + + + + + + + + + + + + +

This panel is centered in the viewport using getAnchorPosition.

+

The position is calculated based on the boundary rectangle.

+
+ +
+
+
+
+
diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/controlled.svelte b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/controlled.svelte new file mode 100644 index 0000000000..3440096ca7 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/controlled.svelte @@ -0,0 +1,61 @@ + + +
+ + + +
+ + (open = details.open)} {size} onSizeChange={(details) => (size = details.size)}> + + + + + + + + Controlled Panel + + + + + + + + + + + + + + + + + + +

This panel's open state and size are controlled via the inputs above.

+

Try changing the values or resizing/closing the panel to see the inputs update.

+
+ +
+
+
+
diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/default.svelte b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/default.svelte new file mode 100644 index 0000000000..7b435b4806 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/default.svelte @@ -0,0 +1,43 @@ + + + + Open Panel + + + + + + + + Floating Panel + + + + + + + + + + + + + + + + + + +

+ This is a floating panel that can be dragged, resized, minimized, and maximized. Try dragging from the header or resizing from + the bottom-right corner. +

+
+ +
+
+
+
diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/dir.svelte b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/dir.svelte new file mode 100644 index 0000000000..afb59813dd --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/dir.svelte @@ -0,0 +1,41 @@ + + + + Open Panel + + + + + + + + Floating Panel + + + + + + + + + + + + + + + + + + +

This is a floating panel with right-to-left (RTL) direction.

+

You can drag it from the header or resize it from the bottom-right corner.

+
+ +
+
+
+
diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/disable-dragging.svelte b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/disable-dragging.svelte new file mode 100644 index 0000000000..b144152e7a --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/disable-dragging.svelte @@ -0,0 +1,41 @@ + + + + Open Panel + + + + + + + + Fixed Position Panel + + + + + + + + + + + + + + + + + + +

This panel cannot be dragged - the position is fixed.

+

However, it can still be resized from the bottom-right corner.

+
+ +
+
+
+
diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/disable-resizing.svelte b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/disable-resizing.svelte new file mode 100644 index 0000000000..3a7c5cd583 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/disable-resizing.svelte @@ -0,0 +1,41 @@ + + + + Open Panel + + + + + + + + Fixed Size Panel + + + + + + + + + + + + + + + + + + +

This panel cannot be resized.

+

Try dragging the edges - they won't respond.

+
+ +
+
+
+
diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/resize-triggers.svelte b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/resize-triggers.svelte new file mode 100644 index 0000000000..9ea27c30d2 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/resize-triggers.svelte @@ -0,0 +1,50 @@ + + + + Open Panel + + + + + + + + Floating Panel + + + + + + + + + + + + + + + + + + +

+ This is a floating panel that can be dragged, resized, minimized, and maximized. Try dragging from the header or resizing from + the bottom-right corner. +

+
+ + + + + + + + +
+
+
+
diff --git a/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/size-constraints.svelte b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/size-constraints.svelte new file mode 100644 index 0000000000..d2343b3899 --- /dev/null +++ b/sites/skeleton.dev/src/components/examples/framework-components/floating-panel/svelte/size-constraints.svelte @@ -0,0 +1,41 @@ + + + + Open Panel + + + + + + + + Floating Panel + + + + + + + + + + + + + + + + + + +

This panel has size constraints applied: minimum 300x200 pixels and maximum 900x600 pixels.

+

Try resizing from the bottom-right corner - the panel will respect these boundaries.

+
+ +
+
+
+
diff --git a/sites/skeleton.dev/src/content/docs/framework-components/floating-panel.mdx b/sites/skeleton.dev/src/content/docs/framework-components/floating-panel.mdx new file mode 100644 index 0000000000..514181aad4 --- /dev/null +++ b/sites/skeleton.dev/src/content/docs/framework-components/floating-panel.mdx @@ -0,0 +1,236 @@ +--- +title: Floating Panel +description: A draggable, resizable floating panel with minimize/maximize controls. +stability: beta +--- + +import AnchorPositionReact from '@/components/examples/framework-components/floating-panel/react/anchor-position'; +import AnchorPositionReactRaw from '@/components/examples/framework-components/floating-panel/react/anchor-position?raw'; +import ControlledReact from '@/components/examples/framework-components/floating-panel/react/controlled'; +import ControlledReactRaw from '@/components/examples/framework-components/floating-panel/react/controlled?raw'; +import DefaultReact from '@/components/examples/framework-components/floating-panel/react/default'; +import DefaultReactRaw from '@/components/examples/framework-components/floating-panel/react/default?raw'; +import DirReact from '@/components/examples/framework-components/floating-panel/react/dir'; +import DirReactRaw from '@/components/examples/framework-components/floating-panel/react/dir?raw'; +import DisableDraggingReact from '@/components/examples/framework-components/floating-panel/react/disable-dragging'; +import DisableDraggingReactRaw from '@/components/examples/framework-components/floating-panel/react/disable-dragging?raw'; +import DisableResizingReact from '@/components/examples/framework-components/floating-panel/react/disable-resizing'; +import DisableResizingReactRaw from '@/components/examples/framework-components/floating-panel/react/disable-resizing?raw'; +import ResizeTriggersReact from '@/components/examples/framework-components/floating-panel/react/resize-triggers'; +import ResizeTriggersReactRaw from '@/components/examples/framework-components/floating-panel/react/resize-triggers?raw'; +import SizeConstraintsReact from '@/components/examples/framework-components/floating-panel/react/size-constraints'; +import SizeConstraintsReactRaw from '@/components/examples/framework-components/floating-panel/react/size-constraints?raw'; +import AnchorPositionSvelte from '@/components/examples/framework-components/floating-panel/svelte/anchor-position.svelte'; +import AnchorPositionSvelteRaw from '@/components/examples/framework-components/floating-panel/svelte/anchor-position.svelte?raw'; +import ControlledSvelte from '@/components/examples/framework-components/floating-panel/svelte/controlled.svelte'; +import ControlledSvelteRaw from '@/components/examples/framework-components/floating-panel/svelte/controlled.svelte?raw'; +import DefaultSvelte from '@/components/examples/framework-components/floating-panel/svelte/default.svelte'; +import DefaultSvelteRaw from '@/components/examples/framework-components/floating-panel/svelte/default.svelte?raw'; +import DirSvelte from '@/components/examples/framework-components/floating-panel/svelte/dir.svelte'; +import DirSvelteRaw from '@/components/examples/framework-components/floating-panel/svelte/dir.svelte?raw'; +import DisableDraggingSvelte from '@/components/examples/framework-components/floating-panel/svelte/disable-dragging.svelte'; +import DisableDraggingSvelteRaw from '@/components/examples/framework-components/floating-panel/svelte/disable-dragging.svelte?raw'; +import DisableResizingSvelte from '@/components/examples/framework-components/floating-panel/svelte/disable-resizing.svelte'; +import DisableResizingSvelteRaw from '@/components/examples/framework-components/floating-panel/svelte/disable-resizing.svelte?raw'; +import ResizeTriggersSvelte from '@/components/examples/framework-components/floating-panel/svelte/resize-triggers.svelte'; +import ResizeTriggersSvelteRaw from '@/components/examples/framework-components/floating-panel/svelte/resize-triggers.svelte?raw'; +import SizeConstraintsSvelte from '@/components/examples/framework-components/floating-panel/svelte/size-constraints.svelte'; +import SizeConstraintsSvelteRaw from '@/components/examples/framework-components/floating-panel/svelte/size-constraints.svelte?raw'; + + + + + + + + + + + + +## Size Constraints + +Use the `minSize` and `maxSize` props to set size constraints on the Floating Panel. + + + + + + + + + + + + + +## Controlled + +Control the open state and size of the Floating Panel with your own state. + + + + + + + + + + + + + +## Anchor Position + +Position the panel relative to another element using the `defaultPosition` prop. + + + + + + + + + + + + + +## Resize Triggers + +Add resize triggers on all sides and corners of the Floating Panel using the `axis` prop. + + + + + + + + + + + + +## Disable Dragging + +Disable dragging by setting the `draggable` prop to `false`. The panel will remain in a fixed position but can still be resized. + + + + + + + + + + + + + +## Disable Resizing + +Disable resizing by setting the `resizable` prop to `false`. The panel will have a fixed size but can still be dragged. + + + + + + + + + + + + + +## Direction + +Set the text direction (`ltr` or `rtl`) using the `dir` prop. + + + + + + + + + + + + + +## Anatomy + +Here's an overview of how the Floating Panel component is structured in code: + + + +```tsx +import { FloatingPanel, Portal } from '@skeletonlabs/skeleton-react'; + +export default function Anatomy() { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} +``` + + + + +```svelte + + + + + + + + + + + + + + + + + + + + + + +``` + + + +## API Reference + + + + + + +