From db274669de957f764d03d8ffbc53cd20269e6ff8 Mon Sep 17 00:00:00 2001 From: ChanHyung-Lee Date: Thu, 5 Feb 2026 16:51:07 +0900 Subject: [PATCH 01/14] feat: add anatomy JSON files for various components and implement component explorer - Created anatomy JSON files for Popover, RadioGroup, Radio, Select, Sheet, Switch, Table, Tabs, Toast, Tooltip components. - Implemented a script to generate anatomy files from component parts. - Enhanced the preview page to support an explorer mode for visualizing component anatomy. - Developed the ComponentExplorer and AnatomyPanel components for displaying component parts. - Added highlight overlay functionality to visually indicate hovered parts in the component preview. - Updated the layout and styling of the preview components for better user experience. --- .../content/docs/components/dialog.mdx | 8 ++ apps/website/package.json | 1 + .../public/components/anatomy/avatar.json | 21 +++ .../public/components/anatomy/breadcrumb.json | 51 +++++++ .../public/components/anatomy/callout.json | 16 +++ .../public/components/anatomy/card.json | 26 ++++ .../public/components/anatomy/checkbox.json | 16 +++ .../components/anatomy/collapsible.json | 21 +++ .../public/components/anatomy/dialog.json | 66 ++++++++++ .../public/components/anatomy/field.json | 36 +++++ .../components/anatomy/floating-bar.json | 41 ++++++ .../public/components/anatomy/grid.json | 16 +++ .../components/anatomy/input-group.json | 16 +++ .../public/components/anatomy/menu.json | 111 ++++++++++++++++ .../components/anatomy/multi-select.json | 86 ++++++++++++ .../components/anatomy/navigation-menu.json | 71 ++++++++++ .../public/components/anatomy/pagination.json | 61 +++++++++ .../public/components/anatomy/popover.json | 51 +++++++ .../components/anatomy/radio-group.json | 16 +++ .../public/components/anatomy/radio.json | 16 +++ .../public/components/anatomy/select.json | 86 ++++++++++++ .../public/components/anatomy/sheet.json | 71 ++++++++++ .../public/components/anatomy/switch.json | 16 +++ .../public/components/anatomy/table.json | 51 +++++++ .../public/components/anatomy/tabs.json | 36 +++++ .../public/components/anatomy/toast.json | 61 +++++++++ .../public/components/anatomy/tooltip.json | 36 +++++ apps/website/scripts/generate-anatomy.mjs | 89 +++++++++++++ .../src/app/preview/[component]/page.tsx | 4 +- .../preview/[component]/preview-wrapper.tsx | 14 +- apps/website/src/app/preview/layout.tsx | 4 +- .../component-explorer/anatomy-panel.tsx | 86 ++++++++++++ .../component-explorer/component-explorer.tsx | 118 +++++++++++++++++ .../iframe/highlight-overlay.tsx | 124 ++++++++++++++++++ .../iframe/use-highlight-receiver.ts | 29 ++++ .../components/component-explorer/index.ts | 2 + .../component-explorer/part-button.tsx | 101 ++++++++++++++ .../components/component-explorer/types.ts | 29 ++++ .../use-explorer-communication.ts | 34 +++++ .../demo/examples/dialog/anatomy-dialog.tsx | 29 ++++ apps/website/src/mdx-components.tsx | 2 + 41 files changed, 1764 insertions(+), 5 deletions(-) create mode 100644 apps/website/public/components/anatomy/avatar.json create mode 100644 apps/website/public/components/anatomy/breadcrumb.json create mode 100644 apps/website/public/components/anatomy/callout.json create mode 100644 apps/website/public/components/anatomy/card.json create mode 100644 apps/website/public/components/anatomy/checkbox.json create mode 100644 apps/website/public/components/anatomy/collapsible.json create mode 100644 apps/website/public/components/anatomy/dialog.json create mode 100644 apps/website/public/components/anatomy/field.json create mode 100644 apps/website/public/components/anatomy/floating-bar.json create mode 100644 apps/website/public/components/anatomy/grid.json create mode 100644 apps/website/public/components/anatomy/input-group.json create mode 100644 apps/website/public/components/anatomy/menu.json create mode 100644 apps/website/public/components/anatomy/multi-select.json create mode 100644 apps/website/public/components/anatomy/navigation-menu.json create mode 100644 apps/website/public/components/anatomy/pagination.json create mode 100644 apps/website/public/components/anatomy/popover.json create mode 100644 apps/website/public/components/anatomy/radio-group.json create mode 100644 apps/website/public/components/anatomy/radio.json create mode 100644 apps/website/public/components/anatomy/select.json create mode 100644 apps/website/public/components/anatomy/sheet.json create mode 100644 apps/website/public/components/anatomy/switch.json create mode 100644 apps/website/public/components/anatomy/table.json create mode 100644 apps/website/public/components/anatomy/tabs.json create mode 100644 apps/website/public/components/anatomy/toast.json create mode 100644 apps/website/public/components/anatomy/tooltip.json create mode 100644 apps/website/scripts/generate-anatomy.mjs create mode 100644 apps/website/src/components/component-explorer/anatomy-panel.tsx create mode 100644 apps/website/src/components/component-explorer/component-explorer.tsx create mode 100644 apps/website/src/components/component-explorer/iframe/highlight-overlay.tsx create mode 100644 apps/website/src/components/component-explorer/iframe/use-highlight-receiver.ts create mode 100644 apps/website/src/components/component-explorer/index.ts create mode 100644 apps/website/src/components/component-explorer/part-button.tsx create mode 100644 apps/website/src/components/component-explorer/types.ts create mode 100644 apps/website/src/components/component-explorer/use-explorer-communication.ts create mode 100644 apps/website/src/components/demo/examples/dialog/anatomy-dialog.tsx diff --git a/apps/website/content/docs/components/dialog.mdx b/apps/website/content/docs/components/dialog.mdx index 4d6339cc5..5f943155f 100644 --- a/apps/website/content/docs/components/dialog.mdx +++ b/apps/website/content/docs/components/dialog.mdx @@ -60,6 +60,14 @@ Dialog 컴포넌트의 다양한 구성 패턴입니다. ``` +## Anatomy + +--- + +컴포넌트의 구조를 탐색하고 각 파트의 역할을 확인합니다. 파트 이름에 마우스를 올리면 해당 영역이 하이라이트됩니다. + + + ## Props Table --- diff --git a/apps/website/package.json b/apps/website/package.json index c0b1da360..6107c9e74 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "build": "next build", + "generate:anatomy": "node scripts/generate-anatomy.mjs", "clean": "rm -rf node_modules .next .turbo .source", "dev": "next dev --turbo", "format": "prettier --write \"./src/**/*.{ts,tsx,md}\"", diff --git a/apps/website/public/components/anatomy/avatar.json b/apps/website/public/components/anatomy/avatar.json new file mode 100644 index 000000000..813befa80 --- /dev/null +++ b/apps/website/public/components/anatomy/avatar.json @@ -0,0 +1,21 @@ +{ + "componentName": "Avatar", + "displayNamePrefix": "Avatar", + "parts": [ + { + "name": "Root", + "fullName": "AvatarRoot", + "isPrimitive": false + }, + { + "name": "ImagePrimitive", + "fullName": "AvatarImagePrimitive", + "isPrimitive": true + }, + { + "name": "FallbackPrimitive", + "fullName": "AvatarFallbackPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/breadcrumb.json b/apps/website/public/components/anatomy/breadcrumb.json new file mode 100644 index 000000000..d99e8edcf --- /dev/null +++ b/apps/website/public/components/anatomy/breadcrumb.json @@ -0,0 +1,51 @@ +{ + "componentName": "Breadcrumb", + "displayNamePrefix": "Breadcrumb", + "parts": [ + { + "name": "Root", + "fullName": "BreadcrumbRoot", + "isPrimitive": false + }, + { + "name": "Item", + "fullName": "BreadcrumbItem", + "isPrimitive": false + }, + { + "name": "Separator", + "fullName": "BreadcrumbSeparator", + "isPrimitive": false + }, + { + "name": "Ellipsis", + "fullName": "BreadcrumbEllipsis", + "isPrimitive": false + }, + { + "name": "RootPrimitive", + "fullName": "BreadcrumbRootPrimitive", + "isPrimitive": true + }, + { + "name": "ListPrimitive", + "fullName": "BreadcrumbListPrimitive", + "isPrimitive": true + }, + { + "name": "ItemPrimitive", + "fullName": "BreadcrumbItemPrimitive", + "isPrimitive": true + }, + { + "name": "LinkPrimitive", + "fullName": "BreadcrumbLinkPrimitive", + "isPrimitive": true + }, + { + "name": "EllipsisPrimitive", + "fullName": "BreadcrumbEllipsisPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/callout.json b/apps/website/public/components/anatomy/callout.json new file mode 100644 index 000000000..98e34b2e0 --- /dev/null +++ b/apps/website/public/components/anatomy/callout.json @@ -0,0 +1,16 @@ +{ + "componentName": "Callout", + "displayNamePrefix": "Callout", + "parts": [ + { + "name": "Root", + "fullName": "CalloutRoot", + "isPrimitive": false + }, + { + "name": "Icon", + "fullName": "CalloutIcon", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/card.json b/apps/website/public/components/anatomy/card.json new file mode 100644 index 000000000..79801f572 --- /dev/null +++ b/apps/website/public/components/anatomy/card.json @@ -0,0 +1,26 @@ +{ + "componentName": "Card", + "displayNamePrefix": "Card", + "parts": [ + { + "name": "Root", + "fullName": "CardRoot", + "isPrimitive": false + }, + { + "name": "Header", + "fullName": "CardHeader", + "isPrimitive": false + }, + { + "name": "Body", + "fullName": "CardBody", + "isPrimitive": false + }, + { + "name": "Footer", + "fullName": "CardFooter", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/checkbox.json b/apps/website/public/components/anatomy/checkbox.json new file mode 100644 index 000000000..295ccc03b --- /dev/null +++ b/apps/website/public/components/anatomy/checkbox.json @@ -0,0 +1,16 @@ +{ + "componentName": "Checkbox", + "displayNamePrefix": "Checkbox", + "parts": [ + { + "name": "Root", + "fullName": "CheckboxRoot", + "isPrimitive": false + }, + { + "name": "IndicatorPrimitive", + "fullName": "CheckboxIndicatorPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/collapsible.json b/apps/website/public/components/anatomy/collapsible.json new file mode 100644 index 000000000..eae4e0ce4 --- /dev/null +++ b/apps/website/public/components/anatomy/collapsible.json @@ -0,0 +1,21 @@ +{ + "componentName": "Collapsible", + "displayNamePrefix": "Collapsible", + "parts": [ + { + "name": "Root", + "fullName": "CollapsibleRoot", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "CollapsibleTrigger", + "isPrimitive": false + }, + { + "name": "Panel", + "fullName": "CollapsiblePanel", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/dialog.json b/apps/website/public/components/anatomy/dialog.json new file mode 100644 index 000000000..88684538f --- /dev/null +++ b/apps/website/public/components/anatomy/dialog.json @@ -0,0 +1,66 @@ +{ + "componentName": "Dialog", + "displayNamePrefix": "Dialog", + "parts": [ + { + "name": "Root", + "fullName": "DialogRoot", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "DialogTrigger", + "isPrimitive": false + }, + { + "name": "PortalPrimitive", + "fullName": "DialogPortalPrimitive", + "isPrimitive": true + }, + { + "name": "OverlayPrimitive", + "fullName": "DialogOverlayPrimitive", + "isPrimitive": true + }, + { + "name": "PopupPrimitive", + "fullName": "DialogPopupPrimitive", + "isPrimitive": true + }, + { + "name": "Popup", + "fullName": "DialogPopup", + "isPrimitive": false + }, + { + "name": "Title", + "fullName": "DialogTitle", + "isPrimitive": false + }, + { + "name": "Description", + "fullName": "DialogDescription", + "isPrimitive": false + }, + { + "name": "Close", + "fullName": "DialogClose", + "isPrimitive": false + }, + { + "name": "Header", + "fullName": "DialogHeader", + "isPrimitive": false + }, + { + "name": "Body", + "fullName": "DialogBody", + "isPrimitive": false + }, + { + "name": "Footer", + "fullName": "DialogFooter", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/field.json b/apps/website/public/components/anatomy/field.json new file mode 100644 index 000000000..f2ea1c6a1 --- /dev/null +++ b/apps/website/public/components/anatomy/field.json @@ -0,0 +1,36 @@ +{ + "componentName": "Field", + "displayNamePrefix": "Field", + "parts": [ + { + "name": "Root", + "fullName": "FieldRoot", + "isPrimitive": false + }, + { + "name": "Label", + "fullName": "FieldLabel", + "isPrimitive": false + }, + { + "name": "Description", + "fullName": "FieldDescription", + "isPrimitive": false + }, + { + "name": "Error", + "fullName": "FieldError", + "isPrimitive": false + }, + { + "name": "Success", + "fullName": "FieldSuccess", + "isPrimitive": false + }, + { + "name": "Item", + "fullName": "FieldItem", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/floating-bar.json b/apps/website/public/components/anatomy/floating-bar.json new file mode 100644 index 000000000..b3de990d0 --- /dev/null +++ b/apps/website/public/components/anatomy/floating-bar.json @@ -0,0 +1,41 @@ +{ + "componentName": "FloatingBar", + "displayNamePrefix": "FloatingBar", + "parts": [ + { + "name": "Root", + "fullName": "FloatingBarRoot", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "FloatingBarTrigger", + "isPrimitive": false + }, + { + "name": "Popup", + "fullName": "FloatingBarPopup", + "isPrimitive": false + }, + { + "name": "Close", + "fullName": "FloatingBarClose", + "isPrimitive": false + }, + { + "name": "PortalPrimitive", + "fullName": "FloatingBarPortalPrimitive", + "isPrimitive": true + }, + { + "name": "PositionerPrimitive", + "fullName": "FloatingBarPositionerPrimitive", + "isPrimitive": true + }, + { + "name": "PopupPrimitive", + "fullName": "FloatingBarPopupPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/grid.json b/apps/website/public/components/anatomy/grid.json new file mode 100644 index 000000000..dbbfe8aa3 --- /dev/null +++ b/apps/website/public/components/anatomy/grid.json @@ -0,0 +1,16 @@ +{ + "componentName": "Grid", + "displayNamePrefix": "Grid", + "parts": [ + { + "name": "Root", + "fullName": "GridRoot", + "isPrimitive": false + }, + { + "name": "Item", + "fullName": "GridItem", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/input-group.json b/apps/website/public/components/anatomy/input-group.json new file mode 100644 index 000000000..a6a0ccf2f --- /dev/null +++ b/apps/website/public/components/anatomy/input-group.json @@ -0,0 +1,16 @@ +{ + "componentName": "InputGroup", + "displayNamePrefix": "InputGroup", + "parts": [ + { + "name": "Root", + "fullName": "InputGroupRoot", + "isPrimitive": false + }, + { + "name": "Counter", + "fullName": "InputGroupCounter", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/menu.json b/apps/website/public/components/anatomy/menu.json new file mode 100644 index 000000000..4c235a4c5 --- /dev/null +++ b/apps/website/public/components/anatomy/menu.json @@ -0,0 +1,111 @@ +{ + "componentName": "Menu", + "displayNamePrefix": "Menu", + "parts": [ + { + "name": "Root", + "fullName": "MenuRoot", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "MenuTrigger", + "isPrimitive": false + }, + { + "name": "PortalPrimitive", + "fullName": "MenuPortalPrimitive", + "isPrimitive": true + }, + { + "name": "PositionerPrimitive", + "fullName": "MenuPositionerPrimitive", + "isPrimitive": true + }, + { + "name": "PopupPrimitive", + "fullName": "MenuPopupPrimitive", + "isPrimitive": true + }, + { + "name": "Popup", + "fullName": "MenuPopup", + "isPrimitive": false + }, + { + "name": "Group", + "fullName": "MenuGroup", + "isPrimitive": false + }, + { + "name": "GroupLabel", + "fullName": "MenuGroupLabel", + "isPrimitive": false + }, + { + "name": "Item", + "fullName": "MenuItem", + "isPrimitive": false + }, + { + "name": "Separator", + "fullName": "MenuSeparator", + "isPrimitive": false + }, + { + "name": "SubmenuRoot", + "fullName": "MenuSubmenuRoot", + "isPrimitive": false + }, + { + "name": "SubmenuTriggerItem", + "fullName": "MenuSubmenuTriggerItem", + "isPrimitive": false + }, + { + "name": "SubmenuPopupPrimitive", + "fullName": "MenuSubmenuPopupPrimitive", + "isPrimitive": true + }, + { + "name": "SubmenuPopup", + "fullName": "MenuSubmenuPopup", + "isPrimitive": false + }, + { + "name": "CheckboxItem", + "fullName": "MenuCheckboxItem", + "isPrimitive": false + }, + { + "name": "CheckboxItemPrimitive", + "fullName": "MenuCheckboxItemPrimitive", + "isPrimitive": true + }, + { + "name": "CheckboxItemIndicatorPrimitive", + "fullName": "MenuCheckboxItemIndicatorPrimitive", + "isPrimitive": true + }, + { + "name": "RadioGroup", + "fullName": "MenuRadioGroup", + "isPrimitive": false + }, + { + "name": "RadioItem", + "fullName": "MenuRadioItem", + "isPrimitive": false + }, + { + "name": "RadioItemPrimitive", + "fullName": "MenuRadioItemPrimitive", + "isPrimitive": true + }, + { + "name": "RadioItemIndicatorPrimitive", + "fullName": "MenuRadioItemIndicatorPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/multi-select.json b/apps/website/public/components/anatomy/multi-select.json new file mode 100644 index 000000000..a4ee1f3bc --- /dev/null +++ b/apps/website/public/components/anatomy/multi-select.json @@ -0,0 +1,86 @@ +{ + "componentName": "MultiSelect", + "displayNamePrefix": "MultiSelect", + "parts": [ + { + "name": "Root", + "fullName": "MultiSelectRoot", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "MultiSelectTrigger", + "isPrimitive": false + }, + { + "name": "Popup", + "fullName": "MultiSelectPopup", + "isPrimitive": false + }, + { + "name": "Group", + "fullName": "MultiSelectGroup", + "isPrimitive": false + }, + { + "name": "GroupLabel", + "fullName": "MultiSelectGroupLabel", + "isPrimitive": false + }, + { + "name": "Item", + "fullName": "MultiSelectItem", + "isPrimitive": false + }, + { + "name": "Separator", + "fullName": "MultiSelectSeparator", + "isPrimitive": false + }, + { + "name": "ValuePrimitive", + "fullName": "MultiSelectValuePrimitive", + "isPrimitive": true + }, + { + "name": "PlaceholderPrimitive", + "fullName": "MultiSelectPlaceholderPrimitive", + "isPrimitive": true + }, + { + "name": "TriggerPrimitive", + "fullName": "MultiSelectTriggerPrimitive", + "isPrimitive": true + }, + { + "name": "TriggerIconPrimitive", + "fullName": "MultiSelectTriggerIconPrimitive", + "isPrimitive": true + }, + { + "name": "PopupPrimitive", + "fullName": "MultiSelectPopupPrimitive", + "isPrimitive": true + }, + { + "name": "PortalPrimitive", + "fullName": "MultiSelectPortalPrimitive", + "isPrimitive": true + }, + { + "name": "PositionerPrimitive", + "fullName": "MultiSelectPositionerPrimitive", + "isPrimitive": true + }, + { + "name": "ItemPrimitive", + "fullName": "MultiSelectItemPrimitive", + "isPrimitive": true + }, + { + "name": "ItemIndicatorPrimitive", + "fullName": "MultiSelectItemIndicatorPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/navigation-menu.json b/apps/website/public/components/anatomy/navigation-menu.json new file mode 100644 index 000000000..9964f9e81 --- /dev/null +++ b/apps/website/public/components/anatomy/navigation-menu.json @@ -0,0 +1,71 @@ +{ + "componentName": "NavigationMenu", + "displayNamePrefix": "NavigationMenu", + "parts": [ + { + "name": "Root", + "fullName": "NavigationMenuRoot", + "isPrimitive": false + }, + { + "name": "List", + "fullName": "NavigationMenuList", + "isPrimitive": false + }, + { + "name": "Item", + "fullName": "NavigationMenuItem", + "isPrimitive": false + }, + { + "name": "Link", + "fullName": "NavigationMenuLink", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "NavigationMenuTrigger", + "isPrimitive": false + }, + { + "name": "Viewport", + "fullName": "NavigationMenuViewport", + "isPrimitive": false + }, + { + "name": "Content", + "fullName": "NavigationMenuContent", + "isPrimitive": false + }, + { + "name": "TriggerPrimitive", + "fullName": "NavigationMenuTriggerPrimitive", + "isPrimitive": true + }, + { + "name": "TriggerIndicatorPrimitive", + "fullName": "NavigationMenuTriggerIndicatorPrimitive", + "isPrimitive": true + }, + { + "name": "PortalPrimitive", + "fullName": "NavigationMenuPortalPrimitive", + "isPrimitive": true + }, + { + "name": "PositionerPrimitive", + "fullName": "NavigationMenuPositionerPrimitive", + "isPrimitive": true + }, + { + "name": "PopupPrimitive", + "fullName": "NavigationMenuPopupPrimitive", + "isPrimitive": true + }, + { + "name": "ViewportPrimitive", + "fullName": "NavigationMenuViewportPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/pagination.json b/apps/website/public/components/anatomy/pagination.json new file mode 100644 index 000000000..24005884a --- /dev/null +++ b/apps/website/public/components/anatomy/pagination.json @@ -0,0 +1,61 @@ +{ + "componentName": "Pagination", + "displayNamePrefix": "Pagination", + "parts": [ + { + "name": "Root", + "fullName": "PaginationRoot", + "isPrimitive": false + }, + { + "name": "Items", + "fullName": "PaginationItems", + "isPrimitive": false + }, + { + "name": "Previous", + "fullName": "PaginationPrevious", + "isPrimitive": false + }, + { + "name": "Next", + "fullName": "PaginationNext", + "isPrimitive": false + }, + { + "name": "RootPrimitive", + "fullName": "PaginationRootPrimitive", + "isPrimitive": true + }, + { + "name": "ListPrimitive", + "fullName": "PaginationListPrimitive", + "isPrimitive": true + }, + { + "name": "ItemPrimitive", + "fullName": "PaginationItemPrimitive", + "isPrimitive": true + }, + { + "name": "ButtonPrimitive", + "fullName": "PaginationButtonPrimitive", + "isPrimitive": true + }, + { + "name": "PreviousPrimitive", + "fullName": "PaginationPreviousPrimitive", + "isPrimitive": true + }, + { + "name": "NextPrimitive", + "fullName": "PaginationNextPrimitive", + "isPrimitive": true + }, + { + "name": "EllipsisPrimitive", + "fullName": "PaginationEllipsisPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/popover.json b/apps/website/public/components/anatomy/popover.json new file mode 100644 index 000000000..652fe84de --- /dev/null +++ b/apps/website/public/components/anatomy/popover.json @@ -0,0 +1,51 @@ +{ + "componentName": "Popover", + "displayNamePrefix": "Popover", + "parts": [ + { + "name": "Root", + "fullName": "PopoverRoot", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "PopoverTrigger", + "isPrimitive": false + }, + { + "name": "Popup", + "fullName": "PopoverPopup", + "isPrimitive": false + }, + { + "name": "Title", + "fullName": "PopoverTitle", + "isPrimitive": false + }, + { + "name": "Description", + "fullName": "PopoverDescription", + "isPrimitive": false + }, + { + "name": "Close", + "fullName": "PopoverClose", + "isPrimitive": false + }, + { + "name": "PortalPrimitive", + "fullName": "PopoverPortalPrimitive", + "isPrimitive": true + }, + { + "name": "PositionerPrimitive", + "fullName": "PopoverPositionerPrimitive", + "isPrimitive": true + }, + { + "name": "PopupPrimitive", + "fullName": "PopoverPopupPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/radio-group.json b/apps/website/public/components/anatomy/radio-group.json new file mode 100644 index 000000000..4dab94227 --- /dev/null +++ b/apps/website/public/components/anatomy/radio-group.json @@ -0,0 +1,16 @@ +{ + "componentName": "RadioGroup", + "displayNamePrefix": "RadioGroup", + "parts": [ + { + "name": "Root", + "fullName": "RadioGroupRoot", + "isPrimitive": false + }, + { + "name": "Label", + "fullName": "RadioGroupLabel", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/radio.json b/apps/website/public/components/anatomy/radio.json new file mode 100644 index 000000000..b03cd4380 --- /dev/null +++ b/apps/website/public/components/anatomy/radio.json @@ -0,0 +1,16 @@ +{ + "componentName": "Radio", + "displayNamePrefix": "Radio", + "parts": [ + { + "name": "Root", + "fullName": "RadioRoot", + "isPrimitive": false + }, + { + "name": "IndicatorPrimitive", + "fullName": "RadioIndicatorPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/select.json b/apps/website/public/components/anatomy/select.json new file mode 100644 index 000000000..83c5eaafc --- /dev/null +++ b/apps/website/public/components/anatomy/select.json @@ -0,0 +1,86 @@ +{ + "componentName": "Select", + "displayNamePrefix": "Select", + "parts": [ + { + "name": "Root", + "fullName": "SelectRoot", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "SelectTrigger", + "isPrimitive": false + }, + { + "name": "Popup", + "fullName": "SelectPopup", + "isPrimitive": false + }, + { + "name": "Item", + "fullName": "SelectItem", + "isPrimitive": false + }, + { + "name": "Group", + "fullName": "SelectGroup", + "isPrimitive": false + }, + { + "name": "GroupLabel", + "fullName": "SelectGroupLabel", + "isPrimitive": false + }, + { + "name": "Separator", + "fullName": "SelectSeparator", + "isPrimitive": false + }, + { + "name": "ValuePrimitive", + "fullName": "SelectValuePrimitive", + "isPrimitive": true + }, + { + "name": "PlaceholderPrimitive", + "fullName": "SelectPlaceholderPrimitive", + "isPrimitive": true + }, + { + "name": "TriggerPrimitive", + "fullName": "SelectTriggerPrimitive", + "isPrimitive": true + }, + { + "name": "TriggerIconPrimitive", + "fullName": "SelectTriggerIconPrimitive", + "isPrimitive": true + }, + { + "name": "PortalPrimitive", + "fullName": "SelectPortalPrimitive", + "isPrimitive": true + }, + { + "name": "PositionerPrimitive", + "fullName": "SelectPositionerPrimitive", + "isPrimitive": true + }, + { + "name": "PopupPrimitive", + "fullName": "SelectPopupPrimitive", + "isPrimitive": true + }, + { + "name": "ItemPrimitive", + "fullName": "SelectItemPrimitive", + "isPrimitive": true + }, + { + "name": "ItemIndicatorPrimitive", + "fullName": "SelectItemIndicatorPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/sheet.json b/apps/website/public/components/anatomy/sheet.json new file mode 100644 index 000000000..11df648a7 --- /dev/null +++ b/apps/website/public/components/anatomy/sheet.json @@ -0,0 +1,71 @@ +{ + "componentName": "Sheet", + "displayNamePrefix": "Sheet", + "parts": [ + { + "name": "Root", + "fullName": "SheetRoot", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "SheetTrigger", + "isPrimitive": false + }, + { + "name": "Close", + "fullName": "SheetClose", + "isPrimitive": false + }, + { + "name": "Title", + "fullName": "SheetTitle", + "isPrimitive": false + }, + { + "name": "Description", + "fullName": "SheetDescription", + "isPrimitive": false + }, + { + "name": "Popup", + "fullName": "SheetPopup", + "isPrimitive": false + }, + { + "name": "Header", + "fullName": "SheetHeader", + "isPrimitive": false + }, + { + "name": "Body", + "fullName": "SheetBody", + "isPrimitive": false + }, + { + "name": "Footer", + "fullName": "SheetFooter", + "isPrimitive": false + }, + { + "name": "PortalPrimitive", + "fullName": "SheetPortalPrimitive", + "isPrimitive": true + }, + { + "name": "OverlayPrimitive", + "fullName": "SheetOverlayPrimitive", + "isPrimitive": true + }, + { + "name": "PopupPrimitive", + "fullName": "SheetPopupPrimitive", + "isPrimitive": true + }, + { + "name": "PositionerPrimitive", + "fullName": "SheetPositionerPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/switch.json b/apps/website/public/components/anatomy/switch.json new file mode 100644 index 000000000..12a6cb459 --- /dev/null +++ b/apps/website/public/components/anatomy/switch.json @@ -0,0 +1,16 @@ +{ + "componentName": "Switch", + "displayNamePrefix": "Switch", + "parts": [ + { + "name": "Root", + "fullName": "SwitchRoot", + "isPrimitive": false + }, + { + "name": "ThumbPrimitive", + "fullName": "SwitchThumbPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/table.json b/apps/website/public/components/anatomy/table.json new file mode 100644 index 000000000..263a2c0b1 --- /dev/null +++ b/apps/website/public/components/anatomy/table.json @@ -0,0 +1,51 @@ +{ + "componentName": "Table", + "displayNamePrefix": "Table", + "parts": [ + { + "name": "Root", + "fullName": "TableRoot", + "isPrimitive": false + }, + { + "name": "Header", + "fullName": "TableHeader", + "isPrimitive": false + }, + { + "name": "Body", + "fullName": "TableBody", + "isPrimitive": false + }, + { + "name": "Footer", + "fullName": "TableFooter", + "isPrimitive": false + }, + { + "name": "Heading", + "fullName": "TableHeading", + "isPrimitive": false + }, + { + "name": "Row", + "fullName": "TableRow", + "isPrimitive": false + }, + { + "name": "Cell", + "fullName": "TableCell", + "isPrimitive": false + }, + { + "name": "ColumnGroup", + "fullName": "TableColumnGroup", + "isPrimitive": false + }, + { + "name": "Column", + "fullName": "TableColumn", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/tabs.json b/apps/website/public/components/anatomy/tabs.json new file mode 100644 index 000000000..af936f0b4 --- /dev/null +++ b/apps/website/public/components/anatomy/tabs.json @@ -0,0 +1,36 @@ +{ + "componentName": "Tabs", + "displayNamePrefix": "Tabs", + "parts": [ + { + "name": "Root", + "fullName": "TabsRoot", + "isPrimitive": false + }, + { + "name": "ListPrimitive", + "fullName": "TabsListPrimitive", + "isPrimitive": true + }, + { + "name": "List", + "fullName": "TabsList", + "isPrimitive": false + }, + { + "name": "Button", + "fullName": "TabsButton", + "isPrimitive": false + }, + { + "name": "IndicatorPrimitive", + "fullName": "TabsIndicatorPrimitive", + "isPrimitive": true + }, + { + "name": "Panel", + "fullName": "TabsPanel", + "isPrimitive": false + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/toast.json b/apps/website/public/components/anatomy/toast.json new file mode 100644 index 000000000..2b036a7b8 --- /dev/null +++ b/apps/website/public/components/anatomy/toast.json @@ -0,0 +1,61 @@ +{ + "componentName": "Toast", + "displayNamePrefix": "Toast", + "parts": [ + { + "name": "Provider", + "fullName": "ToastProvider", + "isPrimitive": false + }, + { + "name": "ProviderPrimitive", + "fullName": "ToastProviderPrimitive", + "isPrimitive": true + }, + { + "name": "PortalPrimitive", + "fullName": "ToastPortalPrimitive", + "isPrimitive": true + }, + { + "name": "ViewportPrimitive", + "fullName": "ToastViewportPrimitive", + "isPrimitive": true + }, + { + "name": "RootPrimitive", + "fullName": "ToastRootPrimitive", + "isPrimitive": true + }, + { + "name": "ContentPrimitive", + "fullName": "ToastContentPrimitive", + "isPrimitive": true + }, + { + "name": "TitlePrimitive", + "fullName": "ToastTitlePrimitive", + "isPrimitive": true + }, + { + "name": "DescriptionPrimitive", + "fullName": "ToastDescriptionPrimitive", + "isPrimitive": true + }, + { + "name": "ActionPrimitive", + "fullName": "ToastActionPrimitive", + "isPrimitive": true + }, + { + "name": "ClosePrimitive", + "fullName": "ToastClosePrimitive", + "isPrimitive": true + }, + { + "name": "IconPrimitive", + "fullName": "ToastIconPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/public/components/anatomy/tooltip.json b/apps/website/public/components/anatomy/tooltip.json new file mode 100644 index 000000000..63aa01f9c --- /dev/null +++ b/apps/website/public/components/anatomy/tooltip.json @@ -0,0 +1,36 @@ +{ + "componentName": "Tooltip", + "displayNamePrefix": "Tooltip", + "parts": [ + { + "name": "Root", + "fullName": "TooltipRoot", + "isPrimitive": false + }, + { + "name": "Trigger", + "fullName": "TooltipTrigger", + "isPrimitive": false + }, + { + "name": "Popup", + "fullName": "TooltipPopup", + "isPrimitive": false + }, + { + "name": "PortalPrimitive", + "fullName": "TooltipPortalPrimitive", + "isPrimitive": true + }, + { + "name": "PositionerPrimitive", + "fullName": "TooltipPositionerPrimitive", + "isPrimitive": true + }, + { + "name": "PopupPrimitive", + "fullName": "TooltipPopupPrimitive", + "isPrimitive": true + } + ] +} \ No newline at end of file diff --git a/apps/website/scripts/generate-anatomy.mjs b/apps/website/scripts/generate-anatomy.mjs new file mode 100644 index 000000000..d3cdc20c7 --- /dev/null +++ b/apps/website/scripts/generate-anatomy.mjs @@ -0,0 +1,89 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const CORE_COMPONENTS_PATH = path.resolve(__dirname, '../../../packages/core/src/components'); +const OUTPUT_PATH = path.resolve(__dirname, '../public/components/anatomy'); + +/** + * @param {string} filePath + * @returns {{ name: string, fullName: string, isPrimitive: boolean }[]} + */ +function parsePartsFile(filePath) { + const content = fs.readFileSync(filePath, 'utf-8'); + const parts = []; + + // Match export statements like: ComponentNamePartName as ShortName + const exportRegex = /(\w+)\s+as\s+(\w+)/g; + let match; + + while ((match = exportRegex.exec(content)) !== null) { + const [, internalName, exportedName] = match; + const isPrimitive = exportedName.endsWith('Primitive'); + + parts.push({ + name: exportedName, + fullName: internalName, + isPrimitive, + }); + } + + return parts; +} + +/** + * @param {string} dirPath + * @returns {string} + */ +function getComponentNameFromPath(dirPath) { + const dirName = path.basename(dirPath); + // Convert kebab-case to PascalCase + return dirName + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); +} + +function generateAnatomy() { + // Ensure output directory exists + if (!fs.existsSync(OUTPUT_PATH)) { + fs.mkdirSync(OUTPUT_PATH, { recursive: true }); + } + + // Find all index.parts.ts files + const componentDirs = fs.readdirSync(CORE_COMPONENTS_PATH, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + + let generatedCount = 0; + + for (const dirName of componentDirs) { + const partsFilePath = path.join(CORE_COMPONENTS_PATH, dirName, 'index.parts.ts'); + + if (!fs.existsSync(partsFilePath)) { + continue; + } + + const componentName = getComponentNameFromPath(dirName); + const parts = parsePartsFile(partsFilePath); + + const anatomyData = { + componentName, + displayNamePrefix: componentName, + parts, + }; + + const outputFilePath = path.join(OUTPUT_PATH, `${dirName}.json`); + fs.writeFileSync(outputFilePath, JSON.stringify(anatomyData, null, 2)); + + console.log(`Generated: ${dirName}.json (${parts.length} parts)`); + generatedCount++; + } + + console.log(`\nGenerated ${generatedCount} anatomy files.`); +} + +generateAnatomy(); diff --git a/apps/website/src/app/preview/[component]/page.tsx b/apps/website/src/app/preview/[component]/page.tsx index 61722a430..648fb37a7 100644 --- a/apps/website/src/app/preview/[component]/page.tsx +++ b/apps/website/src/app/preview/[component]/page.tsx @@ -8,6 +8,7 @@ interface PreviewPageProps { searchParams: Promise<{ path?: string; theme?: string; + explorer?: string; }>; } @@ -49,10 +50,11 @@ export default async function Page({ searchParams }: PreviewPageProps) { const resolvedSearchParams = await searchParams; const componentPath = resolvedSearchParams.path; const theme = resolvedSearchParams.theme || 'light'; + const explorer = resolvedSearchParams.explorer === 'true'; if (!isValidComponentPath(componentPath)) { return ; } - return ; + return ; } diff --git a/apps/website/src/app/preview/[component]/preview-wrapper.tsx b/apps/website/src/app/preview/[component]/preview-wrapper.tsx index 2f0a0a233..a86bf6ef2 100644 --- a/apps/website/src/app/preview/[component]/preview-wrapper.tsx +++ b/apps/website/src/app/preview/[component]/preview-wrapper.tsx @@ -2,15 +2,18 @@ import * as React from 'react'; +import { HighlightOverlay } from '~/components/component-explorer/iframe/highlight-overlay'; + import { DynamicComponent } from './dynamic-component'; interface PreviewWrapperProps { theme: string; componentPath?: string; + explorer?: boolean; children?: React.ReactNode; } -export function PreviewWrapper({ theme, componentPath, children }: PreviewWrapperProps) { +export function PreviewWrapper({ theme, componentPath, explorer = false, children }: PreviewWrapperProps) { // Sync theme changes (e.g., when user switches theme while iframe is open) React.useEffect(() => { const root = document.documentElement; @@ -24,7 +27,14 @@ export function PreviewWrapper({ theme, componentPath, children }: PreviewWrappe }, [theme]); if (componentPath) { - return ; + return ( + <> +
+ +
+ {explorer && } + + ); } return <>{children}; diff --git a/apps/website/src/app/preview/layout.tsx b/apps/website/src/app/preview/layout.tsx index 7096950e8..b3d41411b 100644 --- a/apps/website/src/app/preview/layout.tsx +++ b/apps/website/src/app/preview/layout.tsx @@ -4,7 +4,7 @@ import type { ReactNode } from 'react'; export default function PreviewLayout({ children }: { children: ReactNode }) { return ( - +