@pihanga2/shadcn 0.1.0 → 0.2.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/AGENT.building-cards.md +413 -0
- package/AGENT.md +50 -0
- package/AGENT.using-cards.md +1242 -0
- package/cards/box/box.types.d.ts +1 -4
- package/cards/box/box.types.js.map +1 -1
- package/cards/core-index.js +6 -6
- package/cards/fileDrop/fileDrop.component.d.ts +5 -0
- package/cards/fileDrop/fileDrop.types.d.ts +43 -0
- package/cards/fileDrop/index.d.ts +2 -0
- package/cards/icons.js +7 -3
- package/cards/icons.js.map +1 -1
- package/cards/index.d.ts +1 -0
- package/cards/stack/stack.types.d.ts +19 -22
- package/cards/stack/stack.types.js +1 -1
- package/cards/stack/stack.types.js.map +1 -1
- package/cards/tabs/index.js +4 -4
- package/cards/tabs/tabs.types.d.ts +2 -0
- package/cards/tabs/tabs.types.js +2 -2
- package/cards/tabs/tabs.types.js.map +1 -1
- package/cards/types.d.ts +4 -0
- package/cards/types.js.map +1 -1
- package/package.json +9 -1
- package/pihanga-shadcn.css +1 -1
- package/mountain-snow.svg +0 -4
- package/r/badge.json +0 -35
- package/r/box.json +0 -26
- package/r/button.json +0 -39
- package/r/checkbox.json +0 -31
- package/r/conditional.json +0 -30
- package/r/dataTable.json +0 -43
- package/r/dialog.json +0 -36
- package/r/dropDownMenu.json +0 -47
- package/r/field.json +0 -35
- package/r/flexGrid.json +0 -32
- package/r/form.json +0 -37
- package/r/framework.json +0 -28
- package/r/graphin.json +0 -42
- package/r/input.json +0 -35
- package/r/jsonViewer.json +0 -40
- package/r/list.json +0 -39
- package/r/loadingOverlay.json +0 -39
- package/r/loadingSkeleton.json +0 -30
- package/r/markdownViewer.json +0 -51
- package/r/menu.json +0 -34
- package/r/modeToggle.json +0 -36
- package/r/navbarSearch.json +0 -31
- package/r/pageWithNavbar.json +0 -46
- package/r/pasteTarget.json +0 -41
- package/r/pihanga-base.json +0 -11
- package/r/pihanga-cards-icons.json +0 -16
- package/r/pihanga-cards-types.json +0 -16
- package/r/pihanga-hook-use-is-touch-device.json +0 -15
- package/r/pihanga-lib-utils.json +0 -18
- package/r/pihanga-theme-provider.json +0 -25
- package/r/pihanga-ui-extras.json +0 -68
- package/r/registry.json +0 -171
- package/r/resizable.json +0 -35
- package/r/select.json +0 -35
- package/r/stack.json +0 -27
- package/r/stepper.json +0 -39
- package/r/switch.json +0 -36
- package/r/tabs.json +0 -33
- package/r/textField.json +0 -32
- package/r/toast.json +0 -32
- package/r/toggleGroup.json +0 -35
- package/r/typography.json +0 -31
package/r/conditional.json
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
-
"name": "conditional",
|
|
4
|
-
"type": "registry:component",
|
|
5
|
-
"description": "Pihanga conditional card component",
|
|
6
|
-
"dependencies": [],
|
|
7
|
-
"registryDependencies": [
|
|
8
|
-
"https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json"
|
|
9
|
-
],
|
|
10
|
-
"files": [
|
|
11
|
-
{
|
|
12
|
-
"path": "cards/conditional/conditional.component.tsx",
|
|
13
|
-
"content": "import * as React from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport type {ConditionalProps} from \"./conditional.types\";\n\n/**\n * ConditionalComponent\n *\n * Transparent pass-through: renders the `content` card when `show` is `true`,\n * returns `null` otherwise. No extra DOM wrapper is added — the mounted\n * card's own root element is the only node in the tree.\n */\nexport const ConditionalComponent = (\n props: PiCardProps<ConditionalProps>,\n): React.ReactNode => {\n const {cardName, show, content} = props;\n if (!show) return null;\n return <Card cardName={content} parentCard={cardName} />;\n};\n",
|
|
14
|
-
"type": "registry:component",
|
|
15
|
-
"target": "src/cards/conditional/conditional.component.tsx"
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"path": "cards/conditional/conditional.types.ts",
|
|
19
|
-
"content": "import {createCardDeclaration} from \"@pihanga2/core\";\nimport type {PiCardRef} from \"@pihanga2/core\";\n\n// ── Card id ───────────────────────────────────────────────────────────────────\n\nexport const CONDITIONAL_CARD = \"shad/conditional\";\n\n// ── Card declaration factory ──────────────────────────────────────────────────\n\nexport const Conditional =\n createCardDeclaration<ConditionalProps>(CONDITIONAL_CARD);\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\n/**\n * Props for the `shad/conditional` card.\n *\n * Renders `content` only when `show` is `true`; renders nothing otherwise.\n * This is a transparent pass-through — no extra DOM wrapper is added.\n *\n * In a real app, drive `show` from a `memo()` selector so the card\n * reactively mounts/unmounts as state changes:\n *\n * ```ts\n * import {memo, registerCard} from \"@pihanga2/core\";\n * import {Conditional} from \"@/cards/conditional\";\n *\n * registerCard(\"myApp/hint\", Conditional({\n * show: memo((s: AppState) => s.items.length === 0 && !s.isLoading),\n * content: \"myApp/emptyStateHint\",\n * }));\n * ```\n */\nexport type ConditionalProps = {\n /** Render `content` only when this is `true`. Drive with `memo()` for\n * reactive mount/unmount behaviour. */\n show: boolean;\n\n /** The card to render when `show` is `true`. */\n content: PiCardRef;\n};\n",
|
|
20
|
-
"type": "registry:component",
|
|
21
|
-
"target": "src/cards/conditional/conditional.types.ts"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"path": "cards/conditional/index.ts",
|
|
25
|
-
"content": "import {registerCardComponent} from \"@pihanga2/core\";\nimport {ConditionalComponent} from \"./conditional.component\";\nimport {CONDITIONAL_CARD} from \"./conditional.types\";\n\nexport * from \"./conditional.types\";\n\n// No events — this card is purely structural.\nregisterCardComponent({\n name: CONDITIONAL_CARD,\n component: ConditionalComponent,\n});\n",
|
|
26
|
-
"type": "registry:component",
|
|
27
|
-
"target": "src/cards/conditional/index.ts"
|
|
28
|
-
}
|
|
29
|
-
]
|
|
30
|
-
}
|
package/r/dataTable.json
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
-
"name": "dataTable",
|
|
4
|
-
"type": "registry:component",
|
|
5
|
-
"description": "Pihanga dataTable 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
|
-
],
|
|
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
|
-
"badge",
|
|
15
|
-
"table"
|
|
16
|
-
],
|
|
17
|
-
"files": [
|
|
18
|
-
{
|
|
19
|
-
"path": "cards/dataTable/dataTable.component.tsx",
|
|
20
|
-
"content": "import React, {useMemo, useState} from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport {\n Check,\n ChevronDown,\n ChevronRight,\n ChevronUp,\n ChevronsUpDown,\n X,\n} from \"lucide-react\";\nimport {cn} from \"@/lib/utils\";\nimport {Badge} from \"@/components/ui/badge\";\nimport {\n Table,\n TableBody,\n TableCaption,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport type {\n DataTableColumn,\n DataTableEvents,\n DataTableProps,\n DataTableRow as DataTableRowType,\n} from \"./dataTable.types\";\n\n// ---------------------------------------------------------------------------\n// Cell renderer\n// ---------------------------------------------------------------------------\n\nfunction renderCell(\n column: DataTableColumn,\n value: unknown,\n parentCard: string,\n): React.ReactNode {\n if (value == null) return null;\n\n switch (column.type) {\n case \"number\": {\n const num = typeof value === \"number\" ? value : Number(value);\n return isNaN(num)\n ? String(value)\n : column.format\n ? column.format(num)\n : String(num);\n }\n\n case \"date\": {\n if (column.format) {\n return column.format(value as Date | string);\n }\n const d = value instanceof Date ? value : new Date(String(value));\n return isNaN(d.getTime()) ? String(value) : d.toLocaleDateString();\n }\n\n case \"badge\": {\n const strVal = String(value);\n const variant = column.variants?.[strVal] ?? \"secondary\";\n return <Badge variant={variant}>{strVal}</Badge>;\n }\n\n case \"boolean\": {\n return value ? (\n <Check className=\"size-4 text-green-600\" aria-label=\"true\" />\n ) : (\n <X className=\"size-4 text-muted-foreground\" aria-label=\"false\" />\n );\n }\n\n case \"card\": {\n // Cell value is expected to be a PiCardRef (string)\n return <Card cardName={String(value)} parentCard={parentCard} />;\n }\n\n default:\n // \"text\" or omitted type\n return String(value);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Sort icon\n// ---------------------------------------------------------------------------\n\nfunction SortIcon({\n columnKey,\n activeKey,\n ascending,\n}: {\n columnKey: string;\n activeKey: string | undefined;\n ascending: boolean;\n}): React.ReactNode {\n if (columnKey !== activeKey) {\n return <ChevronsUpDown className=\"ml-1 inline size-3.5 opacity-40\" />;\n }\n return ascending ? (\n <ChevronUp className=\"ml-1 inline size-3.5\" />\n ) : (\n <ChevronDown className=\"ml-1 inline size-3.5\" />\n );\n}\n\n// ---------------------------------------------------------------------------\n// Pagination controls\n// ---------------------------------------------------------------------------\n\nfunction PaginationBar({\n page,\n totalPages,\n onPrev,\n onNext,\n}: {\n page: number;\n totalPages: number;\n onPrev: () => void;\n onNext: () => void;\n}): React.ReactNode {\n return (\n <div className=\"flex items-center justify-end gap-2 mt-2 px-2\">\n <span className=\"text-sm text-muted-foreground\">\n Page {page + 1} of {totalPages}\n </span>\n <button\n type=\"button\"\n className={cn(\n \"rounded border px-2 py-1 text-sm transition-colors\",\n \"hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40\",\n )}\n disabled={page === 0}\n onClick={onPrev}\n >\n Previous\n </button>\n <button\n type=\"button\"\n className={cn(\n \"rounded border px-2 py-1 text-sm transition-colors\",\n \"hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40\",\n )}\n disabled={page >= totalPages - 1}\n onClick={onNext}\n >\n Next\n </button>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Helper: derive a human-readable title from a camelCase key\n// ---------------------------------------------------------------------------\n\nfunction keyToTitle(key: string): string {\n return key\n .replace(/([A-Z])/g, \" $1\")\n .replace(/^./, (s) => s.toUpperCase())\n .trim();\n}\n\n// ---------------------------------------------------------------------------\n// Main data-table component\n// ---------------------------------------------------------------------------\n\nexport const DataTableComponent = (\n props: PiCardProps<DataTableProps, DataTableEvents>,\n): React.ReactNode => {\n const {\n columns,\n rows,\n caption,\n sortKey: sortKeyProp,\n sortAscending: sortAscendingProp,\n pageSize,\n currentPage: currentPageProp,\n stickyHeader = false,\n striped = false,\n hoverable = true,\n compact = false,\n emptyCard,\n emptyText = \"No data\",\n className,\n cardName,\n onRowClicked,\n onSortChanged,\n onShowDetail,\n onHideDetail,\n onPageChanged,\n } = props;\n\n // ── Expanded-rows state ──────────────────────────────────────────────────\n const [expandedRows, setExpandedRows] = useState<Set<string | number>>(\n new Set(),\n );\n\n // ── Sort state (uncontrolled; props override when present) ───────────────\n const [localSortKey, setLocalSortKey] = useState<string | undefined>(\n sortKeyProp,\n );\n const [localSortAsc, setLocalSortAsc] = useState<boolean>(\n sortAscendingProp ?? true,\n );\n const activeSortKey = sortKeyProp !== undefined ? sortKeyProp : localSortKey;\n const activeSortAsc =\n sortAscendingProp !== undefined ? sortAscendingProp : localSortAsc;\n\n // ── Pagination state (uncontrolled; props override when present) ─────────\n const [localPage, setLocalPage] = useState<number>(currentPageProp ?? 0);\n const activePage =\n currentPageProp !== undefined ? currentPageProp : localPage;\n\n // ── Derived flags ────────────────────────────────────────────────────────\n const hasDetailRows = rows.some((row) => row.detailCard != null);\n // Total column count used for detail-row colSpan\n const colCount = columns.length + (hasDetailRows ? 1 : 0);\n\n // ── Sorted rows ──────────────────────────────────────────────────────────\n const sortedRows = useMemo(() => {\n if (!activeSortKey) return rows;\n return [...rows].sort((a, b) => {\n const av = (a.data as Record<string, unknown>)[activeSortKey];\n const bv = (b.data as Record<string, unknown>)[activeSortKey];\n if (av == null && bv == null) return 0;\n if (av == null) return 1;\n if (bv == null) return -1;\n const cmp = av < bv ? -1 : av > bv ? 1 : 0;\n return activeSortAsc ? cmp : -cmp;\n });\n }, [rows, activeSortKey, activeSortAsc]);\n\n // ── Paginated slice ──────────────────────────────────────────────────────\n const paginatedRows = useMemo(() => {\n if (!pageSize) return sortedRows;\n const start = activePage * pageSize;\n return sortedRows.slice(start, start + pageSize);\n }, [sortedRows, pageSize, activePage]);\n\n const totalPages = pageSize ? Math.ceil(sortedRows.length / pageSize) : 1;\n\n // ── Handlers ─────────────────────────────────────────────────────────────\n const handleSortClick = (key: string) => {\n const newAsc = activeSortKey === key ? !activeSortAsc : true;\n setLocalSortKey(key);\n setLocalSortAsc(newAsc);\n onSortChanged({key, ascending: newAsc});\n };\n\n const handleToggleDetail = (row: DataTableRowType, e: React.MouseEvent) => {\n e.stopPropagation();\n setExpandedRows((prev) => {\n const next = new Set(prev);\n if (next.has(row.id)) {\n next.delete(row.id);\n onHideDetail({rowId: row.id, row});\n } else {\n next.add(row.id);\n onShowDetail({rowId: row.id, row});\n }\n return next;\n });\n };\n\n const handleRowClick = (row: DataTableRowType) => {\n onRowClicked({rowId: row.id, row});\n };\n\n const handlePageChange = (page: number) => {\n setLocalPage(page);\n if (pageSize) {\n onPageChanged({page, pageSize});\n }\n };\n\n // ── Cell padding ─────────────────────────────────────────────────────────\n const cellPad = compact ? \"py-1 px-2\" : \"py-2 px-3\";\n\n // ── Render ────────────────────────────────────────────────────────────────\n return (\n <div className={cn(\"w-full\", className)} data-pihanga={cardName}>\n <Table>\n {caption && <TableCaption>{caption}</TableCaption>}\n\n {/* ── Header ─────────────────────────────────────────────────────── */}\n <TableHeader\n className={\n stickyHeader ? \"sticky top-0 z-10 bg-background\" : undefined\n }\n >\n <TableRow>\n {/* Expand-toggle placeholder header cell */}\n {hasDetailRows && (\n <TableHead className=\"w-10 px-2\" aria-label=\"Row details\" />\n )}\n\n {columns.map((col) => (\n <TableHead\n key={col.key}\n style={col.width ? {width: col.width} : undefined}\n className={cn(\n cellPad,\n col.align === \"center\" && \"text-center\",\n col.align === \"right\" && \"text-right\",\n col.sortable && \"cursor-pointer select-none\",\n col.headerClassName,\n )}\n onClick={\n col.sortable ? () => handleSortClick(col.key) : undefined\n }\n >\n <span className=\"inline-flex items-center\">\n {col.title ?? keyToTitle(col.key)}\n {col.sortable && (\n <SortIcon\n columnKey={col.key}\n activeKey={activeSortKey}\n ascending={activeSortAsc}\n />\n )}\n </span>\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n\n {/* ── Body ───────────────────────────────────────────────────────── */}\n <TableBody>\n {paginatedRows.length === 0 ? (\n /* Empty state */\n <TableRow>\n <TableCell\n colSpan={colCount}\n className=\"py-8 text-center text-muted-foreground\"\n >\n {emptyCard ? (\n <Card cardName={emptyCard} parentCard={cardName} />\n ) : (\n emptyText\n )}\n </TableCell>\n </TableRow>\n ) : (\n paginatedRows.map((row, rowIdx) => {\n const isExpanded = expandedRows.has(row.id);\n const isStriped = striped && rowIdx % 2 === 1;\n\n return (\n <React.Fragment key={row.id}>\n {/* ── Data row ─────────────────────────────────────────── */}\n <TableRow\n className={cn(\n isStriped && \"bg-muted/30\",\n hoverable && \"cursor-pointer\",\n )}\n data-expanded={isExpanded || undefined}\n onClick={() => handleRowClick(row)}\n >\n {/* Expand / collapse toggle */}\n {hasDetailRows && (\n <TableCell className=\"w-10 px-2\">\n {row.detailCard && (\n <button\n type=\"button\"\n aria-label={\n isExpanded ? \"Collapse detail\" : \"Expand detail\"\n }\n aria-expanded={isExpanded}\n className={cn(\n \"flex items-center justify-center rounded p-0.5 transition-colors\",\n \"hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n )}\n onClick={(e) => handleToggleDetail(row, e)}\n >\n {isExpanded ? (\n <ChevronDown className=\"size-4\" />\n ) : (\n <ChevronRight className=\"size-4\" />\n )}\n </button>\n )}\n </TableCell>\n )}\n\n {/* Data cells */}\n {columns.map((col) => {\n const value = (row.data as Record<string, unknown>)[\n col.key\n ];\n return (\n <TableCell\n key={col.key}\n className={cn(\n cellPad,\n col.align === \"center\" && \"text-center\",\n col.align === \"right\" && \"text-right\",\n col.cellClassName,\n )}\n >\n {renderCell(col, value, cardName)}\n </TableCell>\n );\n })}\n </TableRow>\n\n {/* ── Detail row (full-width, shown when expanded) ──────── */}\n {isExpanded && row.detailCard && (\n <TableRow\n // Suppress the default hover/click styles on the detail row\n className=\"hover:bg-transparent\"\n onClick={(e) => e.stopPropagation()}\n >\n <TableCell colSpan={colCount} className=\"border-b p-0\">\n <div className=\"bg-muted/20 px-4 py-3\">\n {/*\n * cardKey creates a unique virtual card instance per row\n * (e.g. \"myDetailTemplate@detail-42\").\n * The `row` prop is forwarded as ctxtProps so the detail\n * card's state-mapper functions can access it via\n * `ctx.ctxtProps.row` — no need to pre-register one card\n * per row.\n */}\n <Card\n cardName={row.detailCard}\n cardKey={`detail-${row.id}`}\n row={row}\n parentCard={cardName}\n key={`${cardName}-${row.id}-detail`}\n />\n </div>\n </TableCell>\n </TableRow>\n )}\n </React.Fragment>\n );\n })\n )}\n </TableBody>\n </Table>\n\n {/* ── Pagination controls ──────────────────────────────────────────── */}\n {pageSize && totalPages > 1 && (\n <PaginationBar\n page={activePage}\n totalPages={totalPages}\n onPrev={() => handlePageChange(activePage - 1)}\n onNext={() => handlePageChange(activePage + 1)}\n />\n )}\n </div>\n );\n};\n",
|
|
21
|
-
"type": "registry:component",
|
|
22
|
-
"target": "src/cards/dataTable/dataTable.component.tsx"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"path": "cards/dataTable/dataTable.types.ts",
|
|
26
|
-
"content": "import {\n PiCardRef,\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const DATA_TABLE_CARD = \"shad/data-table\";\n\nexport const DataTable = createCardDeclaration<DataTableProps, DataTableEvents>(\n DATA_TABLE_CARD,\n);\n\nexport const DATA_TABLE_ACTION = registerActions(DATA_TABLE_CARD, [\n \"row_clicked\",\n \"sort_changed\",\n \"show_detail\",\n \"hide_detail\",\n \"page_changed\",\n]);\n\nexport const onDataTableRowClicked = createOnAction<DataTableRowClickedEvent>(\n DATA_TABLE_ACTION.ROW_CLICKED,\n);\nexport const onDataTableSortChanged = createOnAction<DataTableSortChangedEvent>(\n DATA_TABLE_ACTION.SORT_CHANGED,\n);\nexport const onDataTableShowDetail = createOnAction<DataTableDetailEvent>(\n DATA_TABLE_ACTION.SHOW_DETAIL,\n);\nexport const onDataTableHideDetail = createOnAction<DataTableDetailEvent>(\n DATA_TABLE_ACTION.HIDE_DETAIL,\n);\nexport const onDataTablePageChanged = createOnAction<DataTablePageChangedEvent>(\n DATA_TABLE_ACTION.PAGE_CHANGED,\n);\n\n// ---------------------------------------------------------------------------\n// Column type definitions\n// ---------------------------------------------------------------------------\n\n/** Shared base for all column definitions */\nexport type DataTableColumnBase = {\n /** Key into the row's `data` object used to retrieve the cell value */\n key: string;\n /**\n * Column header label.\n * Defaults to the `key` with camelCase expanded to \"Title Case\".\n */\n title?: string;\n /** Column width as a CSS value (e.g. \"120px\", \"10%\", \"1fr\") */\n width?: string | number;\n /** Horizontal alignment of cell content. Defaults to \"left\". */\n align?: \"left\" | \"center\" | \"right\";\n /** Whether the column header is clickable for sorting */\n sortable?: boolean;\n /** Extra CSS class applied to the `<th>` header cell */\n headerClassName?: string;\n /** Extra CSS class applied to every `<td>` data cell in this column */\n cellClassName?: string;\n};\n\n/** Plain text column — cell value rendered as a string */\nexport type TextColumn = DataTableColumnBase & {\n type?: \"text\";\n};\n\n/** Numeric column with optional custom formatter */\nexport type NumberColumn = DataTableColumnBase & {\n type: \"number\";\n /** Custom number formatter; defaults to `String(value)` */\n format?: (n: number) => string;\n};\n\n/** Badge/chip column — cell value is rendered inside a `<Badge>` */\nexport type BadgeColumn = DataTableColumnBase & {\n type: \"badge\";\n /**\n * Map from cell value (as a string) to a shadcn Badge variant.\n * Falls back to \"secondary\" for unrecognised values.\n */\n variants?: Record<\n string,\n \"default\" | \"secondary\" | \"destructive\" | \"outline\"\n >;\n};\n\n/** Date column with optional custom formatter */\nexport type DateColumn = DataTableColumnBase & {\n type: \"date\";\n /** Custom date formatter; defaults to `toLocaleDateString()` */\n format?: (d: Date | string) => string;\n};\n\n/** Boolean column — renders a check-mark or cross icon */\nexport type BooleanColumn = DataTableColumnBase & {\n type: \"boolean\";\n};\n\n/**\n * Card column — cell value must be a `PiCardRef` (string).\n * The referenced card is rendered inline via Pihanga's `<Card />`.\n * This enables full reuse of any registered Pihanga card as cell content.\n */\nexport type CardColumn = DataTableColumnBase & {\n type: \"card\";\n};\n\nexport type DataTableColumn =\n | TextColumn\n | NumberColumn\n | BadgeColumn\n | DateColumn\n | BooleanColumn\n | CardColumn;\n\n// ---------------------------------------------------------------------------\n// Row\n// ---------------------------------------------------------------------------\n\nexport type DataTableRow<T = Record<string, unknown>> = {\n /** Unique row identifier used as the React key and in events */\n id: string | number;\n /**\n * Row payload — values are keyed by the column's `key` field.\n * For \"card\" columns the value should be a `PiCardRef` string.\n */\n data: T;\n /**\n * When set, the row shows an expand/collapse toggle button.\n * The referenced card is rendered full-width below the row on expand.\n * Use `onShowDetail` / `onHideDetail` events to track expanded state in\n * your redux store and update `detailCard` per row as needed.\n */\n detailCard?: PiCardRef;\n};\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n\nexport type DataTableProps<T = Record<string, unknown>> = {\n /** Column definitions — order determines display order */\n columns: DataTableColumn[];\n /** Row data */\n rows: DataTableRow<T>[];\n /** Optional visible `<caption>` rendered below the table */\n caption?: string;\n\n // --- Sorting ---\n /**\n * Key of the currently sorted column.\n * When provided the sort state is controlled; otherwise managed internally.\n */\n sortKey?: string;\n /**\n * Sort direction.\n * When provided together with `sortKey` the sort state is controlled.\n */\n sortAscending?: boolean;\n\n // --- Pagination ---\n /** Maximum number of rows per page. Omit to disable pagination. */\n pageSize?: number;\n /** Current page index (0-based). Treated as controlled when provided. */\n currentPage?: number;\n /**\n * Total row count, used for server-side pagination display.\n * Leave unset for client-side pagination derived from `rows.length`.\n */\n totalRows?: number;\n\n // --- Display ---\n /** Keep the header visible while the body scrolls */\n stickyHeader?: boolean;\n /** Alternate row background colour (zebra striping) */\n striped?: boolean;\n /** Highlight rows on hover (default: true) */\n hoverable?: boolean;\n /** Use compact row padding */\n compact?: boolean;\n\n // --- Empty state ---\n /** Card rendered when `rows` is empty */\n emptyCard?: PiCardRef;\n /** Fallback text when `rows` is empty and `emptyCard` is not set */\n emptyText?: string;\n\n className?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Events\n// ---------------------------------------------------------------------------\n\nexport type DataTableRowClickedEvent = {\n rowId: string | number;\n row: DataTableRow;\n};\n\nexport type DataTableSortChangedEvent = {\n /** The column key that was clicked */\n key: string;\n /** `true` = ascending, `false` = descending */\n ascending: boolean;\n};\n\nexport type DataTableDetailEvent = {\n rowId: string | number;\n row: DataTableRow;\n};\n\nexport type DataTablePageChangedEvent = {\n /** New page index (0-based) */\n page: number;\n pageSize: number;\n};\n\nexport type DataTableEvents = {\n onRowClicked: DataTableRowClickedEvent;\n onSortChanged: DataTableSortChangedEvent;\n onShowDetail: DataTableDetailEvent;\n onHideDetail: DataTableDetailEvent;\n onPageChanged: DataTablePageChangedEvent;\n};\n\n// ---------------------------------------------------------------------------\n// DataTableRowDetail — generic single-template detail panel\n// ---------------------------------------------------------------------------\n\n/** Configuration for a single field shown in the detail panel. */\nexport type DataTableRowDetailField = {\n /** Key to look up in `row.data` */\n key: string;\n /** Optional human-readable label prefix (e.g. \"Director\") */\n label?: string;\n /** Render style: \"title\" → h4 bold, \"muted\" → muted foreground, \"text\" (default) → plain */\n type?: \"title\" | \"text\" | \"muted\";\n /** Additional Tailwind class names forwarded to the element */\n className?: string;\n};\n\nexport type DataTableRowDetailProps = {\n /**\n * The row to display. Set this via a state mapper so a single template\n * card works for all rows:\n * ```ts\n * DataTableRowDetail({\n * row: (_, ctx) => ctx.ctxtProps?.row,\n * fields: [{ key: \"title\", type: \"title\" }, ...],\n * })\n * ```\n */\n row?: DataTableRow<Record<string, unknown>>;\n /**\n * Fields to render in order.\n * When omitted, every key in `row.data` is rendered as a plain key:value pair.\n */\n fields?: DataTableRowDetailField[];\n};\n\nexport const DATA_TABLE_ROW_DETAIL_CARD = \"shad/data-table-row-detail\";\nexport const DataTableRowDetail =\n createCardDeclaration<DataTableRowDetailProps>(DATA_TABLE_ROW_DETAIL_CARD);\n",
|
|
27
|
-
"type": "registry:component",
|
|
28
|
-
"target": "src/cards/dataTable/dataTable.types.ts"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"path": "cards/dataTable/dataTableRowDetail.component.tsx",
|
|
32
|
-
"content": "import React from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {cn} from \"@/lib/utils\";\nimport type {\n DataTableRowDetailField,\n DataTableRowDetailProps,\n} from \"./dataTable.types\";\n\n/**\n * DataTableRowDetail — generic detail-panel card for use with DataTable.\n *\n * Register it ONCE as a template, then reference it in every row via\n * `detailCard`. The DataTable passes the row via `cardKey` + `row` context\n * props so a state mapper on the `row` field delivers the correct data:\n *\n * ```ts\n * registerCard(\n * \"app/myTable/rowDetail\",\n * DataTableRowDetail({\n * // State mapper — ctx.ctxtProps.row is injected by the DataTable\n * row: (_, ctx) => ctx.ctxtProps?.row,\n * fields: [\n * { key: \"title\", type: \"title\" },\n * { key: \"director\", label: \"Director\" },\n * { key: \"plot\", type: \"muted\" },\n * ],\n * }),\n * );\n * ```\n */\nexport const DataTableRowDetailComponent = (\n props: PiCardProps<DataTableRowDetailProps>,\n): React.ReactNode => {\n const {row, fields, cardName} = props;\n\n if (!row?.data) {\n return null;\n }\n\n const data = row.data as Record<string, unknown>;\n\n /** If no `fields` config, auto-render all keys as plain text */\n if (!fields || fields.length === 0) {\n return (\n <div className=\"flex flex-col gap-1 text-sm\" data-pihanga={cardName}>\n {Object.entries(data).map(([key, value]) => (\n <div key={key}>\n <span className=\"font-medium text-muted-foreground\">{key}: </span>\n <span>{String(value ?? \"\")}</span>\n </div>\n ))}\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-1\" data-pihanga={cardName}>\n {fields.map((field: DataTableRowDetailField) => {\n const value = data[field.key];\n const display = value == null ? \"\" : String(value);\n\n switch (field.type) {\n case \"title\":\n return (\n <h4 key={field.key} className=\"text-base font-semibold\">\n {display}\n </h4>\n );\n\n case \"muted\":\n return (\n <p key={field.key} className=\"text-sm text-muted-foreground\">\n {field.label ? (\n <>\n <span className=\"font-medium\">{field.label}: </span>\n {display}\n </>\n ) : (\n display\n )}\n </p>\n );\n\n default:\n // \"text\" or any unknown type\n return (\n <p key={field.key} className={cn(\"text-sm\", field.className)}>\n {field.label ? (\n <>\n <span className=\"font-medium\">{field.label}: </span>\n {display}\n </>\n ) : (\n display\n )}\n </p>\n );\n }\n })}\n </div>\n );\n};\n",
|
|
33
|
-
"type": "registry:component",
|
|
34
|
-
"target": "src/cards/dataTable/dataTableRowDetail.component.tsx"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"path": "cards/dataTable/index.ts",
|
|
38
|
-
"content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {DataTableComponent} from \"./dataTable.component\";\nimport {DataTableRowDetailComponent} from \"./dataTableRowDetail.component\";\nimport {\n DATA_TABLE_ACTION,\n DATA_TABLE_CARD,\n DATA_TABLE_ROW_DETAIL_CARD,\n} from \"./dataTable.types\";\n\nexport * from \"./dataTable.types\";\n\nregisterCardComponent({\n name: DATA_TABLE_CARD,\n component: DataTableComponent,\n events: actionTypesToEvents(DATA_TABLE_ACTION),\n});\n\nregisterCardComponent({\n name: DATA_TABLE_ROW_DETAIL_CARD,\n component: DataTableRowDetailComponent,\n});\n",
|
|
39
|
-
"type": "registry:component",
|
|
40
|
-
"target": "src/cards/dataTable/index.ts"
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
}
|
package/r/dialog.json
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
}
|
package/r/dropDownMenu.json
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|