From 295dd1388c623949fcf1c7ddcf04bf6ac7dd0969 Mon Sep 17 00:00:00 2001 From: Mats Bosson Date: Sun, 29 Mar 2026 09:15:53 +0700 Subject: [PATCH] Switch to sub-path exports Consumers use sub-path imports (e.g. "pettyui/slider") for tree-shaking. Adds Drawer component + 15 sub-path entries to package.json exports. --- packages/core/package.json | 25 ++-- .../src/components/drawer/drawer-close.tsx | 27 +++++ .../src/components/drawer/drawer-content.tsx | 65 ++++++++++ .../src/components/drawer/drawer-context.ts | 58 +++++++++ .../components/drawer/drawer-description.tsx | 24 ++++ .../src/components/drawer/drawer-overlay.tsx | 22 ++++ .../src/components/drawer/drawer-portal.tsx | 20 ++++ .../src/components/drawer/drawer-root.tsx | 68 +++++++++++ .../src/components/drawer/drawer-title.tsx | 24 ++++ .../src/components/drawer/drawer-trigger.tsx | 35 ++++++ packages/core/src/components/drawer/index.ts | 31 +++++ packages/core/src/index.ts | 31 ----- .../tests/components/drawer/drawer.test.tsx | 112 ++++++++++++++++++ 13 files changed, 502 insertions(+), 40 deletions(-) create mode 100644 packages/core/src/components/drawer/drawer-close.tsx create mode 100644 packages/core/src/components/drawer/drawer-content.tsx create mode 100644 packages/core/src/components/drawer/drawer-context.ts create mode 100644 packages/core/src/components/drawer/drawer-description.tsx create mode 100644 packages/core/src/components/drawer/drawer-overlay.tsx create mode 100644 packages/core/src/components/drawer/drawer-portal.tsx create mode 100644 packages/core/src/components/drawer/drawer-root.tsx create mode 100644 packages/core/src/components/drawer/drawer-title.tsx create mode 100644 packages/core/src/components/drawer/drawer-trigger.tsx create mode 100644 packages/core/src/components/drawer/index.ts delete mode 100644 packages/core/src/index.ts create mode 100644 packages/core/tests/components/drawer/drawer.test.tsx diff --git a/packages/core/package.json b/packages/core/package.json index ec5b852..5613694 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,15 +3,7 @@ "version": "0.1.0", "description": "AI-native headless UI component library for SolidJS", "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", "exports": { - ".": { - "solid": "./src/index.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - }, "./dialog": { "solid": "./src/components/dialog/index.ts", "import": "./dist/dialog/index.js", @@ -46,7 +38,22 @@ "solid": "./src/utilities/visually-hidden/index.ts", "import": "./dist/utilities/visually-hidden/index.js", "require": "./dist/utilities/visually-hidden/index.cjs" - } + }, + "./separator": { "solid": "./src/components/separator/index.ts", "import": "./dist/components/separator/index.js", "require": "./dist/components/separator/index.cjs" }, + "./toggle": { "solid": "./src/components/toggle/index.ts", "import": "./dist/components/toggle/index.js", "require": "./dist/components/toggle/index.cjs" }, + "./switch": { "solid": "./src/components/switch/index.ts", "import": "./dist/components/switch/index.js", "require": "./dist/components/switch/index.cjs" }, + "./checkbox": { "solid": "./src/components/checkbox/index.ts", "import": "./dist/components/checkbox/index.js", "require": "./dist/components/checkbox/index.cjs" }, + "./progress": { "solid": "./src/components/progress/index.ts", "import": "./dist/components/progress/index.js", "require": "./dist/components/progress/index.cjs" }, + "./text-field": { "solid": "./src/components/text-field/index.ts", "import": "./dist/components/text-field/index.js", "require": "./dist/components/text-field/index.cjs" }, + "./radio-group": { "solid": "./src/components/radio-group/index.ts", "import": "./dist/components/radio-group/index.js", "require": "./dist/components/radio-group/index.cjs" }, + "./toggle-group": { "solid": "./src/components/toggle-group/index.ts", "import": "./dist/components/toggle-group/index.js", "require": "./dist/components/toggle-group/index.cjs" }, + "./collapsible": { "solid": "./src/components/collapsible/index.ts", "import": "./dist/components/collapsible/index.js", "require": "./dist/components/collapsible/index.cjs" }, + "./accordion": { "solid": "./src/components/accordion/index.ts", "import": "./dist/components/accordion/index.js", "require": "./dist/components/accordion/index.cjs" }, + "./alert-dialog": { "solid": "./src/components/alert-dialog/index.ts", "import": "./dist/components/alert-dialog/index.js", "require": "./dist/components/alert-dialog/index.cjs" }, + "./tabs": { "solid": "./src/components/tabs/index.ts", "import": "./dist/components/tabs/index.js", "require": "./dist/components/tabs/index.cjs" }, + "./slider": { "solid": "./src/components/slider/index.ts", "import": "./dist/components/slider/index.js", "require": "./dist/components/slider/index.cjs" }, + "./pagination": { "solid": "./src/components/pagination/index.ts", "import": "./dist/components/pagination/index.js", "require": "./dist/components/pagination/index.cjs" }, + "./drawer": { "solid": "./src/components/drawer/index.ts", "import": "./dist/components/drawer/index.js", "require": "./dist/components/drawer/index.cjs" } }, "scripts": { "build": "tsdown", diff --git a/packages/core/src/components/drawer/drawer-close.tsx b/packages/core/src/components/drawer/drawer-close.tsx new file mode 100644 index 0000000..95ec709 --- /dev/null +++ b/packages/core/src/components/drawer/drawer-close.tsx @@ -0,0 +1,27 @@ +import type { JSX } from "solid-js"; +import { splitProps } from "solid-js"; +import { useInternalDrawerContext } from "./drawer-context"; + +/** Props for Drawer.Close. */ +export interface DrawerCloseProps extends JSX.ButtonHTMLAttributes { + children?: JSX.Element; +} + +/** + * Closes the Drawer when clicked. + */ +export function DrawerClose(props: DrawerCloseProps): JSX.Element { + const [local, rest] = splitProps(props, ["children", "onClick"]); + const ctx = useInternalDrawerContext(); + + const handleClick: JSX.EventHandler = (e) => { + if (typeof local.onClick === "function") local.onClick(e); + ctx.setOpen(false); + }; + + return ( + + ); +} diff --git a/packages/core/src/components/drawer/drawer-content.tsx b/packages/core/src/components/drawer/drawer-content.tsx new file mode 100644 index 0000000..29335e6 --- /dev/null +++ b/packages/core/src/components/drawer/drawer-content.tsx @@ -0,0 +1,65 @@ +import type { JSX } from "solid-js"; +import { Show, createEffect, onCleanup, splitProps } from "solid-js"; +import { createDismiss } from "../../utilities/dismiss/create-dismiss"; +import { createFocusTrap } from "../../utilities/focus-trap/create-focus-trap"; +import { createScrollLock } from "../../utilities/scroll-lock/create-scroll-lock"; +import { useInternalDrawerContext } from "./drawer-context"; + +/** Props for Drawer.Content. */ +export interface DrawerContentProps extends JSX.HTMLAttributes { + /** Keep mounted even when closed (for animation control). */ + forceMount?: boolean; + children?: JSX.Element; +} + +/** + * Drawer content panel. Activates focus trap, scroll lock, and Escape/outside dismiss when open. + * Sets data-side to indicate which edge the drawer slides from. + */ +export function DrawerContent(props: DrawerContentProps): JSX.Element { + const [local, rest] = splitProps(props, ["children", "forceMount"]); + const ctx = useInternalDrawerContext(); + let contentRef: HTMLDivElement | undefined; + + const focusTrap = createFocusTrap(() => contentRef ?? null); + const scrollLock = createScrollLock(); + const dismiss = createDismiss({ + getContainer: () => contentRef ?? null, + onDismiss: () => ctx.setOpen(false), + }); + + createEffect(() => { + if (ctx.isOpen()) { + focusTrap.activate(); + scrollLock.lock(); + dismiss.attach(); + } else { + focusTrap.deactivate(); + scrollLock.unlock(); + dismiss.detach(); + } + onCleanup(() => { + focusTrap.deactivate(); + scrollLock.unlock(); + dismiss.detach(); + }); + }); + + return ( + + + + ); +} diff --git a/packages/core/src/components/drawer/drawer-context.ts b/packages/core/src/components/drawer/drawer-context.ts new file mode 100644 index 0000000..8db3d3f --- /dev/null +++ b/packages/core/src/components/drawer/drawer-context.ts @@ -0,0 +1,58 @@ +import type { Accessor } from "solid-js"; +import { createContext, useContext } from "solid-js"; + +/** Which edge the Drawer slides from. */ +export type DrawerSide = "top" | "bottom" | "left" | "right"; + +/** Internal context shared between all Drawer parts. */ +export interface InternalDrawerContextValue { + isOpen: Accessor; + setOpen: (open: boolean) => void; + side: Accessor; + contentId: Accessor; + titleId: Accessor; + setTitleId: (id: string | undefined) => void; + descriptionId: Accessor; + setDescriptionId: (id: string | undefined) => void; +} + +const InternalDrawerContext = createContext(); + +/** + * Returns the internal Drawer context. Throws if used outside . + */ +export function useInternalDrawerContext(): InternalDrawerContextValue { + const ctx = useContext(InternalDrawerContext); + if (!ctx) { + throw new Error( + "[PettyUI] Drawer parts must be used inside .\n" + + " Fix: Wrap Drawer.Content, Drawer.Trigger, etc. inside .", + ); + } + return ctx; +} + +export const InternalDrawerContextProvider = InternalDrawerContext.Provider; + +/** Public context exposed to consumers via Drawer.useContext(). */ +export interface DrawerContextValue { + /** Whether the drawer is currently open. */ + open: Accessor; + /** Which edge the drawer slides from. */ + side: Accessor; +} + +const DrawerContext = createContext(); + +/** + * Returns the public Drawer context. Throws if used outside . + */ +export function useDrawerContext(): DrawerContextValue { + const ctx = useContext(DrawerContext); + if (!ctx) { + throw new Error("[PettyUI] Drawer.useContext() called outside of ."); + } + return ctx; +} + +export const DrawerContextProvider = DrawerContext.Provider; diff --git a/packages/core/src/components/drawer/drawer-description.tsx b/packages/core/src/components/drawer/drawer-description.tsx new file mode 100644 index 0000000..33b0c12 --- /dev/null +++ b/packages/core/src/components/drawer/drawer-description.tsx @@ -0,0 +1,24 @@ +import type { JSX } from "solid-js"; +import { createUniqueId, onCleanup, onMount, splitProps } from "solid-js"; +import { useInternalDrawerContext } from "./drawer-context"; + +/** Props for Drawer.Description. */ +export interface DrawerDescriptionProps extends JSX.HTMLAttributes { + children?: JSX.Element; +} + +/** + * Description for the Drawer. Registers its ID for aria-describedby. + */ +export function DrawerDescription(props: DrawerDescriptionProps): JSX.Element { + const [local, rest] = splitProps(props, ["children"]); + const ctx = useInternalDrawerContext(); + const id = createUniqueId(); + onMount(() => ctx.setDescriptionId(id)); + onCleanup(() => ctx.setDescriptionId(undefined)); + return ( +

+ {local.children} +

+ ); +} diff --git a/packages/core/src/components/drawer/drawer-overlay.tsx b/packages/core/src/components/drawer/drawer-overlay.tsx new file mode 100644 index 0000000..7268156 --- /dev/null +++ b/packages/core/src/components/drawer/drawer-overlay.tsx @@ -0,0 +1,22 @@ +import type { JSX } from "solid-js"; +import { Show, splitProps } from "solid-js"; +import { useInternalDrawerContext } from "./drawer-context"; + +/** Props for Drawer.Overlay. */ +export interface DrawerOverlayProps extends JSX.HTMLAttributes { + /** Keep mounted even when closed (for animation control). */ + forceMount?: boolean; +} + +/** + * Semi-transparent overlay behind Drawer content. + */ +export function DrawerOverlay(props: DrawerOverlayProps): JSX.Element { + const [local, rest] = splitProps(props, ["forceMount"]); + const ctx = useInternalDrawerContext(); + return ( + +