@pihanga2/shadcn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/cards/badge/badge.component.d.ts +14 -0
- package/cards/badge/badge.component.js +18 -0
- package/cards/badge/badge.component.js.map +1 -0
- package/cards/badge/badge.types.d.ts +37 -0
- package/cards/badge/badge.types.js +7 -0
- package/cards/badge/badge.types.js.map +1 -0
- package/cards/badge/index.d.ts +1 -0
- package/cards/badge/index.js +12 -0
- package/cards/badge/index.js.map +1 -0
- package/cards/box/box.component.d.ts +4 -0
- package/cards/box/box.component.js +24 -0
- package/cards/box/box.component.js.map +1 -0
- package/cards/box/box.types.d.ts +71 -0
- package/cards/box/box.types.js +7 -0
- package/cards/box/box.types.js.map +1 -0
- package/cards/box/index.d.ts +1 -0
- package/cards/box/index.js +12 -0
- package/cards/box/index.js.map +1 -0
- package/cards/button/button.component.d.ts +4 -0
- package/cards/button/button.component.js +71 -0
- package/cards/button/button.component.js.map +1 -0
- package/cards/button/button.types.d.ts +106 -0
- package/cards/button/button.types.js +7 -0
- package/cards/button/button.types.js.map +1 -0
- package/cards/button/index.d.ts +1 -0
- package/cards/button/index.js +13 -0
- package/cards/button/index.js.map +1 -0
- package/cards/checkbox/checkbox.component.d.ts +4 -0
- package/cards/checkbox/checkbox.component.js +36 -0
- package/cards/checkbox/checkbox.component.js.map +1 -0
- package/cards/checkbox/checkbox.types.d.ts +37 -0
- package/cards/checkbox/checkbox.types.js +7 -0
- package/cards/checkbox/checkbox.types.js.map +1 -0
- package/cards/checkbox/index.d.ts +1 -0
- package/cards/checkbox/index.js +13 -0
- package/cards/checkbox/index.js.map +1 -0
- package/cards/conditional/conditional.component.d.ts +11 -0
- package/cards/conditional/conditional.component.js +15 -0
- package/cards/conditional/conditional.component.js.map +1 -0
- package/cards/conditional/conditional.types.d.ts +29 -0
- package/cards/conditional/conditional.types.js +7 -0
- package/cards/conditional/conditional.types.js.map +1 -0
- package/cards/conditional/index.d.ts +1 -0
- package/cards/conditional/index.js +12 -0
- package/cards/conditional/index.js.map +1 -0
- package/cards/core-index.d.ts +30 -0
- package/cards/core-index.js +61 -0
- package/cards/dataTable/dataTable.component.d.ts +4 -0
- package/cards/dataTable/dataTable.component.js +209 -0
- package/cards/dataTable/dataTable.component.js.map +1 -0
- package/cards/dataTable/dataTable.types.d.ts +196 -0
- package/cards/dataTable/dataTable.types.js +13 -0
- package/cards/dataTable/dataTable.types.js.map +1 -0
- package/cards/dataTable/dataTableRowDetail.component.d.ts +26 -0
- package/cards/dataTable/dataTableRowDetail.component.js +47 -0
- package/cards/dataTable/dataTableRowDetail.component.js.map +1 -0
- package/cards/dataTable/index.d.ts +1 -0
- package/cards/dataTable/index.js +16 -0
- package/cards/dataTable/index.js.map +1 -0
- package/cards/dialog/dialog.component.d.ts +4 -0
- package/cards/dialog/dialog.component.js +89 -0
- package/cards/dialog/dialog.component.js.map +1 -0
- package/cards/dialog/dialog.types.d.ts +115 -0
- package/cards/dialog/dialog.types.js +11 -0
- package/cards/dialog/dialog.types.js.map +1 -0
- package/cards/dialog/index.d.ts +1 -0
- package/cards/dialog/index.js +13 -0
- package/cards/dialog/index.js.map +1 -0
- package/cards/dropDownMenu/drop-down.component.d.ts +4 -0
- package/cards/dropDownMenu/drop-down.component.js +205 -0
- package/cards/dropDownMenu/drop-down.component.js.map +1 -0
- package/cards/dropDownMenu/drop-down.types.d.ts +131 -0
- package/cards/dropDownMenu/drop-down.types.js +7 -0
- package/cards/dropDownMenu/drop-down.types.js.map +1 -0
- package/cards/dropDownMenu/dropdown-context.d.ts +7 -0
- package/cards/dropDownMenu/dropdown-context.js +7 -0
- package/cards/dropDownMenu/dropdown-context.js.map +1 -0
- package/cards/dropDownMenu/dropdown-menu.ui.d.ts +42 -0
- package/cards/dropDownMenu/dropdown-menu.ui.js +111 -0
- package/cards/dropDownMenu/dropdown-menu.ui.js.map +1 -0
- package/cards/dropDownMenu/index.d.ts +1 -0
- package/cards/dropDownMenu/index.js +13 -0
- package/cards/dropDownMenu/index.js.map +1 -0
- package/cards/emptyCard.d.ts +7 -0
- package/cards/field/field.component.d.ts +4 -0
- package/cards/field/field.component.js +38 -0
- package/cards/field/field.component.js.map +1 -0
- package/cards/field/field.types.d.ts +30 -0
- package/cards/field/field.types.js +7 -0
- package/cards/field/field.types.js.map +1 -0
- package/cards/field/index.d.ts +1 -0
- package/cards/field/index.js +12 -0
- package/cards/field/index.js.map +1 -0
- package/cards/flexGrid/flexGrid.component.d.ts +4 -0
- package/cards/flexGrid/flexGrid.component.js +40 -0
- package/cards/flexGrid/flexGrid.component.js.map +1 -0
- package/cards/flexGrid/flexGrid.types.d.ts +23 -0
- package/cards/flexGrid/flexGrid.types.js +7 -0
- package/cards/flexGrid/flexGrid.types.js.map +1 -0
- package/cards/flexGrid/index.d.ts +1 -0
- package/cards/flexGrid/index.js +12 -0
- package/cards/flexGrid/index.js.map +1 -0
- package/cards/form/form.component.d.ts +4 -0
- package/cards/form/form.component.js +64 -0
- package/cards/form/form.component.js.map +1 -0
- package/cards/form/form.context.d.ts +23 -0
- package/cards/form/form.context.js +13 -0
- package/cards/form/form.context.js.map +1 -0
- package/cards/form/form.types.d.ts +43 -0
- package/cards/form/form.types.js +7 -0
- package/cards/form/form.types.js.map +1 -0
- package/cards/form/index.d.ts +2 -0
- package/cards/form/index.js +14 -0
- package/cards/form/index.js.map +1 -0
- package/cards/framework/framework.component.d.ts +3 -0
- package/cards/framework/framework.component.js +21 -0
- package/cards/framework/framework.component.js.map +1 -0
- package/cards/framework/index.d.ts +2 -0
- package/cards/framework/index.js +12 -0
- package/cards/framework/index.js.map +1 -0
- package/cards/graphin/graphin.component.d.ts +4 -0
- package/cards/graphin/graphin.types.d.ts +18 -0
- package/cards/graphin/index.d.ts +2 -0
- package/cards/graphin/tooltip.component.d.ts +10 -0
- package/cards/icons.d.ts +15 -0
- package/cards/icons.js +15 -0
- package/cards/icons.js.map +1 -0
- package/cards/input/index.d.ts +1 -0
- package/cards/input/index.js +13 -0
- package/cards/input/index.js.map +1 -0
- package/cards/input/input.component.d.ts +4 -0
- package/cards/input/input.component.js +69 -0
- package/cards/input/input.component.js.map +1 -0
- package/cards/input/input.types.d.ts +80 -0
- package/cards/input/input.types.js +7 -0
- package/cards/input/input.types.js.map +1 -0
- package/cards/jsonViewer/index.d.ts +1 -0
- package/cards/jsonViewer/jsonViewer.component.d.ts +4 -0
- package/cards/jsonViewer/jsonViewer.types.d.ts +76 -0
- package/cards/list/index.d.ts +1 -0
- package/cards/list/index.js +13 -0
- package/cards/list/index.js.map +1 -0
- package/cards/list/list.component.d.ts +4 -0
- package/cards/list/list.component.js +102 -0
- package/cards/list/list.component.js.map +1 -0
- package/cards/list/list.types.d.ts +65 -0
- package/cards/list/list.types.js +7 -0
- package/cards/list/list.types.js.map +1 -0
- package/cards/loadingOverlay/index.d.ts +1 -0
- package/cards/loadingOverlay/index.js +12 -0
- package/cards/loadingOverlay/index.js.map +1 -0
- package/cards/loadingOverlay/loadingOverlay.component.d.ts +4 -0
- package/cards/loadingOverlay/loadingOverlay.component.js +31 -0
- package/cards/loadingOverlay/loadingOverlay.component.js.map +1 -0
- package/cards/loadingOverlay/loadingOverlay.types.d.ts +13 -0
- package/cards/loadingOverlay/loadingOverlay.types.js +7 -0
- package/cards/loadingOverlay/loadingOverlay.types.js.map +1 -0
- package/cards/loadingSkeleton/index.d.ts +1 -0
- package/cards/loadingSkeleton/index.js +12 -0
- package/cards/loadingSkeleton/index.js.map +1 -0
- package/cards/loadingSkeleton/loading-skeleton.component.d.ts +19 -0
- package/cards/loadingSkeleton/loading-skeleton.component.js +33 -0
- package/cards/loadingSkeleton/loading-skeleton.component.js.map +1 -0
- package/cards/loadingSkeleton/loading-skeleton.types.d.ts +94 -0
- package/cards/loadingSkeleton/loading-skeleton.types.js +7 -0
- package/cards/loadingSkeleton/loading-skeleton.types.js.map +1 -0
- package/cards/markdownViewer/index.d.ts +1 -0
- package/cards/markdownViewer/markdownViewer.component.d.ts +4 -0
- package/cards/markdownViewer/markdownViewer.types.d.ts +28 -0
- package/cards/menu/index.d.ts +1 -0
- package/cards/menu/index.js +13 -0
- package/cards/menu/index.js.map +1 -0
- package/cards/menu/menu.component.d.ts +4 -0
- package/cards/menu/menu.component.js +23 -0
- package/cards/menu/menu.component.js.map +1 -0
- package/cards/menu/menu.types.d.ts +53 -0
- package/cards/menu/menu.types.js +7 -0
- package/cards/menu/menu.types.js.map +1 -0
- package/cards/modeToggle/index.d.ts +1 -0
- package/cards/modeToggle/index.js +13 -0
- package/cards/modeToggle/index.js.map +1 -0
- package/cards/modeToggle/mode-toggle.component.d.ts +4 -0
- package/cards/modeToggle/mode-toggle.component.js +32 -0
- package/cards/modeToggle/mode-toggle.component.js.map +1 -0
- package/cards/modeToggle/mode-toggle.types.d.ts +20 -0
- package/cards/modeToggle/mode-toggle.types.js +7 -0
- package/cards/modeToggle/mode-toggle.types.js.map +1 -0
- package/cards/navbarSearch/index.d.ts +1 -0
- package/cards/navbarSearch/index.js +13 -0
- package/cards/navbarSearch/index.js.map +1 -0
- package/cards/navbarSearch/navbarSearch.component.d.ts +3 -0
- package/cards/navbarSearch/navbarSearch.component.js +88 -0
- package/cards/navbarSearch/navbarSearch.component.js.map +1 -0
- package/cards/navbarSearch/navbarSearch.type.d.ts +32 -0
- package/cards/navbarSearch/navbarSearch.type.js +11 -0
- package/cards/navbarSearch/navbarSearch.type.js.map +1 -0
- package/cards/pageWithNavbar/index.d.ts +2 -0
- package/cards/pageWithNavbar/index.js +14 -0
- package/cards/pageWithNavbar/index.js.map +1 -0
- package/cards/pageWithNavbar/pageWithNavbar.component.d.ts +12 -0
- package/cards/pageWithNavbar/pageWithNavbar.component.js +153 -0
- package/cards/pageWithNavbar/pageWithNavbar.component.js.map +1 -0
- package/cards/pageWithNavbar/pageWithNavbar.type.d.ts +81 -0
- package/cards/pageWithNavbar/pageWithNavbar.type.js +7 -0
- package/cards/pageWithNavbar/pageWithNavbar.type.js.map +1 -0
- package/cards/pasteTarget/index.d.ts +1 -0
- package/cards/pasteTarget/index.js +13 -0
- package/cards/pasteTarget/index.js.map +1 -0
- package/cards/pasteTarget/pasteTarget.component.d.ts +4 -0
- package/cards/pasteTarget/pasteTarget.component.js +158 -0
- package/cards/pasteTarget/pasteTarget.component.js.map +1 -0
- package/cards/pasteTarget/pasteTarget.types.d.ts +46 -0
- package/cards/pasteTarget/pasteTarget.types.js +11 -0
- package/cards/pasteTarget/pasteTarget.types.js.map +1 -0
- package/cards/resizable/index.d.ts +1 -0
- package/cards/resizable/resizable.component.d.ts +4 -0
- package/cards/resizable/resizable.types.d.ts +49 -0
- package/cards/select/index.d.ts +1 -0
- package/cards/select/index.js +13 -0
- package/cards/select/index.js.map +1 -0
- package/cards/select/select.component.d.ts +4 -0
- package/cards/select/select.component.js +41 -0
- package/cards/select/select.component.js.map +1 -0
- package/cards/select/select.types.d.ts +73 -0
- package/cards/select/select.types.js +11 -0
- package/cards/select/select.types.js.map +1 -0
- package/cards/stack/index.d.ts +1 -0
- package/cards/stack/index.js +12 -0
- package/cards/stack/index.js.map +1 -0
- package/cards/stack/stack.component.d.ts +7 -0
- package/cards/stack/stack.component.js +83 -0
- package/cards/stack/stack.component.js.map +1 -0
- package/cards/stack/stack.types.d.ts +47 -0
- package/cards/stack/stack.types.js +7 -0
- package/cards/stack/stack.types.js.map +1 -0
- package/cards/stepper/index.d.ts +1 -0
- package/cards/stepper/index.js +13 -0
- package/cards/stepper/index.js.map +1 -0
- package/cards/stepper/stepper.component.d.ts +4 -0
- package/cards/stepper/stepper.component.js +48 -0
- package/cards/stepper/stepper.component.js.map +1 -0
- package/cards/stepper/stepper.types.d.ts +96 -0
- package/cards/stepper/stepper.types.js +7 -0
- package/cards/stepper/stepper.types.js.map +1 -0
- package/cards/switch/index.d.ts +1 -0
- package/cards/switch/index.js +13 -0
- package/cards/switch/index.js.map +1 -0
- package/cards/switch/switch.component.d.ts +4 -0
- package/cards/switch/switch.component.js +34 -0
- package/cards/switch/switch.component.js.map +1 -0
- package/cards/switch/switch.types.d.ts +39 -0
- package/cards/switch/switch.types.js +7 -0
- package/cards/switch/switch.types.js.map +1 -0
- package/cards/tabs/index.d.ts +1 -0
- package/cards/tabs/index.js +13 -0
- package/cards/tabs/index.js.map +1 -0
- package/cards/tabs/tabs.component.d.ts +4 -0
- package/cards/tabs/tabs.component.js +42 -0
- package/cards/tabs/tabs.component.js.map +1 -0
- package/cards/tabs/tabs.types.d.ts +72 -0
- package/cards/tabs/tabs.types.js +7 -0
- package/cards/tabs/tabs.types.js.map +1 -0
- package/cards/textField/index.d.ts +1 -0
- package/cards/textField/index.js +13 -0
- package/cards/textField/index.js.map +1 -0
- package/cards/textField/textField.component.d.ts +4 -0
- package/cards/textField/textField.component.js +29 -0
- package/cards/textField/textField.component.js.map +1 -0
- package/cards/textField/textField.types.d.ts +36 -0
- package/cards/textField/textField.types.js +7 -0
- package/cards/textField/textField.types.js.map +1 -0
- package/cards/toast/index.d.ts +1 -0
- package/cards/toast/index.js +13 -0
- package/cards/toast/index.js.map +1 -0
- package/cards/toast/toast.component.d.ts +4 -0
- package/cards/toast/toast.component.js +76 -0
- package/cards/toast/toast.component.js.map +1 -0
- package/cards/toast/toast.types.d.ts +100 -0
- package/cards/toast/toast.types.js +12 -0
- package/cards/toast/toast.types.js.map +1 -0
- package/cards/toggleGroup/index.d.ts +1 -0
- package/cards/toggleGroup/index.js +13 -0
- package/cards/toggleGroup/index.js.map +1 -0
- package/cards/toggleGroup/toggleGroup.component.d.ts +4 -0
- package/cards/toggleGroup/toggleGroup.component.js +55 -0
- package/cards/toggleGroup/toggleGroup.component.js.map +1 -0
- package/cards/toggleGroup/toggleGroup.types.d.ts +96 -0
- package/cards/toggleGroup/toggleGroup.types.js +7 -0
- package/cards/toggleGroup/toggleGroup.types.js.map +1 -0
- package/cards/types.d.ts +72 -0
- package/cards/types.js +22 -0
- package/cards/types.js.map +1 -0
- package/cards/typography/index.d.ts +1 -0
- package/cards/typography/index.js +12 -0
- package/cards/typography/index.js.map +1 -0
- package/cards/typography/typography.component.d.ts +4 -0
- package/cards/typography/typography.component.js +44 -0
- package/cards/typography/typography.component.js.map +1 -0
- package/cards/typography/typography.types.d.ts +23 -0
- package/cards/typography/typography.types.js +7 -0
- package/cards/typography/typography.types.js.map +1 -0
- package/components/hooks/use-is-touch-device.d.ts +1 -0
- package/components/lib/utils.js +10 -0
- package/components/lib/utils.js.map +1 -0
- package/components/theme-provider/index.d.ts +4 -0
- package/components/theme-provider/index.js +12 -0
- package/components/theme-provider/index.js.map +1 -0
- package/components/theme-provider/theme-provider.component.d.ts +2 -0
- package/components/theme-provider/theme-provider.component.js +31 -0
- package/components/theme-provider/theme-provider.component.js.map +1 -0
- package/components/theme-provider/theme-provider.types.d.ts +12 -0
- package/components/theme-provider/theme-provider.types.js +9 -0
- package/components/theme-provider/theme-provider.types.js.map +1 -0
- package/components/ui/avatar.js +30 -0
- package/components/ui/avatar.js.map +1 -0
- package/components/ui/badge.js +26 -0
- package/components/ui/badge.js.map +1 -0
- package/components/ui/button.js +98 -0
- package/components/ui/button.js.map +1 -0
- package/components/ui/dialog.js +109 -0
- package/components/ui/dialog.js.map +1 -0
- package/components/ui/field.js +68 -0
- package/components/ui/field.js.map +1 -0
- package/components/ui/input.js +16 -0
- package/components/ui/input.js.map +1 -0
- package/components/ui/label.js +16 -0
- package/components/ui/label.js.map +1 -0
- package/components/ui/menubar.js +62 -0
- package/components/ui/menubar.js.map +1 -0
- package/components/ui/select.js +84 -0
- package/components/ui/select.js.map +1 -0
- package/components/ui/separator.js +5 -0
- package/components/ui/sheet.js +49 -0
- package/components/ui/sheet.js.map +1 -0
- package/components/ui/spinner.js +35 -0
- package/components/ui/spinner.js.map +1 -0
- package/components/ui/stepper.js +145 -0
- package/components/ui/stepper.js.map +1 -0
- package/components/ui/switch.js +20 -0
- package/components/ui/switch.js.map +1 -0
- package/components/ui/table.js +61 -0
- package/components/ui/table.js.map +1 -0
- package/components/ui/tabs.js +38 -0
- package/components/ui/tabs.js.map +1 -0
- package/components/ui/toggle-group.js +50 -0
- package/components/ui/toggle-group.js.map +1 -0
- package/components/ui/toggle.js +27 -0
- package/components/ui/toggle.js.map +1 -0
- package/components/ui/tooltip.js +38 -0
- package/components/ui/tooltip.js.map +1 -0
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +10 -0
- package/lib/utils.js.map +1 -0
- package/mountain-snow.svg +4 -0
- package/package.json +203 -0
- package/pihanga-shadcn.css +2 -0
- package/r/badge.json +35 -0
- package/r/box.json +26 -0
- package/r/button.json +39 -0
- package/r/checkbox.json +31 -0
- package/r/conditional.json +30 -0
- package/r/dataTable.json +43 -0
- package/r/dialog.json +36 -0
- package/r/dropDownMenu.json +47 -0
- package/r/field.json +35 -0
- package/r/flexGrid.json +32 -0
- package/r/form.json +37 -0
- package/r/framework.json +28 -0
- package/r/graphin.json +42 -0
- package/r/input.json +35 -0
- package/r/jsonViewer.json +40 -0
- package/r/list.json +39 -0
- package/r/loadingOverlay.json +39 -0
- package/r/loadingSkeleton.json +30 -0
- package/r/markdownViewer.json +51 -0
- package/r/menu.json +34 -0
- package/r/modeToggle.json +36 -0
- package/r/navbarSearch.json +31 -0
- package/r/pageWithNavbar.json +46 -0
- package/r/pasteTarget.json +41 -0
- package/r/pihanga-base.json +11 -0
- package/r/pihanga-cards-icons.json +16 -0
- package/r/pihanga-cards-types.json +16 -0
- package/r/pihanga-hook-use-is-touch-device.json +15 -0
- package/r/pihanga-lib-utils.json +18 -0
- package/r/pihanga-theme-provider.json +25 -0
- package/r/pihanga-ui-extras.json +68 -0
- package/r/registry.json +171 -0
- package/r/resizable.json +35 -0
- package/r/select.json +35 -0
- package/r/stack.json +27 -0
- package/r/stepper.json +39 -0
- package/r/switch.json +36 -0
- package/r/tabs.json +33 -0
- package/r/textField.json +32 -0
- package/r/toast.json +32 -0
- package/r/toggleGroup.json +35 -0
- package/r/typography.json +31 -0
package/r/dialog.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "dialog",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga dialog card component",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@radix-ui/react-dialog@^1.1.15",
|
|
8
|
+
"class-variance-authority@^0.7.1",
|
|
9
|
+
"lucide-react@^0.513.0",
|
|
10
|
+
"radix-ui@^1.4.3"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
|
|
14
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-ui-extras.json"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"path": "cards/dialog/dialog.component.tsx",
|
|
19
|
+
"content": "import React from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\n\nimport {\n Dialog as DialogRoot,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogBody,\n DialogFooter,\n} from \"@/components/ui/dialog\";\nimport {Button} from \"@/components/ui/button\";\n\nimport type {PiDialogEvents, PiDialogProps} from \"./dialog.types\";\n\nexport const DialogComponent = (\n props: PiCardProps<PiDialogProps, PiDialogEvents>,\n): React.ReactNode => {\n const {\n id,\n trigger,\n content,\n title,\n description,\n open: controlledOpen,\n size,\n variant,\n desktopVariant,\n mobileVariant,\n dismissible,\n hideClose,\n className,\n footer,\n footerCloseButtonText,\n cardName,\n onOpened,\n onClosed,\n onOpenChanged,\n } = props;\n\n // Apply default for footer close button text\n const effectiveFooterCloseButtonText =\n footerCloseButtonText === undefined ? \"Close\" : footerCloseButtonText;\n\n // Internal state for uncontrolled mode\n const [internalOpen, setInternalOpen] = React.useState(false);\n\n // Use controlled open if provided, otherwise use internal state\n const open = controlledOpen ?? internalOpen;\n\n // Track previous open state to detect programmatic changes\n const prevControlledOpenRef = React.useRef(controlledOpen);\n\n React.useEffect(() => {\n // Detect programmatic close via props.open changing from true to false\n if (\n prevControlledOpenRef.current === true &&\n controlledOpen === false &&\n open === false\n ) {\n // This is a programmatic close\n onClosed({id, reason: \"programmatic\"});\n }\n prevControlledOpenRef.current = controlledOpen;\n }, [controlledOpen, open, id, onClosed]);\n\n const handleOpenChange = (nextOpen: boolean) => {\n // Update internal state if not controlled\n if (controlledOpen === undefined) {\n setInternalOpen(nextOpen);\n }\n\n // Emit events\n onOpenChanged({open: nextOpen, id});\n\n if (nextOpen) {\n onOpened({id});\n } else {\n // When user dismisses (not programmatic), reason is 'user'\n // We only get here from user actions when controlled open is undefined\n // or when the dialog component itself triggers the change\n if (controlledOpen === undefined) {\n onClosed({id, reason: \"user\"});\n }\n }\n };\n\n function renderTrigger() {\n if (!trigger) return null;\n\n // Similar to dropDownMenu: wrap trigger in a DOM element so Radix\n // can attach handlers via asChild even when trigger is a Pihanga card.\n return (\n <span className=\"inline-flex\" data-pihanga-trigger-wrapper>\n <Card cardName={trigger} parentCard={cardName} />\n </span>\n );\n }\n\n function renderHeader() {\n if (!title && !description) return null;\n\n return (\n <DialogHeader>\n {title && <DialogTitle>{title}</DialogTitle>}\n {description && <DialogDescription>{description}</DialogDescription>}\n </DialogHeader>\n );\n }\n\n function renderFooter() {\n // Custom footer card takes precedence\n if (footer) {\n return (\n <DialogFooter className=\"bg-muted/50\">\n <Card cardName={footer} parentCard={cardName} />\n </DialogFooter>\n );\n }\n\n // Default close button (if not explicitly disabled)\n if (effectiveFooterCloseButtonText !== null) {\n return (\n <DialogFooter className=\"bg-muted/50\">\n <Button variant=\"outline\" onClick={() => handleOpenChange(false)}>\n {effectiveFooterCloseButtonText}\n </Button>\n </DialogFooter>\n );\n }\n\n // No footer\n return null;\n }\n\n // Use fixed layout when we have header or footer\n const hasFixedLayout = !!(\n title ||\n description ||\n footer ||\n effectiveFooterCloseButtonText\n );\n\n return (\n <DialogRoot\n open={open}\n onOpenChange={handleOpenChange}\n desktopVariant={desktopVariant}\n mobileVariant={mobileVariant}\n >\n {trigger && <DialogTrigger asChild>{renderTrigger()}</DialogTrigger>}\n <DialogContent\n data-pihanga={cardName}\n size={size}\n variant={variant === \"drawer\" ? undefined : variant}\n dismissible={dismissible}\n hideClose={hideClose}\n fixed={hasFixedLayout}\n className={className}\n >\n {renderHeader()}\n <DialogBody>\n <Card cardName={content} parentCard={cardName} />\n </DialogBody>\n {renderFooter()}\n </DialogContent>\n </DialogRoot>\n );\n};\n",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "src/cards/dialog/dialog.component.tsx"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "cards/dialog/dialog.types.ts",
|
|
25
|
+
"content": "import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_DIALOG_CARD = \"pi/dialog\";\n\nexport const Dialog = createCardDeclaration<PiDialogProps, PiDialogEvents>(\n PI_DIALOG_CARD,\n);\n\nexport const PI_DIALOG_ACTION = registerActions(PI_DIALOG_CARD, [\n \"opened\",\n \"closed\",\n \"open_changed\",\n]);\n\nexport const onDialogOpened = createOnAction<DialogOpenedEvent>(\n PI_DIALOG_ACTION.OPENED,\n);\n\nexport const onDialogClosed = createOnAction<DialogClosedEvent>(\n PI_DIALOG_ACTION.CLOSED,\n);\n\nexport const onDialogOpenChanged = createOnAction<DialogOpenChangedEvent>(\n PI_DIALOG_ACTION.OPEN_CHANGED,\n);\n\nexport type PiDialogProps = {\n /**\n * Optional id passed through to events for identification.\n */\n id?: string;\n\n /**\n * Optional dialog trigger card (e.g., a button).\n *\n * When provided, this card will be used as the dialog trigger.\n * When omitted, the dialog must be controlled via the `open` prop.\n *\n * Wrapped in a DOM element so Radix can attach trigger handlers\n * even when rendered via Pihanga's <Card />.\n */\n trigger?: PiCardRef;\n\n /**\n * Card to render as the dialog body content.\n */\n content: PiCardRef;\n\n /**\n * Optional dialog title (rendered in header).\n */\n title?: string;\n\n /**\n * Optional dialog description (rendered in header below title).\n */\n description?: string;\n\n /**\n * Controlled open state.\n *\n * When provided, dialog visibility is fully managed externally.\n * When undefined, dialog manages its own state.\n */\n open?: boolean;\n\n /**\n * Dialog size variant.\n */\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"4xl\";\n\n /**\n * Dialog variant.\n */\n variant?: \"modal\" | \"drawer\" | \"full\";\n\n /**\n * Desktop variant (defaults to 'modal').\n * On desktop, this variant will be used instead of the main variant.\n */\n desktopVariant?: \"modal\" | \"drawer\" | \"full\";\n\n /**\n * Mobile variant (defaults to 'drawer').\n * On mobile, this variant will be used instead of the main variant.\n */\n mobileVariant?: \"modal\" | \"drawer\" | \"full\";\n\n /**\n * Whether clicking outside closes the dialog (default: true).\n */\n dismissible?: boolean;\n\n /**\n * Hide the X close button (default: false).\n */\n hideClose?: boolean;\n\n /**\n * Additional CSS classes for the dialog content.\n */\n className?: string;\n\n /**\n * Optional footer card to render at the bottom of the dialog.\n * When provided, this takes precedence over the default close button.\n */\n footer?: PiCardRef;\n\n /**\n * Text for the default close button in the footer.\n * Only used when `footer` is not provided.\n * Set to `null` to hide the default close button entirely.\n * @default \"Close\"\n */\n footerCloseButtonText?: string | null;\n};\n\nexport type DialogOpenedEvent = {\n id?: string;\n};\n\nexport type DialogClosedEvent = {\n id?: string;\n /**\n * Reason for closing:\n * - 'user': User dismissed (clicked outside, pressed ESC, clicked X)\n * - 'programmatic': Closed via props.open change\n */\n reason?: \"user\" | \"programmatic\";\n};\n\nexport type DialogOpenChangedEvent = {\n open: boolean;\n id?: string;\n};\n\nexport type PiDialogEvents = {\n onOpened: DialogOpenedEvent;\n onClosed: DialogClosedEvent;\n onOpenChanged: DialogOpenChangedEvent;\n};\n",
|
|
26
|
+
"type": "registry:component",
|
|
27
|
+
"target": "src/cards/dialog/dialog.types.ts"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"path": "cards/dialog/index.ts",
|
|
31
|
+
"content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {DialogComponent} from \"./dialog.component\";\nimport {PI_DIALOG_ACTION, PI_DIALOG_CARD} from \"./dialog.types\";\n\nexport * from \"./dialog.types\";\n\nregisterCardComponent({\n name: PI_DIALOG_CARD,\n component: DialogComponent,\n events: actionTypesToEvents(PI_DIALOG_ACTION),\n});\n",
|
|
32
|
+
"type": "registry:component",
|
|
33
|
+
"target": "src/cards/dialog/index.ts"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "dropDownMenu",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga dropDownMenu card component",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@radix-ui/react-dropdown-menu@^2.1.16",
|
|
8
|
+
"class-variance-authority@^0.7.1",
|
|
9
|
+
"lucide-react@^0.513.0"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
|
|
13
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-lib-utils.json"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "cards/dropDownMenu/drop-down.component.tsx",
|
|
18
|
+
"content": "import React from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\n\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu.ui\";\n\nimport type {\n DropDownMenuEntry,\n DropDownMenuEvents,\n DropDownMenuProps,\n MenuContent,\n MenuLabelEntry,\n MenuSubMenuEntry,\n} from \"./drop-down.types\";\n\nimport {DropdownOpenContext} from \"./dropdown-context\";\n\nexport const Component = (\n props: PiCardProps<DropDownMenuProps, DropDownMenuEvents>,\n): React.ReactNode => {\n const {trigger, menuLabel, menuAlign, items, cardName, checkboxCloseDelayMs} =\n props;\n\n const [open, setOpen] = React.useState(false);\n const closeTimerRef = React.useRef<number | null>(null);\n\n const [localCheckboxChecked, setLocalCheckboxChecked] = React.useState<\n Record<string, boolean>\n >({});\n\n const checkboxKey = React.useCallback((path: string[], id: string) => {\n return [...path, id].join(\"::\");\n }, []);\n\n const buildCheckboxStateFromItems = React.useCallback(\n (\n entries: DropDownMenuEntry[] | undefined,\n path: string[] = [],\n ): Record<string, boolean> => {\n const state: Record<string, boolean> = {};\n if (!entries?.length) return state;\n\n for (const entry of entries) {\n if (!entry || typeof entry === \"string\") continue;\n if (entry.type === \"group\") {\n Object.assign(state, buildCheckboxStateFromItems(entry.items, path));\n continue;\n }\n if (entry.type === \"submenu\") {\n const subId = entryIdForPath(entry);\n Object.assign(\n state,\n buildCheckboxStateFromItems(entry.items, [...path, subId]),\n );\n continue;\n }\n if (entry.type === \"checkbox\") {\n const initial = Boolean(\n entry.checked ?? entry.defaultChecked ?? false,\n );\n state[checkboxKey(path, entry.id)] = initial;\n }\n }\n\n return state;\n },\n [checkboxKey],\n );\n\n function clearCloseTimer() {\n if (closeTimerRef.current !== null) {\n window.clearTimeout(closeTimerRef.current);\n closeTimerRef.current = null;\n }\n }\n\n function closeNow() {\n clearCloseTimer();\n setOpen(false);\n }\n\n function scheduleClose(delayMs: number) {\n clearCloseTimer();\n closeTimerRef.current = window.setTimeout(() => {\n closeTimerRef.current = null;\n setOpen(false);\n }, delayMs);\n }\n\n React.useEffect(() => {\n return () => {\n // cleanup on unmount\n clearCloseTimer();\n };\n }, []);\n\n // If the menu model changes while open, resync the local checkbox state.\n // This lets external state updates still win.\n React.useEffect(() => {\n if (!open) return;\n setLocalCheckboxChecked(buildCheckboxStateFromItems(items));\n }, [open, items, buildCheckboxStateFromItems]);\n\n function renderTrigger() {\n // IMPORTANT: Radix `DropdownMenuTrigger` with `asChild` requires its direct\n // child to accept/forward the props Radix injects (pointer handlers, aria\n // attributes, refs). `@pihanga2/core`'s <Card /> does not forward arbitrary\n // props to the underlying DOM element rendered by the referenced card.\n //\n // So we always provide a real DOM element as the trigger and render the\n // PiCard inside it. Events bubble, so clicking the inner PiButton still\n // toggles the menu.\n //\n // We provide the dropdown's open state via context so nested components\n // (like Button) can hide their tooltips when the dropdown is open.\n return (\n <span className=\"inline-flex\" data-pihanga-trigger-wrapper>\n <DropdownOpenContext.Provider value={open}>\n <Card cardName={trigger} parentCard={cardName} />\n </DropdownOpenContext.Provider>\n </span>\n );\n }\n\n function renderLabel() {\n if (!menuLabel) return null;\n return (\n <>\n <DropdownMenuLabel>{menuLabel}</DropdownMenuLabel>\n <DropdownMenuSeparator />\n </>\n );\n }\n\n function contentToNode(content: MenuContent): React.ReactNode {\n if (typeof content === \"string\") return content;\n return <Card cardName={content} parentCard={cardName} />;\n }\n\n function entryIdForPath(entry: MenuSubMenuEntry): string {\n if (entry.id) return entry.id;\n return typeof entry.label === \"string\" ? entry.label : \"submenu\";\n }\n\n function emitSelected(ev: {\n type: \"item\" | \"checkbox\" | \"radio\";\n id: string;\n value?: string;\n checked?: boolean;\n path?: string[];\n }) {\n props.onSelected(ev);\n }\n\n function renderShortcut(shortcut?: string) {\n if (!shortcut) return null;\n return <DropdownMenuShortcut>{shortcut}</DropdownMenuShortcut>;\n }\n\n function renderEntries(entries: DropDownMenuEntry[], path: string[] = []) {\n return entries.map((entry, idx) => {\n if (entry === null) {\n return <DropdownMenuSeparator key={`sep-${idx}`} />;\n }\n\n if (typeof entry === \"string\") {\n // legacy style within `items` (still supported)\n return (\n <DropdownMenuItem\n key={`s-${idx}`}\n onSelect={() => emitSelected({type: \"item\", id: entry, path})}\n >\n {entry}\n </DropdownMenuItem>\n );\n }\n\n if (entry.type === \"separator\") {\n return <DropdownMenuSeparator key={`sepobj-${idx}`} />;\n }\n\n if (entry.type === \"label\") {\n const e: MenuLabelEntry = entry;\n return (\n <DropdownMenuLabel key={`label-${idx}`} inset={e.inset}>\n {contentToNode(e.label)}\n </DropdownMenuLabel>\n );\n }\n\n if (entry.type === \"group\") {\n const e = entry;\n return (\n <DropdownMenuGroup key={`group-${idx}`}>\n {e.label && (\n <DropdownMenuLabel inset={e.inset}>\n {contentToNode(e.label)}\n </DropdownMenuLabel>\n )}\n {renderEntries(e.items, path)}\n </DropdownMenuGroup>\n );\n }\n\n if (entry.type === \"submenu\") {\n const e: MenuSubMenuEntry = entry;\n const subId = entryIdForPath(e);\n const nextPath = [...path, subId];\n return (\n <DropdownMenuSub key={`sub-${idx}`}>\n <DropdownMenuSubTrigger inset={e.inset} disabled={e.disabled}>\n {contentToNode(e.label)}\n {renderShortcut(e.shortcut)}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n {renderEntries(e.items, nextPath)}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n );\n }\n\n if (entry.type === \"checkbox\") {\n const e = entry;\n const key = checkboxKey(path, e.id);\n const checked =\n localCheckboxChecked[key] ??\n Boolean(e.checked ?? e.defaultChecked ?? false);\n return (\n <DropdownMenuCheckboxItem\n key={`cb-${e.id}`}\n checked={checked}\n disabled={e.disabled}\n onCheckedChange={(checked) => {\n const nextChecked = Boolean(checked);\n\n // Update local state immediately so the tick updates while menu is open.\n setLocalCheckboxChecked((prev) => ({\n ...prev,\n [key]: nextChecked,\n }));\n\n emitSelected({\n type: \"checkbox\",\n id: e.id,\n checked: nextChecked,\n path,\n });\n\n // If a delay is configured, keep menu open but schedule an auto-close.\n // Each checkbox selection resets the timer.\n if (typeof checkboxCloseDelayMs === \"number\") {\n if (checkboxCloseDelayMs > 0) {\n scheduleClose(checkboxCloseDelayMs);\n } else {\n // Treat 0ms as \"close immediately\".\n closeNow();\n }\n }\n }}\n onSelect={(evt) => {\n // keepOpen OR delayed-close mode should keep menu open after select.\n if (e.keepOpen || typeof checkboxCloseDelayMs === \"number\") {\n evt.preventDefault();\n }\n }}\n >\n {contentToNode(e.label)}\n {renderShortcut(e.shortcut)}\n </DropdownMenuCheckboxItem>\n );\n }\n\n if (entry.type === \"radio-group\") {\n const e = entry;\n return (\n <DropdownMenuRadioGroup\n key={`rg-${e.id}`}\n value={e.value}\n defaultValue={e.defaultValue}\n onValueChange={(value) => {\n emitSelected({type: \"radio\", id: e.id, value, path});\n }}\n >\n {e.label && (\n <DropdownMenuLabel inset={e.inset}>\n {contentToNode(e.label)}\n </DropdownMenuLabel>\n )}\n {e.items.map((ri) => (\n <DropdownMenuRadioItem\n key={`${e.id}:${ri.value}`}\n value={ri.value}\n disabled={ri.disabled}\n className={ri.inset ? \"pl-8\" : undefined}\n hideIcon={ri.hideIcon}\n onSelect={(evt) => {\n if (ri.keepOpen) evt.preventDefault();\n }}\n >\n {contentToNode(ri.label)}\n {renderShortcut(ri.shortcut)}\n </DropdownMenuRadioItem>\n ))}\n </DropdownMenuRadioGroup>\n );\n }\n\n if (entry.type === \"item\") {\n const e = entry;\n return (\n <DropdownMenuItem\n key={`it-${e.id}`}\n disabled={e.disabled}\n className={e.inset ? \"pl-8\" : undefined}\n onSelect={(evt) => {\n if (e.keepOpen) evt.preventDefault();\n emitSelected({type: \"item\", id: e.id, path});\n }}\n >\n {contentToNode(e.label)}\n {renderShortcut(e.shortcut)}\n </DropdownMenuItem>\n );\n }\n\n // Exhaustiveness guard (should never happen)\n return null;\n });\n }\n\n const effectiveItems = items;\n\n return (\n <DropdownMenu\n data-pihanga={cardName}\n open={open}\n onOpenChange={(nextOpen) => {\n // Any non-checkbox close reason (outside click, escape, blur, etc.)\n // should close immediately and cancel pending timers.\n if (!nextOpen) {\n closeNow();\n return;\n }\n\n clearCloseTimer();\n setLocalCheckboxChecked(buildCheckboxStateFromItems(items));\n setOpen(true);\n }}\n >\n <DropdownMenuTrigger asChild>{renderTrigger()}</DropdownMenuTrigger>\n <DropdownMenuContent align={menuAlign}>\n {renderLabel()}\n {effectiveItems?.length ? renderEntries(effectiveItems) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n};\n",
|
|
19
|
+
"type": "registry:component",
|
|
20
|
+
"target": "src/cards/dropDownMenu/drop-down.component.tsx"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "cards/dropDownMenu/drop-down.types.ts",
|
|
24
|
+
"content": "import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const DROP_DOWN_MENU_CARD = \"pi/drop-down-menu\";\nexport const DropDownMenu = createCardDeclaration<\n DropDownMenuProps,\n DropDownMenuEvents\n>(DROP_DOWN_MENU_CARD);\n\nexport const DROP_DOWN_MENU_ACTION = registerActions(DROP_DOWN_MENU_CARD, [\n \"selected\",\n]);\n\nexport const onDropDownMenuClicked = createOnAction<DropDownMenuSelectedEvent>(\n DROP_DOWN_MENU_ACTION.SELECTED,\n);\n\nexport type DropDownMenuProps = {\n /**\n * Dropdown trigger.\n *\n * - For a PiButton (or any custom) trigger, pass a `PiCardRef` and use the\n * referenced card's props. For `PiButton` specifically:\n * - `opts.iconName` + `opts.iconPlacement` to show an icon on the left/right.\n * - `label: \"\"` for an icon-only button.\n *\n * Implementation note: the dropdown trigger is wrapped in a DOM element so\n * Radix can attach trigger handlers even when the trigger is rendered via\n * `@pihanga2/core`'s `<Card />`.\n */\n trigger: PiCardRef;\n menuAlign?: \"center\" | \"end\" | \"start\";\n menuLabel?: string;\n\n /**\n * Menu declaration (supports sub-menus, checkbox/radio items, groups, labels, separators, ...).\n *\n * Prefer this over `itemGroups`.\n */\n items?: DropDownMenuEntry[];\n\n /**\n * When set (ms), selecting a checkbox item will keep the menu open and auto-close\n * it after this delay. Each checkbox selection resets the timer.\n *\n * Blur / outside interactions still close immediately.\n */\n checkboxCloseDelayMs?: number;\n\n className?: string;\n};\n\n/**\n * Menu content can either be plain text or an embedded PiCard.\n *\n * NOTE: we intentionally type this as `PiCardRef` (a string), not a card definition.\n */\nexport type MenuContent = string | PiCardRef;\n\nexport type MenuSeparatorEntry = null | {type: \"separator\"};\n\nexport type MenuLabelEntry = {\n type: \"label\";\n label: MenuContent;\n inset?: boolean;\n};\n\nexport type MenuActionItemEntry = {\n type: \"item\";\n id: string;\n label: MenuContent;\n disabled?: boolean;\n inset?: boolean;\n shortcut?: string;\n /** Prevents the dropdown from closing when selected. */\n keepOpen?: boolean;\n};\n\nexport type MenuCheckboxItemEntry = {\n type: \"checkbox\";\n id: string;\n label: MenuContent;\n disabled?: boolean;\n inset?: boolean;\n shortcut?: string;\n checked?: boolean;\n defaultChecked?: boolean;\n /** Prevents the dropdown from closing when selected. */\n keepOpen?: boolean;\n};\n\nexport type MenuRadioItemEntry = {\n value: string;\n label: MenuContent;\n disabled?: boolean;\n inset?: boolean;\n shortcut?: string;\n /** Prevents the dropdown from closing when selected. */\n keepOpen?: boolean;\n /** Only affects the icon rendering (registry/ui supports this). */\n hideIcon?: boolean;\n};\n\nexport type MenuRadioGroupEntry = {\n type: \"radio-group\";\n id: string;\n label?: MenuContent;\n /** Optional extra left padding for the label (matches shadcn dropdown label inset). */\n inset?: boolean;\n value?: string;\n defaultValue?: string;\n items: MenuRadioItemEntry[];\n};\n\nexport type MenuGroupEntry = {\n type: \"group\";\n label?: MenuContent;\n /** Optional extra left padding for the label (matches shadcn dropdown label inset). */\n inset?: boolean;\n items: DropDownMenuEntry[];\n};\n\nexport type MenuSubMenuEntry = {\n type: \"submenu\";\n /** Optional id used for event `path` (falls back to label string if possible). */\n id?: string;\n label: MenuContent;\n disabled?: boolean;\n inset?: boolean;\n shortcut?: string;\n items: DropDownMenuEntry[];\n};\n\nexport type DropDownMenuEntry =\n | string\n | MenuSeparatorEntry\n | MenuLabelEntry\n | MenuActionItemEntry\n | MenuCheckboxItemEntry\n | MenuRadioGroupEntry\n | MenuGroupEntry\n | MenuSubMenuEntry;\n\nexport type DropDownMenuSelectedEventAdvanced = {\n /** Discriminant for advanced menu items. */\n type: \"item\" | \"checkbox\" | \"radio\";\n\n /** The id of the selected menu entry (or the radio group id for `type: 'radio'`). */\n id: string;\n\n /** For radio items this is the selected value. */\n value?: string;\n\n /** For checkbox items this is the new checked state. */\n checked?: boolean;\n\n /** Hierarchical path of submenu ids leading to the item (if any). */\n path?: string[];\n};\n\nexport type DropDownMenuSelectedEvent = DropDownMenuSelectedEventAdvanced;\n\nexport type DropDownMenuEvents = {\n onSelected: DropDownMenuSelectedEvent;\n};\n",
|
|
25
|
+
"type": "registry:component",
|
|
26
|
+
"target": "src/cards/dropDownMenu/drop-down.types.ts"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "cards/dropDownMenu/dropdown-context.tsx",
|
|
30
|
+
"content": "import React from \"react\";\n\n/**\n * Context to communicate dropdown menu state to nested components (e.g., Button triggers).\n * This allows triggers to hide their tooltips when the dropdown is open.\n */\nexport const DropdownOpenContext = React.createContext<boolean>(false);\n\nexport const useDropdownOpen = () => React.useContext(DropdownOpenContext);\n",
|
|
31
|
+
"type": "registry:component",
|
|
32
|
+
"target": "src/cards/dropDownMenu/dropdown-context.tsx"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": "cards/dropDownMenu/dropdown-menu.ui.tsx",
|
|
36
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport {type VariantProps, cva} from \"class-variance-authority\";\nimport {CheckIcon, ChevronRightIcon} from \"lucide-react\";\n\nimport {cn} from \"@/lib/utils\";\n\n/**\n * Pihanga-owned copy of the registry dropdown-menu primitives.\n *\n * Why this exists:\n * - `src/registry/**` is upstream-managed and must remain read-only.\n * - We needed menu width/shrink-to-fit fixes (`w-max`, avoid %/calc widths)\n * for correct sizing with long labels.\n */\nexport const dropdownMenuItemVariants = cva(\n cn(\n \"relative flex cursor-pointer items-center gap-2 rounded-md align-middle text-sm no-focus-ring transition-bg-ease select-none data-disabled:pointer-events-none data-disabled:opacity-50\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-subtle-foreground\",\n \"text-accent-foreground hover:bg-accent focus:bg-accent focus:text-accent-foreground\",\n ),\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n // NOTE: Do NOT use %/calc-based widths here.\n // Radix positions the dropdown content with a shrink-to-fit layout.\n // If menu items have percentage widths, they don't contribute to the\n // intrinsic (max-content) sizing of the container.\n default: \"mx-1 h-[28px] px-2.5\",\n none: \"\",\n },\n variant: {\n default: \"focus:bg-accent focus:text-accent-foreground\",\n none: \"\",\n },\n },\n },\n);\n\nconst dropdownMenuLabelVariants = cva(\n cn(\n \"mt-1.5 mb-2 cursor-default px-[14px] text-xs font-medium text-muted-foreground select-none\",\n ),\n {\n variants: {\n inset: {\n true: \"pl-8\",\n },\n },\n },\n);\n\nexport function DropdownMenu(props: DropdownMenuPrimitive.DropdownMenuProps) {\n return <DropdownMenuPrimitive.Root {...props} />;\n}\n\nexport function DropdownMenuTrigger(\n props: DropdownMenuPrimitive.DropdownMenuTriggerProps,\n) {\n return <DropdownMenuPrimitive.Trigger {...props} />;\n}\n\nexport function DropdownMenuGroup(\n props: DropdownMenuPrimitive.DropdownMenuGroupProps,\n) {\n return <DropdownMenuPrimitive.Group className=\"py-1.5\" {...props} />;\n}\n\nexport function DropdownMenuPortal(\n props: DropdownMenuPrimitive.DropdownMenuPortalProps,\n) {\n return <DropdownMenuPrimitive.Portal {...props} />;\n}\n\nexport function DropdownMenuSub(\n props: DropdownMenuPrimitive.DropdownMenuSubProps,\n) {\n return <DropdownMenuPrimitive.Sub {...props} />;\n}\n\nexport function DropdownMenuRadioGroup(\n props: DropdownMenuPrimitive.DropdownMenuRadioGroupProps,\n) {\n return <DropdownMenuPrimitive.RadioGroup {...props} />;\n}\n\nexport function DropdownMenuSubTrigger({\n children,\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.SubTrigger\n className={cn(\n \"mx-1 flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent data-[state=open]:bg-accent\",\n \"no-focus-ring\",\n \"data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n inset && \"pl-8\",\n className,\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ml-auto\" />\n </DropdownMenuPrimitive.SubTrigger>\n );\n}\n\nexport function DropdownMenuSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n return (\n <DropdownMenuPrimitive.SubContent\n className={cn(\n // `w-max` ensures submenu width grows with its content (up to max-w)\n \"z-50 max-w-[100vw] min-w-32 w-max overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-floating data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuContent({\n className,\n portal,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content> & {\n portal?: boolean;\n}) {\n const content = (\n <DropdownMenuPrimitive.Content\n className={cn(\n // `w-max` makes the menu container grow with its content (up to max-w)\n \"z-50 max-w-[100vw] min-w-32 w-max overflow-hidden rounded-lg bg-popover p-0 text-sm text-popover-foreground shadow-floating no-focus-ring\",\n \"data-[side=bottom]:origin-top data-[side=left]:origin-right data-[side=right]:origin-left data-[side=top]:origin-bottom data-[state=closed]:hidden data-[state=open]:animate-zoom\",\n className,\n )}\n sideOffset={4}\n {...props}\n />\n );\n\n if (portal) {\n return (\n <DropdownMenuPrimitive.Portal>{content}</DropdownMenuPrimitive.Portal>\n );\n }\n\n return content;\n}\n\nexport function DropdownMenuItem({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> &\n VariantProps<typeof dropdownMenuItemVariants>) {\n return (\n <DropdownMenuPrimitive.Item\n className={cn(dropdownMenuItemVariants(), className)}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuCheckboxItem({\n children,\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n className={cn(\n \"relative flex items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 no-focus-ring transition-bg-ease select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4\",\n // NOTE: Do NOT use %/calc-based widths here for the same reason as\n // dropdownMenuItemVariants: it prevents shrink-to-fit content sizing.\n // IMPORTANT: Don't use `px-*` here, it would override `pr-8` above and\n // cause the label to overlap the right-side check indicator.\n \"mx-1 h-[28px] cursor-pointer data-[state=highlighted]:bg-accent data-[state=highlighted]:text-accent-foreground\",\n className,\n )}\n {...props}\n >\n <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CheckIcon />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n}\n\nexport function DropdownMenuRadioItem({\n children,\n className,\n hideIcon,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {\n hideIcon?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.RadioItem\n className={cn(\n \"relative flex items-center rounded-sm pr-2 pl-8 no-focus-ring transition-bg-ease select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n // NOTE: Do NOT use %/calc-based widths here for the same reason as\n // dropdownMenuItemVariants: it prevents shrink-to-fit content sizing.\n // IMPORTANT: Don't use `px-*` here, it would override `pl-8` above and\n // misalign the radio indicator.\n \"mx-1 h-[28px] cursor-pointer gap-2 data-[state=highlighted]:bg-accent data-[state=highlighted]:text-accent-foreground [&_svg]:size-4\",\n className,\n )}\n {...props}\n >\n {!hideIcon && (\n <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CheckIcon />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n )}\n {children}\n </DropdownMenuPrimitive.RadioItem>\n );\n}\n\nexport function DropdownMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.Label\n className={cn(dropdownMenuLabelVariants({inset}), className)}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n {...props}\n />\n );\n}\n\nexport function useOpenState() {\n const [open, setOpen] = React.useState(false);\n\n const onOpenChange = React.useCallback(\n (_value = !open) => {\n setOpen(_value);\n },\n [open],\n );\n\n return {\n open,\n onOpenChange,\n };\n}\n",
|
|
37
|
+
"type": "registry:component",
|
|
38
|
+
"target": "src/cards/dropDownMenu/dropdown-menu.ui.tsx"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"path": "cards/dropDownMenu/index.ts",
|
|
42
|
+
"content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {DROP_DOWN_MENU_ACTION, DROP_DOWN_MENU_CARD} from \"./drop-down.types\";\nimport {Component} from \"./drop-down.component\";\n\nexport * from \"./drop-down.types\";\n\nregisterCardComponent({\n name: DROP_DOWN_MENU_CARD,\n component: Component,\n events: actionTypesToEvents(DROP_DOWN_MENU_ACTION),\n});\n",
|
|
43
|
+
"type": "registry:component",
|
|
44
|
+
"target": "src/cards/dropDownMenu/index.ts"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
package/r/field.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "field",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga field card component",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"class-variance-authority@^0.7.1"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
|
|
11
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-ui-extras.json",
|
|
12
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-lib-utils.json",
|
|
13
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/form.json"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "cards/field/field.component.tsx",
|
|
18
|
+
"content": "import React, {useMemo} from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldLabel,\n} from \"@/components/ui/field\";\nimport {cn} from \"@/lib/utils\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport type {PiFieldProps} from \"./field.types\";\n\nexport const FieldCardComponent = (\n props: PiCardProps<PiFieldProps>,\n): React.ReactNode => {\n const {\n label,\n fieldCard,\n name,\n description,\n error: propError,\n className,\n cardName,\n } = props;\n\n // When name is provided and we're inside a pi/form, use the form error.\n const form = useFormContext();\n const error = form.isInForm && name ? form.errors[name] : propError;\n\n // Stable id that links <FieldLabel htmlFor> to the inner control's id.\n const fieldId = React.useId();\n\n // Memoize the extra props forwarded to the inner card so that Pihanga does\n // not see a brand-new object on every render (which would cause it to treat\n // the inner card as a new/changed card and unmount/remount it).\n const isInvalid = Boolean(error);\n /*\n Pihanga's Card forwards every prop other than cardName / parentCard to\n the bound component. We inject `id` (so the control's element gets\n the id that FieldLabel points at) and `invalid` (so the control can\n set aria-invalid without needing its own error/label logic).\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const fieldCardProps = useMemo<any>(\n () => ({\n cardName: fieldCard,\n parentCard: cardName,\n id: fieldId,\n invalid: isInvalid,\n }),\n [cardName, fieldCard, fieldId, isInvalid],\n );\n\n return (\n <Field\n data-invalid={Boolean(error) || undefined}\n className={cn(className)}\n data-pihanga={cardName}\n >\n <FieldLabel htmlFor={fieldId}>{label}</FieldLabel>\n\n <Card {...fieldCardProps} />\n\n {description && <FieldDescription>{description}</FieldDescription>}\n <FieldError>{error}</FieldError>\n </Field>\n );\n};\n",
|
|
19
|
+
"type": "registry:component",
|
|
20
|
+
"target": "src/cards/field/field.component.tsx"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "cards/field/field.types.ts",
|
|
24
|
+
"content": "import {createCardDeclaration, PiCardRef} from \"@pihanga2/core\";\n\nexport const PI_FIELD_CARD = \"pi/field\";\n\nexport const Field = createCardDeclaration<PiFieldProps>(PI_FIELD_CARD);\n\nexport type PiFieldProps = {\n /** Visible label rendered above the control. */\n label: string;\n\n /**\n * The Pihanga card reference for the plain control to render\n * (e.g. pi/text-field, pi/select, pi/checkbox, …).\n *\n * The field card will forward an `id` prop and an `invalid` boolean to the\n * inner card via Pihanga's standard prop-passing mechanism so the control\n * can wire up accessibility attributes without needing its own label logic.\n */\n fieldCard: PiCardRef;\n\n /**\n * Field name — when provided the card reads the current error from\n * FormContext (when inside a pi/form) to decide whether to show an error.\n */\n name?: string;\n\n /** Secondary help text rendered below the control. */\n description?: string;\n\n /**\n * Static error message. Overridden by the form-context error when `name`\n * is provided and the card is inside a pi/form.\n */\n error?: string;\n\n /** Additional CSS classes applied to the outer Field wrapper. */\n className?: string;\n};\n",
|
|
25
|
+
"type": "registry:component",
|
|
26
|
+
"target": "src/cards/field/field.types.ts"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "cards/field/index.ts",
|
|
30
|
+
"content": "import {registerCardComponent} from \"@pihanga2/core\";\n\nimport {FieldCardComponent} from \"./field.component\";\nimport {PI_FIELD_CARD} from \"./field.types\";\n\nexport * from \"./field.types\";\n\n// pi/field has no events of its own; events fire from the inner fieldCard.\nregisterCardComponent({\n name: PI_FIELD_CARD,\n component: FieldCardComponent,\n});\n",
|
|
31
|
+
"type": "registry:component",
|
|
32
|
+
"target": "src/cards/field/index.ts"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
package/r/flexGrid.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "flexGrid",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga flexGrid card component",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx@^2.1.1"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "cards/flexGrid/flexGrid.component.tsx",
|
|
15
|
+
"content": "import React from \"react\";\nimport {Card, PiCardProps, PiCardRef} from \"@pihanga2/core\";\nimport {FlexGridProps} from \"./flexGrid.types\";\nimport clsx from \"clsx\";\n\nexport const FlexGridComponent = (\n props: PiCardProps<FlexGridProps>,\n): React.ReactNode => {\n const {\n cardName,\n cards = {},\n template,\n height = \"auto\", //'100vh',\n margin = 0,\n overflow = \"hidden\", // 'scroll',\n style,\n className,\n _cls,\n } = props;\n\n // console.log(\"AREA\", area)\n const _style = {\n display: \"grid\",\n gridGap: template.gap || \"10px\",\n height,\n margin,\n width: \"100%\",\n ...style?.root,\n };\n\n if (template.area) {\n const areaRows = template.area.map((rn) => `\"${rn.join(\" \")}\"`);\n _style.gridTemplateAreas = areaRows.join(\" \");\n }\n if (template.rows) {\n _style.gridTemplateRows = template.rows.join(\" \");\n }\n if (template.columns) {\n _style.gridTemplateColumns = template.columns.join(\" \");\n }\n\n function renderGridCard(v: [string, PiCardRef]): React.ReactElement {\n const [name, gridCard] = v;\n const _style = {\n overflow,\n ...style?.item,\n };\n if (template.area) {\n _style.gridArea = name;\n }\n return (\n <div style={_style} data-pihanga-grid={name} key={name}>\n <Card cardName={gridCard} parentCard={cardName} />\n </div>\n );\n }\n\n return (\n <div\n style={_style}\n className={clsx(_cls(\"root\"), className)}\n data-pihanga={cardName}\n >\n {Object.entries(cards).map(renderGridCard)}\n </div>\n );\n};\n",
|
|
16
|
+
"type": "registry:component",
|
|
17
|
+
"target": "src/cards/flexGrid/flexGrid.component.tsx"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "cards/flexGrid/flexGrid.types.ts",
|
|
21
|
+
"content": "import { createCardDeclaration, PiCardRef } from \"@pihanga2/core\"\n\nexport const FLEX_GRID_CARD = \"flex_grid\"\nexport const FlexGrid = createCardDeclaration<FlexGridProps>(FLEX_GRID_CARD)\n\nexport type FlexGridProps = {\n cards: { [name: string]: PiCardRef }\n template: TemplateT\n height?: string\n margin?: string\n overflow?: string\n\n style?: {\n root?: React.CSSProperties\n item?: React.CSSProperties\n }\n className?: string\n}\n\n// body {\n// display: grid;\n// grid-template-areas:\n// \"header header header\"\n// \"nav article ads\"\n// \"footer footer footer\";\n// grid-template-rows: 60px 1fr 60px;\n// grid-template-columns: 20% 1fr 15%;\n// grid-gap: 10px;\n// height: 100vh;\n// margin: 0;\n// }\n\n// https://css-tricks.com/snippets/css/complete-guide-grid/\nexport type TemplateT = {\n area?: string[][] // name of card dict\n rows?: string[] // grid-template-rows (e.g [\"min-content\", \"1fr\", \"min-content\"])\n columns?: string[] // grid-template-cols (e.g. [\"1fr\", \"50px\", \"1fr\", \"1fr\"])\n gap?: string\n}\n",
|
|
22
|
+
"type": "registry:component",
|
|
23
|
+
"target": "src/cards/flexGrid/flexGrid.types.ts"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "cards/flexGrid/index.ts",
|
|
27
|
+
"content": "import {registerCardComponent} from \"@pihanga2/core\";\n\nimport {FlexGridComponent} from \"./flexGrid.component\";\nimport {FLEX_GRID_CARD} from \"./flexGrid.types\";\n\nexport * from \"./flexGrid.types\";\n\nregisterCardComponent({\n name: FLEX_GRID_CARD,\n component: FlexGridComponent,\n});\n",
|
|
28
|
+
"type": "registry:component",
|
|
29
|
+
"target": "src/cards/flexGrid/index.ts"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
package/r/form.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "form",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga form card component",
|
|
6
|
+
"dependencies": [],
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
|
|
9
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-lib-utils.json"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "cards/form/form.component.tsx",
|
|
14
|
+
"content": "import React, {useState, useCallback} from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport {cn} from \"@/lib/utils\";\nimport {FormContext, type FormContextValue} from \"./form.context\";\nimport type {PiFormEvents, PiFormProps} from \"./form.types\";\n\nexport const FormComponent = (\n props: PiCardProps<PiFormProps, PiFormEvents>,\n): React.ReactNode => {\n const {\n id,\n content = [],\n initialValues = {},\n submitLabel = \"Submit\",\n className,\n cardName,\n onSubmitted,\n } = props;\n\n const [formData, setFormData] = useState<Record<string, unknown>>(() => ({\n ...initialValues,\n }));\n const [errors, setErrors] = useState<Record<string, string>>({});\n\n const handleChange = useCallback((fieldName: string, value: unknown) => {\n setFormData((prev) => ({...prev, [fieldName]: value}));\n // Clear validation error when the user modifies the field\n setErrors((prev) => {\n if (!prev[fieldName]) return prev;\n const next = {...prev};\n delete next[fieldName];\n return next;\n });\n }, []);\n\n const setError = useCallback((fieldName: string, error: string | null) => {\n setErrors((prev) => {\n if (error === null) {\n const next = {...prev};\n delete next[fieldName];\n return next;\n }\n return {...prev, [fieldName]: error};\n });\n }, []);\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n onSubmitted({id, formData});\n },\n [id, formData, onSubmitted],\n );\n\n const contextValue: FormContextValue = {\n formData,\n errors,\n handleChange,\n setError,\n isInForm: true,\n };\n\n return (\n <FormContext.Provider value={contextValue}>\n <form\n onSubmit={handleSubmit}\n className={cn(\"flex flex-col gap-4\", className)}\n data-pihanga={cardName}\n >\n {content.map((cref, idx) => (\n <Card key={idx} cardName={cref} parentCard={cardName} />\n ))}\n <button\n type=\"submit\"\n className=\"inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\"\n >\n {submitLabel}\n </button>\n </form>\n </FormContext.Provider>\n );\n};\n",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "src/cards/form/form.component.tsx"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "cards/form/form.context.tsx",
|
|
20
|
+
"content": "import React, {useContext} from \"react\";\n\n/**\n * Shape of the data provided by FormContext.\n * When inside a <Form> card, isInForm is true and all fields are populated.\n * When outside (fallback), isInForm is false.\n */\nexport interface FormContextValue {\n formData: Record<string, unknown>;\n errors: Record<string, string>;\n handleChange: (fieldName: string, value: unknown) => void;\n setError: (fieldName: string, error: string | null) => void;\n isInForm: boolean;\n}\n\nconst FALLBACK_CONTEXT: FormContextValue = {\n formData: {},\n errors: {},\n handleChange: () => {},\n setError: () => {},\n isInForm: false,\n};\n\nexport const FormContext = React.createContext<FormContextValue | null>(null);\n\n/**\n * Hook that reads the nearest FormContext.\n * Returns a fallback (isInForm=false) when called outside a <Form> card.\n *\n * Usage in field components:\n * const form = useFormContext();\n * const value = form.isInForm && name ? form.formData[name] ?? '' : propValue;\n */\nexport const useFormContext = (): FormContextValue => {\n const ctx = useContext(FormContext);\n return ctx ?? FALLBACK_CONTEXT;\n};\n",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "src/cards/form/form.context.tsx"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "cards/form/form.types.ts",
|
|
26
|
+
"content": "import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_FORM_CARD = \"pi/form\";\n\nexport const Form = createCardDeclaration<PiFormProps, PiFormEvents>(\n PI_FORM_CARD,\n);\n\nexport const PI_FORM_ACTION = registerActions(PI_FORM_CARD, [\"submitted\"]);\n\nexport const onPiFormSubmitted = createOnAction<PiFormSubmittedEvent>(\n PI_FORM_ACTION.SUBMITTED,\n);\n\nexport type PiFormProps = {\n /**\n * Optional id passed through to events for identification.\n */\n id?: string;\n\n /**\n * Ordered list of field card refs to render inside the form.\n * Each card (e.g. TextField, Checkbox, FormSelect) will be rendered\n * in order and wrapped by a FormContext.Provider so they can read\n * and write shared form state.\n */\n content?: PiCardRef[];\n\n /**\n * Initial values for the form fields, keyed by field name.\n */\n initialValues?: Record<string, unknown>;\n\n /**\n * Label for the submit button.\n * @default \"Submit\"\n */\n submitLabel?: string;\n\n /**\n * Additional CSS classes for the <form> element.\n */\n className?: string;\n};\n\nexport type PiFormSubmittedEvent = {\n id?: string;\n /** Snapshot of form state at the time of submission. */\n formData: Record<string, unknown>;\n};\n\nexport type PiFormEvents = {\n onSubmitted: PiFormSubmittedEvent;\n};\n",
|
|
27
|
+
"type": "registry:component",
|
|
28
|
+
"target": "src/cards/form/form.types.ts"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "cards/form/index.ts",
|
|
32
|
+
"content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {FormComponent} from \"./form.component\";\nimport {PI_FORM_ACTION, PI_FORM_CARD} from \"./form.types\";\n\nexport * from \"./form.types\";\nexport * from \"./form.context\";\n\nregisterCardComponent({\n name: PI_FORM_CARD,\n component: FormComponent,\n events: actionTypesToEvents(PI_FORM_ACTION),\n});\n",
|
|
33
|
+
"type": "registry:component",
|
|
34
|
+
"target": "src/cards/form/index.ts"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
package/r/framework.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "framework",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga framework card component",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@radix-ui/react-tooltip@^1.2.8"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
|
|
11
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-theme-provider.json",
|
|
12
|
+
"tooltip"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "cards/framework/framework.component.tsx",
|
|
17
|
+
"content": "import * as React from \"react\";\nimport {StrictMode} from \"react\";\nimport {ThemeProvider} from \"@/components/theme-provider/theme-provider.component\";\n\nimport {Card, type PiCardProps, type WindowProps} from \"@pihanga2/core\";\nimport {TooltipProvider} from \"@/components/ui/tooltip\";\n\nexport const Component = (props: PiCardProps<WindowProps>): React.ReactNode => {\n const {page, theme = \"dark\", cardName} = props;\n\n return (\n <StrictMode>\n <ThemeProvider defaultTheme={theme} storageKey=\"shadcn-ui-theme\">\n <TooltipProvider>\n <Card cardName={page} parentCard={cardName} />\n </TooltipProvider>\n </ThemeProvider>\n </StrictMode>\n );\n};\n",
|
|
18
|
+
"type": "registry:component",
|
|
19
|
+
"target": "src/cards/framework/framework.component.tsx"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "cards/framework/index.ts",
|
|
23
|
+
"content": "import {\n type WindowProps,\n createCardDeclaration,\n registerCardComponent,\n} from \"@pihanga2/core\";\n\nimport {Component} from \"./framework.component\";\n\nconst CARD_TYPE = \"shad/framework\";\nexport const SdFramework = createCardDeclaration<WindowProps>(CARD_TYPE);\n\nregisterCardComponent({\n name: CARD_TYPE,\n component: Component,\n});\n",
|
|
24
|
+
"type": "registry:component",
|
|
25
|
+
"target": "src/cards/framework/index.ts"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
package/r/graphin.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "graphin",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga graphin card component",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@antv/g@^6.3.1",
|
|
8
|
+
"@antv/g6@^5.1.1",
|
|
9
|
+
"@antv/graphin@^3.0.5",
|
|
10
|
+
"clsx@^2.1.1",
|
|
11
|
+
"lodash@^4.18.1"
|
|
12
|
+
],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"path": "cards/graphin/graphin.component.tsx",
|
|
19
|
+
"content": "import React from \"react\";\nimport {PiCardProps} from \"@pihanga2/core\";\nimport {Graphin} from \"@antv/graphin\";\n\nimport {\n NodeEvent,\n type GraphOptions,\n type Graph,\n} from \"@antv/g6\";\n\nimport {GraphinProps} from \"./graphin.types\";\nimport clsx from \"clsx\";\nimport {cloneDeep, merge} from \"lodash\";\nimport {TooltipComponent} from \"./tooltip.component\";\n\nexport const GraphinComponent = (\n props: PiCardProps<GraphinProps>,\n): React.ReactNode => {\n const {cardName, data, options, tooltip, style, className, _cls} = props;\n\n const handleReady = (graph: Graph) => {\n console.log(\"Graph ready:\", graph);\n\n // Add event handlers\n graph.on(NodeEvent.CLICK, (evt) => {\n console.log(\"Node clicked:\", evt);\n });\n\n graph.on(\"edge:click\", (evt) => {\n console.log(\"Edge clicked:\", evt);\n });\n };\n\n // console.log(\"AREA\", area)\n const _style = {\n // display: \"grid\",\n // gridGap: template.gap || \"10px\",\n // height,\n // margin,\n // width: \"100%\",\n\n display: \"flex\",\n width: \"100%\",\n ...style?.root,\n };\n\n const defOptions: GraphOptions = {\n data,\n autoResize: true,\n node: {\n style: {\n labelText: (d) => {\n return (d.data?.displayName || d.id) as string;\n },\n lod: {\n 0: {labelFontSize: 10}, // Zoomed out: smaller font\n 1: {labelFontSize: 12}, // Normal: standard font\n 2: {labelFontSize: 6}, // Zoomed in: reduce font size so it doesn't overwhelm\n },\n // labelFontSize: 12, // This is your \"Base\" size\n // labelWordWrap: true,\n // labelWordWrapWidth: 100, // Limits the horizontal growth\n },\n // palette: {\n // type: \"group\",\n // field: \"cluster\",\n // },\n },\n // layout: {\n // type: \"force\", // The layout engine type\n // preventOverlap: true, // Parameters specific to the 'force' engine\n // nodeSize: 30,\n // linkDistance: 100,\n // },\n layout: {\n type: \"force-atlas2\",\n preventOverlap: true,\n kr: 20,\n // center: [250, 250],\n },\n behaviors: [\n \"drag-canvas\",\n \"drag-element\",\n // \"zoom-canvas\",\n {\n type: \"auto-adapt-label\",\n enableAnimation: true,\n throttle: 100,\n padding: 0,\n },\n {\n type: \"zoom-canvas\",\n id: \"zoom-canvas-1\",\n // trigger: {\n // zoomIn: [\"Control\", \"+\"], // Zoom in shortcut\n // zoomOut: [\"Control\", \"-\"], // Zoom out shortcut\n // reset: [\"Control\", \"0\"], // Reset zoom ratio shortcut\n // },\n // trigger: [\"Control\"],\n fixSelectedItems: {fixLabel: true},\n },\n ],\n animation: true,\n };\n\n const mergedOptions = merge(cloneDeep(defOptions), options);\n return (\n <div\n style={_style}\n className={clsx(_cls(\"root\"), className)}\n data-pihanga={cardName}\n >\n <Graphin\n options={mergedOptions}\n onReady={handleReady}\n style={{width: \"inherit\"}}\n >\n {tooltip && (\n <TooltipComponent contentCards={tooltip} parentCard={cardName} />\n )}\n </Graphin>\n </div>\n );\n};\n",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "src/cards/graphin/graphin.component.tsx"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "cards/graphin/graphin.types.ts",
|
|
25
|
+
"content": "import {createCardDeclaration, PiCardDef} from \"@pihanga2/core\";\nimport {GraphData, GraphOptions} from \"@antv/g6\";\n\nexport const GRAPHIN_CARD = \"graphin\";\nexport const Graphin = createCardDeclaration<GraphinProps>(GRAPHIN_CARD);\n\nexport type GraphinProps = {\n data: GraphData;\n options?: Partial<Omit<GraphOptions, \"data\">>;\n tooltip?: GraphinTooltip;\n style?: {\n root?: React.CSSProperties;\n item?: React.CSSProperties;\n };\n className?: string;\n};\n\nexport type GraphinTooltip = {\n node?: PiCardDef;\n edge?: PiCardDef;\n};\n",
|
|
26
|
+
"type": "registry:component",
|
|
27
|
+
"target": "src/cards/graphin/graphin.types.ts"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"path": "cards/graphin/index.ts",
|
|
31
|
+
"content": "import {registerCardComponent} from \"@pihanga2/core\";\n\nimport {GraphinComponent} from \"./graphin.component\";\nimport {GRAPHIN_CARD} from \"./graphin.types\";\n\nexport * from \"./graphin.types\";\nexport type {TooltipContext} from \"./tooltip.component\";\n\nregisterCardComponent({\n name: GRAPHIN_CARD,\n component: GraphinComponent,\n});\n",
|
|
32
|
+
"type": "registry:component",
|
|
33
|
+
"target": "src/cards/graphin/index.ts"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"path": "cards/graphin/tooltip.component.tsx",
|
|
37
|
+
"content": "import {useState, useEffect} from \"react\";\nimport {useGraphin} from \"@antv/graphin\";\nimport type {IPointerEvent} from \"@antv/g6\";\nimport type {DisplayObject} from \"@antv/g\";\nimport {Target} from \"@antv/g6/lib/types\";\nimport {GraphinTooltip} from \"@/cards/graphin/graphin.types\";\nimport {Card} from \"@pihanga2/core\";\n\nexport type TooltipContext<T = Record<string, unknown>> = {\n isEdge?: boolean;\n elementID?: string;\n elementData?: T;\n};\n\ntype tooltipState = TooltipContext & {\n visible: boolean;\n x: number;\n y: number;\n};\n\nexport function TooltipComponent(props: {\n contentCards: GraphinTooltip;\n parentCard: string;\n}): React.ReactNode {\n const {contentCards, parentCard} = props;\n const {graph, isReady} = useGraphin();\n const [tooltip, setTooltip] = useState<tooltipState>({\n visible: false,\n x: 0,\n y: 0,\n });\n\n useEffect(() => {\n if (!isReady || !graph) return;\n\n // Try different event name formats\n const handleNodePointerEnter = (\n evt: IPointerEvent<DisplayObject & Target>,\n ) => {\n const nodeId = evt.target.id;\n const nodeData = graph.getNodeData(nodeId);\n\n setTooltip({\n visible: true,\n x: evt.canvas.x || evt.x,\n y: evt.canvas.y || evt.y,\n isEdge: true,\n elementID: nodeId,\n elementData: nodeData.data,\n });\n };\n\n const handleNodePointerLeave = () => {\n console.log(\"Pointer leave event\");\n setTooltip({\n visible: false,\n x: 0,\n y: 0,\n isEdge: undefined,\n elementID: undefined,\n elementData: undefined,\n });\n };\n\n // Listen to G6 events\n console.log(\">>> tooltip effect\", graph);\n graph.on(\"node:pointerenter\", handleNodePointerEnter);\n graph.on(\"node:pointerleave\", handleNodePointerLeave);\n\n return () => {\n graph.off(\"node:pointerenter\", handleNodePointerEnter);\n graph.off(\"node:pointerleave\", handleNodePointerLeave);\n };\n }, [graph, isReady]);\n\n if (!contentCards.node) return null;\n if (!tooltip.visible) return null;\n\n return (\n <div\n style={{\n position: \"absolute\",\n left: tooltip.x + 10,\n top: tooltip.y + 10,\n background: \"white\",\n padding: \"12px\",\n borderRadius: \"4px\",\n boxShadow: \"0 2px 8px rgba(0,0,0,0.15)\",\n pointerEvents: \"none\",\n zIndex: 1000,\n }}\n >\n {/* <h4 style={{margin: \"0 0 8px 0\"}}>{tooltip.elementID}</h4> */}\n <Card cardName={contentCards.node} parentCard={parentCard} {...tooltip} />\n </div>\n );\n}\n",
|
|
38
|
+
"type": "registry:component",
|
|
39
|
+
"target": "src/cards/graphin/tooltip.component.tsx"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
package/r/input.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "input",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga input card component",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"radix-ui@^1.4.3"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
|
|
11
|
+
"input",
|
|
12
|
+
"label",
|
|
13
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/form.json"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "cards/input/index.ts",
|
|
18
|
+
"content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {InputComponent} from \"./input.component\";\nimport {PI_INPUT_ACTION, PI_INPUT_CARD} from \"./input.types\";\n\nexport * from \"./input.types\";\n\nregisterCardComponent({\n name: PI_INPUT_CARD,\n component: InputComponent,\n events: actionTypesToEvents(PI_INPUT_ACTION),\n});\n",
|
|
19
|
+
"type": "registry:component",
|
|
20
|
+
"target": "src/cards/input/index.ts"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "cards/input/input.component.tsx",
|
|
24
|
+
"content": "import React from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {Input} from \"@/components/ui/input\";\nimport {Label} from \"@/components/ui/label\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport type {PiInputEvents, PiInputProps} from \"./input.types\";\n\nexport const InputComponent = (\n props: PiCardProps<PiInputProps, PiInputEvents>,\n): React.ReactNode => {\n const {\n name,\n value: propValue = \"\",\n type = \"text\",\n placeholder,\n disabled,\n label,\n description,\n className,\n cardName,\n onChanged,\n onCommitted,\n } = props;\n\n // `id` and `invalid` may be injected by a parent pi/field card via\n // Pihanga's extra-prop forwarding. They are not in the official type so\n // we read them through an escape hatch.\n const injectedId = (props as {id?: string}).id;\n const invalid = Boolean((props as {invalid?: boolean}).invalid);\n\n // Detect if we are inside a pi/form card via React context.\n const form = useFormContext();\n const useFormData = form.isInForm && Boolean(name);\n\n // Local display state — lets the user type freely without triggering Redux\n // on every keystroke. Only `onCommitted` (blur / Enter) writes back to\n // Redux, so callers that use `onCommitted` instead of `onChanged` avoid\n // per-keystroke panel rebuilds.\n const [localValue, setLocalValue] = React.useState<string>(\n type === \"file\" ? \"\" : propValue,\n );\n\n // Sync local value when the prop changes from outside (e.g. when a different\n // card is selected in the playground, resetting the controls panel).\n React.useEffect(() => {\n if (!useFormData && type !== \"file\") {\n setLocalValue(propValue);\n }\n }, [propValue, useFormData, type]);\n\n // Derive the effective value for the controlled <Input>:\n // - inside a form → read from form.formData[name]\n // - standalone → use localValue (updated on every keystroke locally)\n // - file inputs → uncontrolled (no value binding)\n const inputValue =\n type === \"file\"\n ? undefined\n : useFormData\n ? ((form.formData[name!] as string | undefined) ?? \"\")\n : localValue;\n\n function handleChange(e: React.ChangeEvent<HTMLInputElement>) {\n const newValue =\n type === \"file\" ? (e.target.files?.[0]?.name ?? \"\") : e.target.value;\n if (useFormData) {\n form.handleChange(name!, newValue);\n } else {\n setLocalValue(newValue);\n onChanged({name, value: newValue});\n }\n }\n\n // Fire onCommitted when the user leaves the field or presses Enter.\n function handleBlur() {\n if (!useFormData && type !== \"file\") {\n onCommitted({name, value: localValue});\n }\n }\n\n function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {\n if (e.key === \"Enter\" && !useFormData && type !== \"file\") {\n onCommitted({name, value: localValue});\n }\n }\n\n // Use the id injected by pi/field (for label↔control linking); fall back\n // to a locally-generated id when used standalone.\n const selfId = `${cardName}-${name ?? \"input\"}`;\n const fieldId = injectedId ?? selfId;\n\n return (\n <div data-pihanga={cardName} className=\"grid w-full gap-1.5\">\n {label && <Label htmlFor={fieldId}>{label}</Label>}\n <Input\n id={fieldId}\n type={type}\n value={inputValue}\n onChange={handleChange}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled}\n className={className}\n aria-invalid={invalid || undefined}\n aria-describedby={description ? `${fieldId}-desc` : undefined}\n />\n {description && (\n <p id={`${fieldId}-desc`} className=\"text-sm text-muted-foreground\">\n {description}\n </p>\n )}\n </div>\n );\n};\n",
|
|
25
|
+
"type": "registry:component",
|
|
26
|
+
"target": "src/cards/input/input.component.tsx"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "cards/input/input.types.ts",
|
|
30
|
+
"content": "import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_INPUT_CARD = \"pi/input\";\n\nexport const PiInput = createCardDeclaration<PiInputProps, PiInputEvents>(\n PI_INPUT_CARD,\n);\n\nexport const PI_INPUT_ACTION = registerActions(PI_INPUT_CARD, [\n \"changed\",\n \"committed\",\n]);\n\nexport const onPiInputChanged = createOnAction<PiInputChangedEvent>(\n PI_INPUT_ACTION.CHANGED,\n);\n\n/**\n * Subscribe to \"committed\" events — fired when the user finishes editing\n * (blur or Enter key). Unlike `onChanged` (which fires on every keystroke),\n * `onCommitted` only fires once per editing session, making it the preferred\n * handler for controlled inputs that trigger expensive downstream work.\n */\nexport const onPiInputCommitted = createOnAction<PiInputCommittedEvent>(\n PI_INPUT_ACTION.COMMITTED,\n);\n\n// ---------------------------------------------------------------------------\n// Props & Events\n// ---------------------------------------------------------------------------\n\nexport type PiInputProps = {\n /**\n * Field name used to bind to FormContext when inside a pi/form card.\n * When provided the component reads its value from form state and writes\n * back via form.handleChange.\n */\n name?: string;\n\n /**\n * Controlled value used in standalone mode (outside a Form).\n * Ignored when `name` is set and the component is inside a pi/form.\n */\n value?: string;\n\n /**\n * HTML input type.\n * Common values: \"text\" | \"email\" | \"password\" | \"number\" | \"search\" |\n * \"url\" | \"tel\" | \"date\" | \"file\".\n * Defaults to \"text\".\n */\n type?: string;\n\n /** Placeholder text shown when the input is empty. */\n placeholder?: string;\n\n /** When true, the input is disabled and non-interactive. */\n disabled?: boolean;\n\n /**\n * Optional label text rendered in a `<label>` element above the input.\n * The label is automatically associated with the input via `htmlFor`.\n */\n label?: string;\n\n /**\n * Optional helper / description text rendered below the input.\n * Use this for hints, formatting guidance, or validation feedback.\n */\n description?: string;\n\n /** Extra Tailwind / CSS classes forwarded to the underlying <input> element. */\n className?: string;\n};\n\nexport type PiInputChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** New value after the change. */\n value: string;\n};\n\n/**\n * Payload for the `onCommitted` event (blur / Enter key).\n * Same shape as `PiInputChangedEvent`.\n */\nexport type PiInputCommittedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** Final committed value. */\n value: string;\n};\n\nexport type PiInputEvents = {\n /** Fires on every keystroke. */\n onChanged: PiInputChangedEvent;\n /**\n * Fires once when the user finishes editing (blur or Enter key).\n * Use this instead of `onChanged` when each change triggers expensive\n * downstream work (e.g. rebuilding a panel).\n */\n onCommitted: PiInputCommittedEvent;\n};\n",
|
|
31
|
+
"type": "registry:component",
|
|
32
|
+
"target": "src/cards/input/input.types.ts"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "jsonViewer",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga jsonViewer card component",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"lucide-react@^0.513.0",
|
|
8
|
+
"react-json-view-lite@^2.5.0"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
|
|
12
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-cards-icons.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "cards/jsonViewer/index.ts",
|
|
17
|
+
"content": "import {registerCardComponent, actionTypesToEvents} from \"@pihanga2/core\";\n\nimport {JSON_VIEWER_CARD, JSON_VIEWER_ACTION} from \"./jsonViewer.types\";\nimport {JsonViewerComponent} from \"./jsonViewer.component\";\n\nexport * from \"./jsonViewer.types\";\n\nregisterCardComponent({\n name: JSON_VIEWER_CARD,\n component: JsonViewerComponent,\n events: actionTypesToEvents(JSON_VIEWER_ACTION),\n});\n",
|
|
18
|
+
"type": "registry:component",
|
|
19
|
+
"target": "src/cards/jsonViewer/index.ts"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "cards/jsonViewer/jsonViewer.component.tsx",
|
|
23
|
+
"content": "import React, {useLayoutEffect, useRef, useState, useCallback} from \"react\";\nimport {PiCardProps} from \"@pihanga2/core\";\nimport {JsonView, allExpanded, defaultStyles} from \"react-json-view-lite\";\nimport {Copy, Check} from \"lucide-react\";\nimport \"react-json-view-lite/dist/index.css\";\nimport \"./jsonViewer.css\";\n\nimport {getIconElement} from \"@/cards/icons\";\nimport type {JsonViewerProps, JsonViewerEvents} from \"./jsonViewer.types\";\n\n/**\n * Converts the `collapsed` prop (from the react-json-view API) into a\n * `shouldExpandNode` predicate for react-json-view-lite.\n *\n * - `false` → expand everything\n * - `true` → collapse everything\n * - number N → expand nodes at depth < N (root is depth 0)\n */\nfunction toShouldExpandNode(\n collapsed: boolean | number | undefined,\n): (level: number) => boolean {\n if (collapsed === false) return allExpanded;\n if (collapsed === true) return () => false;\n // default: collapsed = 1 → only root level expanded\n const depth = collapsed ?? 1;\n return (level: number) => level < depth;\n}\n\n/**\n * Compact, code-editor style for react-json-view-lite.\n *\n * The expand/collapse icon is positioned absolutely inside a fixed left gutter\n * (via padding-left on `.jv-basic-child`), so the opening `{` and closing `}`\n * always start at the same column — fixing the original misalignment where the\n * icon pushed the open-brace further right than the close-brace.\n */\nconst compactStyles = {\n // spread defaultStyles first to satisfy required fields (ariaLables, stringifyStringValues, …)\n ...defaultStyles,\n container: \"jv-container\",\n basicChildStyle: \"jv-basic-child\",\n label: \"jv-label\",\n clickableLabel: \"jv-clickable-label\",\n nullValue: \"jv-null\",\n undefinedValue: \"jv-undefined\",\n numberValue: \"jv-number\",\n stringValue: \"jv-string\",\n booleanValue: \"jv-boolean\",\n otherValue: \"jv-other\",\n punctuation: \"jv-punctuation\",\n expandIcon: \"jv-expand-icon\",\n collapseIcon: \"jv-collapse-icon\",\n collapsedContent: \"jv-collapsed-content\",\n childFieldsContainer: \"jv-child-fields-container\",\n};\n\n/**\n * Resolve the icon element to use for the copy button.\n *\n * Resolution order:\n * 1. `iconName` looked up in the global icon registry\n * 2. Lucide `Copy` as the built-in fallback\n */\nfunction resolveCopyIcon(iconName?: string): React.ElementType {\n if (iconName) {\n const registered = getIconElement(iconName);\n if (registered) return registered;\n }\n return Copy;\n}\n\n/** Write `text` to the clipboard, preferring `ClipboardItem` for explicit MIME. */\nasync function writeToClipboard(text: string): Promise<void> {\n if (typeof ClipboardItem !== \"undefined\") {\n const blob = new Blob([text], {type: \"text/plain\"});\n await navigator.clipboard.write([new ClipboardItem({\"text/plain\": blob})]);\n } else {\n // Fallback for browsers without ClipboardItem support\n await navigator.clipboard.writeText(text);\n }\n}\n\nexport const JsonViewerComponent = (\n props: PiCardProps<JsonViewerProps, JsonViewerEvents>,\n): React.ReactNode => {\n const {\n source,\n collapsed = 1,\n modifyFn,\n style,\n className,\n cardName,\n _cls,\n copyToClipboard,\n copyIcon,\n onCopied,\n } = props;\n\n const elRef = useRef<HTMLDivElement>(null);\n const [copied, setCopied] = useState(false);\n\n useLayoutEffect(() => {\n if (modifyFn) {\n modifyFn(source, elRef.current);\n }\n }, [modifyFn, source]);\n\n const shouldExpandNode = toShouldExpandNode(collapsed);\n\n const handleCopy = useCallback(async () => {\n const text = JSON.stringify(source, null, 2);\n let success = false;\n try {\n await writeToClipboard(text);\n success = true;\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.warn(\"[JsonViewer] clipboard write failed:\", err);\n }\n onCopied?.({success});\n }, [source, onCopied]);\n\n const CopyIconEl = resolveCopyIcon(copyIcon);\n const ConfirmIconEl = Check;\n\n return (\n <div\n className={[_cls(\"root\", className), \"jv-wrapper\"].join(\" \")}\n ref={elRef}\n data-pihanga={cardName}\n style={style as React.CSSProperties | undefined}\n >\n {copyToClipboard && (\n <button\n type=\"button\"\n aria-label={copied ? \"Copied!\" : \"Copy JSON to clipboard\"}\n title={copied ? \"Copied!\" : \"Copy JSON to clipboard\"}\n className={`jv-copy-btn${copied ? \" jv-copy-btn--done\" : \"\"}`}\n onClick={handleCopy}\n >\n {copied ? (\n <ConfirmIconEl size={13} strokeWidth={2.5} />\n ) : (\n <CopyIconEl size={13} strokeWidth={2} />\n )}\n </button>\n )}\n <JsonView\n data={source as object | unknown[]}\n shouldExpandNode={shouldExpandNode}\n style={compactStyles}\n clickToExpandNode\n />\n </div>\n );\n};\n",
|
|
24
|
+
"type": "registry:component",
|
|
25
|
+
"target": "src/cards/jsonViewer/jsonViewer.component.tsx"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "cards/jsonViewer/jsonViewer.css",
|
|
29
|
+
"content": "/*\n * jsonViewer.css — compact, code-editor style for react-json-view-lite\n *\n * Key design goals (matching the \"second screenshot\" style):\n * - Monospace font, light weight (no bold keys)\n * - Vertical guide lines via border-left on child containers\n * - Toggle icon is absolutely positioned so { and } align on the same column\n * - String values in green, numbers in blue, booleans in teal\n * - Adapts to light/dark via CSS variables\n */\n\n/* ── Wrapper (positions the copy button relative to the viewer) ─────────────── */\n.jv-wrapper {\n position: relative;\n}\n\n/* ── Copy-to-clipboard button ───────────────────────────────────────────────── */\n.jv-copy-btn {\n position: absolute;\n top: 4px;\n right: 4px;\n z-index: 10;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 4px;\n background: transparent;\n color: hsl(var(--muted-foreground, 0 0% 55%));\n cursor: pointer;\n opacity: 0.35;\n transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease,\n border-color 0.15s ease;\n}\n\n/* Full opacity when the wrapper is hovered/focused-within */\n.jv-wrapper:hover .jv-copy-btn,\n.jv-wrapper:focus-within .jv-copy-btn {\n opacity: 1;\n}\n\n.jv-copy-btn:hover {\n color: hsl(var(--foreground));\n background: hsl(var(--muted, 0 0% 96%));\n border-color: hsl(var(--border, 0 0% 88%));\n}\n\n/* \"Done\" state — green check mark */\n.jv-copy-btn--done {\n opacity: 1;\n color: #2e7d32;\n}\n\n.dark .jv-copy-btn--done {\n color: #81c784;\n}\n\n/* ── Container ─────────────────────────────────────────────────────────────── */\n.jv-container {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace;\n font-size: 12.5px;\n font-weight: 400;\n line-height: 1.55;\n background: transparent;\n white-space: pre-wrap;\n word-wrap: break-word;\n color: var(--foreground);\n}\n\n/* ── Each row ───────────────────────────────────────────────────────────────── */\n/*\n * position: relative + padding-left reserves a fixed gutter on the LEFT of\n * every row (both the \"key: {\" opening row AND the bare \"}\" closing row).\n * The toggle icon is then placed absolutely inside that gutter so it never\n * displaces the brace characters — fixing the original misalignment.\n */\n.jv-basic-child {\n margin: 0;\n padding: 0 0 0 1.1em;\n position: relative;\n display: block;\n}\n\n/* ── Key labels ─────────────────────────────────────────────────────────────── */\n.jv-label {\n font-weight: 400;\n color: var(--foreground);\n margin-right: 0;\n}\n\n.jv-clickable-label {\n font-weight: 400;\n color: var(--foreground);\n cursor: pointer;\n margin-right: 0;\n}\n\n.jv-clickable-label:hover {\n text-decoration: underline;\n text-decoration-style: dotted;\n}\n\n/* ── Punctuation { } [ ] : , ──────────────────────────────────────────────── */\n.jv-punctuation {\n color: var(--foreground);\n font-weight: 400;\n}\n\n/* ── Toggle icons — positioned in the left gutter ──────────────────────────── */\n.jv-expand-icon,\n.jv-collapse-icon {\n position: absolute;\n left: 0;\n /*\n * With `transform: translateY(-50%)`, `top` sets the CENTRE of the element.\n * The visual centre of the first text line = line-height / 2 = 1.55 / 2 ≈ 0.775em.\n */\n top: 0.775em;\n transform: translateY(-50%);\n width: 1em;\n font-size: 0.65em;\n line-height: 1;\n color: hsl(var(--muted-foreground, 0 0% 55%));\n user-select: none;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.jv-expand-icon::after {\n content: \"▶\";\n}\n\n.jv-collapse-icon::after {\n content: \"▼\";\n}\n\n/* ── Collapsed placeholder ──────────────────────────────────────────────────── */\n.jv-collapsed-content {\n cursor: pointer;\n color: hsl(var(--muted-foreground, 0 0% 55%));\n}\n\n.jv-collapsed-content::after {\n content: \" …\";\n}\n\n/* ── Child-fields container — the vertical guide line lives here ────────────── */\n.jv-child-fields-container {\n margin: 0;\n padding: 0;\n padding-left: 1.25em;\n border-left: 1px solid hsl(var(--border, 0 0% 88%));\n margin-left: 0.3em;\n}\n\n/* ── Value colours ──────────────────────────────────────────────────────────── */\n.jv-null {\n color: #c0392b;\n}\n\n.jv-undefined {\n color: #c0392b;\n}\n\n.jv-string {\n color: #2e7d32;\n}\n\n/* green — matches screenshot */\n.jv-number {\n color: #1565c0;\n}\n\n/* blue */\n.jv-boolean {\n color: #00796b;\n}\n\n/* teal */\n.jv-other {\n color: var(--foreground);\n}\n\n/* ── Dark-mode overrides ───────────────────────────────────────────────────── */\n.dark .jv-string {\n color: #81c784;\n}\n\n.dark .jv-number {\n color: #64b5f6;\n}\n\n.dark .jv-boolean {\n color: #4db6ac;\n}\n\n.dark .jv-null,\n.dark .jv-undefined {\n color: #ef9a9a;\n}\n\n.dark .jv-child-fields-container {\n border-left-color: hsl(var(--border, 0 0% 25%));\n}",
|
|
30
|
+
"type": "registry:component",
|
|
31
|
+
"target": "src/cards/jsonViewer/jsonViewer.css"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"path": "cards/jsonViewer/jsonViewer.types.ts",
|
|
35
|
+
"content": "import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const JSON_VIEWER_CARD = \"json-viewer\";\n\nexport const JsonViewer = createCardDeclaration<\n JsonViewerProps,\n JsonViewerEvents\n>(JSON_VIEWER_CARD);\n\nexport const JSON_VIEWER_ACTION = registerActions(JSON_VIEWER_CARD, [\n \"clicked\",\n \"copied\",\n]);\n\nexport const onJsonViewerClicked = createOnAction<ClickedEvent>(\n JSON_VIEWER_ACTION.CLICKED,\n);\n\nexport const onJsonViewerCopied = createOnAction<CopiedEvent>(\n JSON_VIEWER_ACTION.COPIED,\n);\n\nexport type JsonViewerProps<S = object> = {\n /** The JSON value to display. */\n source: unknown;\n /**\n * Theme name. Maps loosely to a visual style.\n * Use \"monokai\", \"solarized\", \"ocean\", etc. (visual only — actual theming\n * is done via className / global CSS since react-json-view-lite uses CSS\n * variables rather than named themes).\n */\n theme?: string;\n iconStyle?: \"circle\" | \"triangle\" | \"square\";\n /**\n * When `true`, all nodes are collapsed.\n * When `false`, all nodes are expanded.\n * When a number N, nodes at depth >= N are collapsed (root is depth 0).\n * Default: 1 (expand root only).\n */\n collapsed?: boolean | number;\n /** When `true`, a click-to-expand interaction is enabled on node labels. */\n enableClipboard?: boolean;\n /** When `true`, removes quotes from object keys in display. */\n removeQuotesOnKeys?: boolean;\n /** When `true`, objects and arrays are labelled with their size. */\n displayObjectSize?: boolean;\n /** When `true`, data-type labels prefix values. */\n displayDataTypes?: boolean;\n /**\n * Called after the JSON is rendered, allowing post-render DOM modifications.\n * Receives the raw `source` value and the container element.\n */\n modifyFn?: ModifyFn;\n\n /**\n * When `true`, shows a copy-to-clipboard icon button in the top-right\n * corner of the viewer. Clicking it copies the pretty-printed JSON to the\n * clipboard using `ClipboardItem` with `text/plain` MIME type.\n */\n copyToClipboard?: boolean;\n\n /**\n * Icon name resolved via the global icon registry (`getIconElement`).\n * If omitted or the name is not registered, falls back to the Lucide\n * `Copy` icon automatically.\n *\n * @example\n * // Register your own icon first:\n * registerIcon(\"my-copy\", MyCustomCopyIcon);\n * // Then pass the name to the card:\n * JsonViewer({ copyToClipboard: true, copyIcon: \"my-copy\", ... })\n */\n copyIcon?: string;\n\n /** Inline style for the react-json-view-lite container. */\n style?: S;\n /** Additional CSS class for the outer wrapper. */\n className?: string;\n};\n\nexport type ModifyFn = (source: unknown, el: HTMLElement | null) => void;\n\nexport type ClickedEvent = Record<string, never>;\n\nexport type CopiedEvent = {\n /** `true` when the clipboard write succeeded, `false` on error. */\n success: boolean;\n};\n\nexport type JsonViewerEvents = {\n onClicked: ClickedEvent;\n onCopied: CopiedEvent;\n};\n",
|
|
36
|
+
"type": "registry:component",
|
|
37
|
+
"target": "src/cards/jsonViewer/jsonViewer.types.ts"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
package/r/list.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "list",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"description": "Pihanga list card component",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@radix-ui/react-slot@^1.2.4",
|
|
8
|
+
"class-variance-authority@^0.7.1",
|
|
9
|
+
"lucide-react@^0.513.0",
|
|
10
|
+
"radix-ui@^1.4.3"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
|
|
14
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-lib-utils.json",
|
|
15
|
+
"avatar",
|
|
16
|
+
"badge",
|
|
17
|
+
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-cards-icons.json"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
{
|
|
21
|
+
"path": "cards/list/index.ts",
|
|
22
|
+
"content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {ListComponent} from \"./list.component\";\nimport {LIST_ACTION, LIST_CARD} from \"./list.types\";\n\nexport * from \"./list.types\";\n\nregisterCardComponent({\n name: LIST_CARD,\n component: ListComponent,\n events: actionTypesToEvents(LIST_ACTION),\n});\n",
|
|
23
|
+
"type": "registry:component",
|
|
24
|
+
"target": "src/cards/list/index.ts"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "cards/list/list.component.tsx",
|
|
28
|
+
"content": "import * as React from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport {ChevronDown} from \"lucide-react\";\nimport {cn} from \"@/lib/utils\";\nimport {Avatar, AvatarFallback, AvatarImage} from \"@/components/ui/avatar\";\nimport {Badge} from \"@/components/ui/badge\";\nimport {getIcon} from \"@/cards/icons\";\nimport type {DecoratorT, ListEvents, ListItem, ListProps} from \"./list.types\";\n\n// ---------------------------------------------------------------------------\n// Size tokens\n// ---------------------------------------------------------------------------\n\n/** Padding + font-size + gap for item buttons */\nconst ITEM_SIZE: Record<\"sm\" | \"md\" | \"lg\", string> = {\n sm: \"px-2 py-1 text-xs gap-1.5\",\n md: \"px-2 py-1.5 text-sm gap-2\",\n lg: \"px-3 py-2 text-base gap-2.5\",\n};\n\n/** Sub-title font-size (one step smaller than title) */\nconst SUB_SIZE: Record<\"sm\" | \"md\" | \"lg\", string> = {\n sm: \"text-xs\",\n md: \"text-xs\",\n lg: \"text-sm\",\n};\n\n/** Avatar size per density */\nconst AVATAR_SIZE: Record<\"sm\" | \"md\" | \"lg\", string> = {\n sm: \"size-5\",\n md: \"size-6\",\n lg: \"size-7\",\n};\n\n// ---------------------------------------------------------------------------\n// Decorator renderer\n// ---------------------------------------------------------------------------\n\nfunction renderDecorator(\n decorator: DecoratorT | undefined,\n size: \"sm\" | \"md\" | \"lg\",\n parentCard: string,\n): React.ReactNode {\n if (!decorator) return null;\n\n switch (decorator.type) {\n case \"icon\":\n return (\n <span\n className={cn(\n \"flex shrink-0 items-center [&>svg]:size-4\",\n decorator.className,\n )}\n >\n {getIcon(decorator.name)}\n </span>\n );\n\n case \"avatar\":\n return (\n <Avatar className={cn(AVATAR_SIZE[size], decorator.className)}>\n <AvatarImage src={decorator.src} />\n <AvatarFallback>{decorator.fallback ?? \"\"}</AvatarFallback>\n </Avatar>\n );\n\n case \"chip\":\n return (\n <Badge variant=\"secondary\" className={decorator.className}>\n {decorator.text}\n </Badge>\n );\n\n case \"card\":\n return <Card cardName={decorator.cardName} parentCard={parentCard} />;\n\n default:\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Single item row (recursive for nested)\n// ---------------------------------------------------------------------------\n\ntype ItemRowProps = {\n item: ListItem;\n size: \"sm\" | \"md\" | \"lg\";\n onItemClicked: (ev: {itemID: string | number}) => void;\n cardName: string;\n};\n\nfunction ItemRow({\n item,\n size,\n onItemClicked,\n cardName,\n}: ItemRowProps): React.ReactNode {\n const [open, setOpen] = React.useState(false);\n const hasNested = Boolean(item.nested?.length);\n\n function handleClick() {\n if (hasNested) {\n setOpen((prev) => !prev);\n } else {\n onItemClicked({itemID: item.id});\n }\n }\n\n return (\n <li>\n {/* Item button ---------------------------------------------------- */}\n <button\n type=\"button\"\n className={cn(\n \"flex w-full items-center rounded-sm outline-none transition-colors\",\n \"hover:bg-accent hover:text-accent-foreground\",\n \"focus-visible:ring-1 focus-visible:ring-ring\",\n item.isSelected && \"bg-accent text-accent-foreground font-medium\",\n ITEM_SIZE[size],\n item.className,\n )}\n onClick={handleClick}\n aria-expanded={hasNested ? open : undefined}\n data-selected={item.isSelected || undefined}\n >\n {/* Start decorator */}\n {renderDecorator(item.startDecorator, size, cardName)}\n\n {/* Title + subtitle */}\n <span className=\"flex min-w-0 flex-1 flex-col items-start\">\n <span className=\"truncate leading-none\">{item.title}</span>\n {item.subTitle && (\n <span\n className={cn(\n \"mt-0.5 truncate text-muted-foreground\",\n SUB_SIZE[size],\n )}\n >\n {item.subTitle}\n </span>\n )}\n </span>\n\n {/* End decorator (suppress if we're showing the chevron) */}\n {!hasNested && renderDecorator(item.endDecorator, size, cardName)}\n\n {/* Expand/collapse chevron for nested items */}\n {hasNested && (\n <ChevronDown\n className={cn(\n \"ml-auto size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n open && \"rotate-180\",\n )}\n />\n )}\n </button>\n\n {/* Nested list ---------------------------------------------------- */}\n {hasNested && open && (\n <ul\n className={cn(\n \"ml-3 mt-0.5 flex flex-col gap-0.5 border-l border-border pl-2\",\n \"list-none p-0\",\n // keep the indent aligned regardless of density\n size === \"lg\" ? \"ml-4 pl-3\" : \"ml-3 pl-2\",\n )}\n >\n {item.nested!.map((child, idx) => (\n <ItemRow\n key={child.id ?? idx}\n item={child}\n size={size}\n onItemClicked={onItemClicked}\n cardName={cardName}\n />\n ))}\n </ul>\n )}\n </li>\n );\n}\n\n// ---------------------------------------------------------------------------\n// List card component\n// ---------------------------------------------------------------------------\n\nexport const ListComponent = (\n props: PiCardProps<ListProps, ListEvents>,\n): React.ReactNode => {\n const {\n items,\n size = \"md\",\n marker,\n className,\n style,\n onItemClicked,\n cardName,\n } = props;\n\n // When a marker is requested, expose it via CSS custom property so the\n // list-style-type cascade works without a wrapper element.\n const rootStyle: React.CSSProperties = {\n ...style,\n ...(marker && marker !== \"none\" ? {listStyleType: marker} : undefined),\n };\n\n return (\n <ul\n className={cn(\n \"flex flex-col gap-0.5 p-0\",\n marker && marker !== \"none\" ? \"list-inside\" : \"list-none\",\n className,\n )}\n style={rootStyle}\n data-pihanga={cardName}\n >\n {items.map((item, idx) => (\n <ItemRow\n key={item.id ?? idx}\n item={item}\n size={size}\n onItemClicked={onItemClicked}\n cardName={cardName}\n />\n ))}\n </ul>\n );\n};\n",
|
|
29
|
+
"type": "registry:component",
|
|
30
|
+
"target": "src/cards/list/list.component.tsx"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "cards/list/list.types.ts",
|
|
34
|
+
"content": "import * as React from \"react\";\nimport {\n PiCardRef,\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const LIST_CARD = \"shad/list\";\n\nexport const List = createCardDeclaration<ListProps, ListEvents>(LIST_CARD);\n\nexport const LIST_ACTION = registerActions(LIST_CARD, [\"item_clicked\"]);\n\nexport const onListItemClicked = createOnAction<ItemClickedEvent>(\n LIST_ACTION.ITEM_CLICKED,\n);\n\n// Decorator shown before or after an item's title/subtitle block.\nexport type IconDecorator = {\n type: \"icon\";\n /** Name registered via registerIcon() in src/cards/icons.ts */\n name: string;\n className?: string;\n};\n\nexport type AvatarDecorator = {\n type: \"avatar\";\n src: string;\n fallback?: string;\n className?: string;\n};\n\nexport type ChipDecorator = {\n type: \"chip\";\n text: string;\n className?: string;\n};\n\nexport type CardDecorator = {\n type: \"card\";\n cardName: PiCardRef;\n};\n\nexport type DecoratorT =\n | IconDecorator\n | AvatarDecorator\n | ChipDecorator\n | CardDecorator;\n\nexport type ListItem = {\n /** React key — must be unique within the list. */\n id: string | number;\n isSelected?: boolean;\n title: string;\n subTitle?: string;\n /** When present the item becomes a collapsible parent. */\n nested?: ListItem[];\n startDecorator?: DecoratorT;\n endDecorator?: DecoratorT;\n /** Extra CSS classes on this item's button. */\n className?: string;\n};\n\nexport type ListProps = {\n items: ListItem[];\n /** Visual density — defaults to \"md\". */\n size?: \"sm\" | \"md\" | \"lg\";\n /**\n * CSS `list-style-type` value (e.g. \"disc\", \"decimal\", \"none\").\n * Defaults to \"none\" (plain list without markers).\n */\n marker?: string;\n /** CSS classes applied directly to the root `<ul>`. */\n className?: string;\n /** Inline styles applied directly to the root `<ul>`. */\n style?: React.CSSProperties;\n};\n\nexport type ItemClickedEvent = {\n itemID: string | number;\n};\n\nexport type ListEvents = {\n onItemClicked: ItemClickedEvent;\n};\n",
|
|
35
|
+
"type": "registry:component",
|
|
36
|
+
"target": "src/cards/list/list.types.ts"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|