@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.
Files changed (66) hide show
  1. package/AGENT.building-cards.md +413 -0
  2. package/AGENT.md +50 -0
  3. package/AGENT.using-cards.md +1242 -0
  4. package/cards/box/box.types.d.ts +1 -4
  5. package/cards/box/box.types.js.map +1 -1
  6. package/cards/core-index.js +6 -6
  7. package/cards/fileDrop/fileDrop.component.d.ts +5 -0
  8. package/cards/fileDrop/fileDrop.types.d.ts +43 -0
  9. package/cards/fileDrop/index.d.ts +2 -0
  10. package/cards/icons.js +7 -3
  11. package/cards/icons.js.map +1 -1
  12. package/cards/index.d.ts +1 -0
  13. package/cards/stack/stack.types.d.ts +19 -22
  14. package/cards/stack/stack.types.js +1 -1
  15. package/cards/stack/stack.types.js.map +1 -1
  16. package/cards/tabs/index.js +4 -4
  17. package/cards/tabs/tabs.types.d.ts +2 -0
  18. package/cards/tabs/tabs.types.js +2 -2
  19. package/cards/tabs/tabs.types.js.map +1 -1
  20. package/cards/types.d.ts +4 -0
  21. package/cards/types.js.map +1 -1
  22. package/package.json +9 -1
  23. package/pihanga-shadcn.css +1 -1
  24. package/mountain-snow.svg +0 -4
  25. package/r/badge.json +0 -35
  26. package/r/box.json +0 -26
  27. package/r/button.json +0 -39
  28. package/r/checkbox.json +0 -31
  29. package/r/conditional.json +0 -30
  30. package/r/dataTable.json +0 -43
  31. package/r/dialog.json +0 -36
  32. package/r/dropDownMenu.json +0 -47
  33. package/r/field.json +0 -35
  34. package/r/flexGrid.json +0 -32
  35. package/r/form.json +0 -37
  36. package/r/framework.json +0 -28
  37. package/r/graphin.json +0 -42
  38. package/r/input.json +0 -35
  39. package/r/jsonViewer.json +0 -40
  40. package/r/list.json +0 -39
  41. package/r/loadingOverlay.json +0 -39
  42. package/r/loadingSkeleton.json +0 -30
  43. package/r/markdownViewer.json +0 -51
  44. package/r/menu.json +0 -34
  45. package/r/modeToggle.json +0 -36
  46. package/r/navbarSearch.json +0 -31
  47. package/r/pageWithNavbar.json +0 -46
  48. package/r/pasteTarget.json +0 -41
  49. package/r/pihanga-base.json +0 -11
  50. package/r/pihanga-cards-icons.json +0 -16
  51. package/r/pihanga-cards-types.json +0 -16
  52. package/r/pihanga-hook-use-is-touch-device.json +0 -15
  53. package/r/pihanga-lib-utils.json +0 -18
  54. package/r/pihanga-theme-provider.json +0 -25
  55. package/r/pihanga-ui-extras.json +0 -68
  56. package/r/registry.json +0 -171
  57. package/r/resizable.json +0 -35
  58. package/r/select.json +0 -35
  59. package/r/stack.json +0 -27
  60. package/r/stepper.json +0 -39
  61. package/r/switch.json +0 -36
  62. package/r/tabs.json +0 -33
  63. package/r/textField.json +0 -32
  64. package/r/toast.json +0 -32
  65. package/r/toggleGroup.json +0 -35
  66. package/r/typography.json +0 -31
package/r/resizable.json DELETED
@@ -1,35 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "resizable",
4
- "type": "registry:component",
5
- "description": "Pihanga resizable card component",
6
- "dependencies": [
7
- "clsx@^2.1.1",
8
- "lucide-react@^0.513.0",
9
- "react-resizable-panels@^2.0.0"
10
- ],
11
- "registryDependencies": [
12
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
13
- "resizable"
14
- ],
15
- "files": [
16
- {
17
- "path": "cards/resizable/index.ts",
18
- "content": "import {registerCardComponent} from \"@pihanga2/core\";\n\nimport {ResizableComponent} from \"./resizable.component\";\nimport {RESIZABLE_CARD} from \"./resizable.types\";\n\nexport * from \"./resizable.types\";\n\nregisterCardComponent({\n name: RESIZABLE_CARD,\n component: ResizableComponent,\n});\n",
19
- "type": "registry:component",
20
- "target": "src/cards/resizable/index.ts"
21
- },
22
- {
23
- "path": "cards/resizable/resizable.component.tsx",
24
- "content": "import React, {Fragment} from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport {clsx} from \"clsx\";\nimport {\n ResizableHandle,\n ResizablePanel,\n ResizablePanelGroup,\n} from \"@/components/ui/resizable\";\nimport {\n DefResizableHandle,\n type PiResizableHandle,\n type PiResizablePanel,\n type ResizableProps,\n} from \"./resizable.types\";\n\nexport const ResizableComponent = (\n props: PiCardProps<ResizableProps>,\n): React.ReactNode => {\n const {content, handles, direction, className, style, cardName} = props;\n\n // `style` may carry Shadcn-specific class overrides in a `shad` sub-key.\n const shadStyle = style as\n | {shad?: {root?: string; panel?: string; handle?: string}}\n | undefined;\n const sy = shadStyle?.shad ?? {};\n\n const _handles: PiResizableHandle[] = Array.isArray(handles)\n ? handles\n : Array(content.length - 1).fill(handles ?? DefResizableHandle);\n\n function renderPanel(el: PiResizablePanel, idx: number) {\n const name = el.name ?? `panel${idx}`;\n const panelProps: {\n defaultSize: number;\n collapsible?: boolean;\n maxSize?: number;\n minSize?: number;\n } = {\n defaultSize: el.defaultSize ?? 50,\n collapsible: el.collapsible,\n };\n if (el.maxSize !== undefined) panelProps.maxSize = el.maxSize;\n if (el.minSize !== undefined) panelProps.minSize = el.minSize;\n\n return (\n <Fragment key={idx}>\n {renderHandle(idx)}\n <ResizablePanel\n {...panelProps}\n className={clsx(sy.panel, className && `${className}-${name}`)}\n >\n <Card cardName={el.content} parentCard={cardName} />\n </ResizablePanel>\n </Fragment>\n );\n }\n\n function renderHandle(idx: number) {\n if (idx === 0) return null;\n const h = _handles[idx - 1];\n return <ResizableHandle {...h} className={clsx(sy.handle)} />;\n }\n\n return (\n <ResizablePanelGroup\n direction={direction ?? \"horizontal\"}\n className={clsx(className, sy.root)}\n data-pihanga={cardName}\n >\n {content.map(renderPanel)}\n </ResizablePanelGroup>\n );\n};\n",
25
- "type": "registry:component",
26
- "target": "src/cards/resizable/resizable.component.tsx"
27
- },
28
- {
29
- "path": "cards/resizable/resizable.types.ts",
30
- "content": "import {type PiCardRef, createCardDeclaration} from \"@pihanga2/core\";\n\nexport const RESIZABLE_CARD = \"shad/resizable\";\n\nexport const SdResizable =\n createCardDeclaration<ResizableProps>(RESIZABLE_CARD);\n\n/** Convenience alias — same as {@link SdResizable}. */\nexport const Resizable = SdResizable;\n\nexport type ResizableProps<S = unknown> = {\n /** Ordered list of panels to render inside the resizable group. */\n content: PiResizablePanel[];\n /** Layout direction of the panel group. Defaults to `\"horizontal\"`. */\n direction?: \"horizontal\" | \"vertical\";\n /**\n * Handle configuration(s) between panels.\n *\n * - A single `PiResizableHandle` is used for every handle in the group.\n * - An array must have exactly `content.length - 1` entries.\n *\n * Defaults to {@link DefResizableHandle} (visible drag handle).\n */\n handles?: PiResizableHandle | PiResizableHandle[];\n className?: string;\n /**\n * Optional Shadcn-style style overrides.\n *\n * When provided as `{ shad?: { root?: string; panel?: string; handle?: string } }`,\n * the nested class names are applied to the corresponding elements in\n * addition to any `className` already set.\n */\n style?: S;\n};\n\nexport type PiResizablePanel = {\n /** Used for CSS class suffixes and stable ordering. Defaults to `\"panel{idx}\"`. */\n name?: string;\n /** The card to render inside this panel. */\n content: PiCardRef;\n /** Initial size as a percentage of the group. Defaults to `50`. */\n defaultSize?: number;\n minSize?: number;\n maxSize?: number;\n /** When `true`, the panel can be collapsed to zero size. */\n collapsible?: boolean;\n};\n\nexport type PiResizableHandle = {\n /** When `true`, renders a visible drag-handle grip indicator. */\n withHandle?: boolean;\n /** When `true`, the handle is decorative only (not draggable). */\n disabled?: boolean;\n};\n\n/** Default handle configuration: visible grip, draggable. */\nexport const DefResizableHandle: PiResizableHandle = {\n withHandle: true,\n disabled: false,\n};\n",
31
- "type": "registry:component",
32
- "target": "src/cards/resizable/resizable.types.ts"
33
- }
34
- ]
35
- }
package/r/select.json DELETED
@@ -1,35 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "select",
4
- "type": "registry:component",
5
- "description": "Pihanga select card component",
6
- "dependencies": [
7
- "@radix-ui/react-select@^2.2.6",
8
- "lucide-react@^0.513.0"
9
- ],
10
- "registryDependencies": [
11
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
12
- "select",
13
- "https://ivcap-works.github.io/pihanga-shadcn/r/form.json"
14
- ],
15
- "files": [
16
- {
17
- "path": "cards/select/index.ts",
18
- "content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {SelectComponent} from \"./select.component\";\nimport {PI_SELECT_ACTION, PI_SELECT_CARD} from \"./select.types\";\n\nexport * from \"./select.types\";\n\nregisterCardComponent({\n name: PI_SELECT_CARD,\n component: SelectComponent,\n events: actionTypesToEvents(PI_SELECT_ACTION),\n});\n",
19
- "type": "registry:component",
20
- "target": "src/cards/select/index.ts"
21
- },
22
- {
23
- "path": "cards/select/select.component.tsx",
24
- "content": "import React, {useState} from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {\n Select as RadixSelect,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport type {PiSelectEvents, PiSelectProps} from \"./select.types\";\n\nexport const SelectComponent = (\n props: PiCardProps<PiSelectProps, PiSelectEvents>,\n): React.ReactNode => {\n const {\n name,\n value: propValue = \"\",\n defaultValue,\n selfManaged = false,\n options,\n placeholder,\n required,\n disabled,\n ariaLabel,\n cardName,\n onChanged,\n onOpened,\n onClosed,\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 // Internal state used only when selfManaged=true and not bound to a form.\n const [managedValue, setManagedValue] = useState<string>(\n defaultValue ?? propValue,\n );\n\n const value = useFormData\n ? ((form.formData[name!] as string | undefined) ?? \"\")\n : selfManaged\n ? managedValue\n : propValue;\n\n function handleChange(newValue: string) {\n if (useFormData) {\n form.handleChange(name!, newValue);\n } else {\n if (selfManaged) {\n setManagedValue(newValue);\n }\n onChanged({name, value: newValue});\n }\n }\n\n function handleOpenChange(open: boolean) {\n if (open) {\n onOpened({});\n } else {\n onClosed({});\n }\n }\n\n // Use the id injected by pi/field; fall back to a local id when standalone.\n const selfId = `${cardName}-${name ?? \"select\"}`;\n const fieldId = injectedId ?? selfId;\n\n return (\n <RadixSelect\n value={value}\n defaultValue={defaultValue}\n onValueChange={handleChange}\n onOpenChange={handleOpenChange}\n disabled={disabled}\n required={required}\n aria-label={ariaLabel}\n >\n <SelectTrigger\n id={fieldId}\n aria-invalid={invalid || undefined}\n data-pihanga={cardName}\n >\n <SelectValue placeholder={placeholder ?? \"Select…\"} />\n </SelectTrigger>\n <SelectContent>\n <SelectGroup>\n {options.map((opt) => (\n <SelectItem\n key={opt.value}\n value={opt.value}\n disabled={opt.disabled}\n >\n {opt.label}\n </SelectItem>\n ))}\n </SelectGroup>\n </SelectContent>\n </RadixSelect>\n );\n};\n",
25
- "type": "registry:component",
26
- "target": "src/cards/select/select.component.tsx"
27
- },
28
- {
29
- "path": "cards/select/select.types.ts",
30
- "content": "import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_SELECT_CARD = \"pi/select\";\n\nexport const Select = createCardDeclaration<PiSelectProps, PiSelectEvents>(\n PI_SELECT_CARD,\n);\n\nexport const PI_SELECT_ACTION = registerActions(PI_SELECT_CARD, [\n \"changed\",\n \"opened\",\n \"closed\",\n]);\n\nexport const onPiSelectChanged = createOnAction<PiSelectChangedEvent>(\n PI_SELECT_ACTION.CHANGED,\n);\n\nexport const onPiSelectOpened = createOnAction<PiSelectOpenedEvent>(\n PI_SELECT_ACTION.OPENED,\n);\n\nexport const onPiSelectClosed = createOnAction<PiSelectClosedEvent>(\n PI_SELECT_ACTION.CLOSED,\n);\n\nexport type PiSelectOption = {\n /** The value stored in form state / dispatched in events. */\n value: string;\n /** Human-readable label shown in the dropdown. */\n label: string;\n /** When true, the option is shown but cannot be selected. */\n disabled?: boolean;\n};\n\nexport type PiSelectProps = {\n /**\n * Field name used to bind to FormContext when inside a pi/form card.\n * When provided the component reads its selected value from form data and\n * writes 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 * When true the component manages its own selected-value state internally.\n * Selecting an item will still dispatch the normal `onChanged` action, but\n * the UI will immediately reflect the new selection without waiting for the\n * host application to update `value` via a reducer.\n *\n * Has no effect when the component is bound to a pi/form via `name`.\n */\n selfManaged?: boolean;\n\n /**\n * Default (uncontrolled) value. Mirrors Radix `defaultValue`.\n */\n defaultValue?: string;\n\n /** Available options to display in the dropdown. */\n options: PiSelectOption[];\n\n /** Placeholder text shown when no option is selected. */\n placeholder?: string;\n\n /** When true, the select is required (HTML form attribute). */\n required?: boolean;\n\n /** When true, the select is disabled and non-interactive. */\n disabled?: boolean;\n\n /** Accessible label for screen readers. */\n ariaLabel?: string;\n};\n\nexport type PiSelectChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** The value of the newly selected option. */\n value: string;\n};\n\nexport type PiSelectOpenedEvent = Record<string, never>;\n\nexport type PiSelectClosedEvent = Record<string, never>;\n\nexport type PiSelectEvents = {\n onChanged: PiSelectChangedEvent;\n onOpened: PiSelectOpenedEvent;\n onClosed: PiSelectClosedEvent;\n};\n",
31
- "type": "registry:component",
32
- "target": "src/cards/select/select.types.ts"
33
- }
34
- ]
35
- }
package/r/stack.json DELETED
@@ -1,27 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "stack",
4
- "type": "registry:component",
5
- "description": "Pihanga stack card component",
6
- "dependencies": [
7
- "@pihanga2/cards@^0.3.15",
8
- "clsx@^2.1.1"
9
- ],
10
- "registryDependencies": [
11
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json"
12
- ],
13
- "files": [
14
- {
15
- "path": "cards/stack/index.ts",
16
- "content": "import {createCardDeclaration, registerCardComponent} from \"@pihanga2/core\";\nimport type {StackProps} from \"@pihanga2/cards\";\n\nimport {Component} from \"./stack.component\";\n\nexport const CARD_TYPE = \"shad/stack\";\nexport const Stack = createCardDeclaration<StackProps>(CARD_TYPE);\n\nregisterCardComponent({\n name: CARD_TYPE,\n component: Component,\n});\n",
17
- "type": "registry:component",
18
- "target": "src/cards/stack/index.ts"
19
- },
20
- {
21
- "path": "cards/stack/stack.component.tsx",
22
- "content": "import * as React from \"react\";\nimport type {StackProps} from \"@pihanga2/cards\";\n\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport clsx from \"clsx\";\nimport {Fragment} from \"react\";\n\nexport type SelectSD = {\n root?: string;\n};\n\nexport const Component = (props: PiCardProps<StackProps>): React.ReactNode => {\n const {\n content,\n direction,\n divider,\n spacing,\n justifyContent,\n alignItems,\n className,\n style,\n cardName,\n } = props;\n const sd: SelectSD = style?.shad || {};\n\n const cn = [className, sd.root, \"flex\"];\n if (direction) {\n switch (direction) {\n // \"column-reverse\" | \"column\" | \"row-reverse\" | \"row\"\n case \"row\":\n cn.push(\"flex-row\");\n break;\n case \"row-reverse\":\n cn.push(\"flex-row-reverse\");\n break;\n case \"column\":\n cn.push(\"flex-col\");\n break;\n case \"column-reverse\":\n cn.push(\"flex-col-reverse\");\n break;\n }\n }\n if (justifyContent) {\n switch (justifyContent) {\n // \"flex-start\" | \"center\" | \"flex-end\" | \"space-between\" | \"space-around\" | \"space-evenly\"\n case \"flex-start\":\n cn.push(\"justify-start\");\n break;\n case \"center\":\n cn.push(\"justify-center\");\n break;\n case \"flex-end\":\n cn.push(\"justify-end\");\n break;\n case \"space-between\":\n cn.push(\"justify-between\");\n break;\n case \"space-around\":\n cn.push(\"justify-around\");\n break;\n case \"space-evenly\":\n cn.push(\"justify-evenly\");\n break;\n }\n }\n if (alignItems) {\n switch (alignItems) {\n // \"flex-start\" | \"center\" | \"flex-end\" | \"normal\" | \"stretch\" | \"start\" | \"end\" | \"baseline\" | \"initial\" | \"inherit\"\n case \"flex-start\":\n cn.push(\"items-start\");\n break;\n case \"center\":\n cn.push(\"items-center\");\n break;\n case \"stretch\":\n cn.push(\"items-stretch\");\n break;\n case \"flex-end\":\n cn.push(\"items-end\");\n break;\n case \"baseline\":\n cn.push(\"items-baseline\");\n break;\n default:\n console.warn(\"stack: unsupported 'alignItems' property\", alignItems);\n }\n }\n if (spacing && spacing > 0) {\n if (\n direction === undefined ||\n direction === \"row\" ||\n direction === \"row-reverse\"\n ) {\n cn.push(`gap-${spacing}`);\n } else {\n cn.push(`gap-${spacing}`);\n }\n }\n\n // const sx = style?.joy\n // const p = {\n // direction,\n // spacing,\n // justifyContent,\n // alignItems,\n // divider: divider ? <Card cardName={divider} parentCard={cardName} /> : null,\n // sx,\n // className,\n // }\n\n function renderContent() {\n if (!content) return null;\n\n return content.map((cn, i) => (\n <Fragment key={i}>\n <Card cardName={cn} parentCard={cardName} key={`item-${i}`} />\n {divider && i < content.length - 1 && (\n <Card cardName={divider} parentCard={cardName} key={`divider-${i}`} />\n )}\n </Fragment>\n ));\n }\n\n return (\n <div className={clsx(cn)} data-pihanga={cardName}>\n {renderContent()}\n {props.children}\n </div>\n );\n};\n",
23
- "type": "registry:component",
24
- "target": "src/cards/stack/stack.component.tsx"
25
- }
26
- ]
27
- }
package/r/stepper.json DELETED
@@ -1,39 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "stepper",
4
- "type": "registry:component",
5
- "description": "Pihanga stepper card component",
6
- "dependencies": [
7
- "lucide-react@^0.513.0"
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
- ],
13
- "files": [
14
- {
15
- "path": "cards/stepper/index.ts",
16
- "content": "import {registerCardComponent} from \"@pihanga2/core\";\n\nimport {StepperComponent} from \"./stepper.component\";\nimport {STEPPER_ACTION, STEPPER_CARD} from \"./stepper.types\";\n\nexport * from \"./stepper.types\";\n\nregisterCardComponent({\n name: STEPPER_CARD,\n component: StepperComponent,\n // Explicit mapping avoids CamelCase issues: STEPCLICKED → onStepClicked\n // (actionTypesToEvents would produce onStepclicked with lowercase 'c').\n events: {\n onStepClicked: STEPPER_ACTION.STEPCLICKED,\n },\n});\n",
17
- "type": "registry:component",
18
- "target": "src/cards/stepper/index.ts"
19
- },
20
- {
21
- "path": "cards/stepper/stepper.component.tsx",
22
- "content": "import React, {useState} from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport {StepperUI, type StepData} from \"@/components/ui/stepper\";\nimport type {StepperEvents, StepperProps} from \"./stepper.types\";\n// Default visual styles — import before component renders so BEM classes are styled.\n// Override by targeting the same class names in a stylesheet loaded after this one.\nimport \"./stepper.css\";\n\nexport const StepperComponent = (\n props: PiCardProps<StepperProps, StepperEvents>,\n): React.ReactNode => {\n const {\n steps,\n activeStep: propActiveStep,\n defaultActiveStep = 0,\n selfManaged = false,\n orientation = \"horizontal\",\n size = \"md\",\n showContent = false,\n className,\n cardName,\n onStepClicked,\n } = props;\n\n // Internal state used only when selfManaged=true.\n // Initialised from propActiveStep (if given) or defaultActiveStep.\n const [managedStep, setManagedStep] = useState<number>(\n propActiveStep ?? defaultActiveStep,\n );\n\n // Resolve the effective active step:\n // - selfManaged → use internal state\n // - controlled → use the prop (fall back to 0 if not set)\n const activeStep = selfManaged ? managedStep : (propActiveStep ?? 0);\n\n // Map StepItem[] → StepData[] (UI primitive only needs serialisable fields\n // plus an optional pre-rendered React node for custom labels).\n const stepData: StepData[] = steps.map((step) => ({\n id: step.id,\n title: step.title,\n description: step.description,\n optional: step.optional,\n // If a labelCard is specified, render it here and pass as `labelNode`.\n // The UI primitive will display it instead of the default title/description.\n labelNode: step.labelCard ? (\n <Card cardName={step.labelCard} parentCard={cardName} />\n ) : undefined,\n }));\n\n function handleStepClick(stepIndex: number, stepId: string) {\n if (selfManaged) {\n setManagedStep(stepIndex);\n }\n // Dispatch Pihanga event when the callback is wired (it may be absent for\n // inline / anonymous card declarations that have no registered reducer).\n if (typeof onStepClicked === \"function\") {\n onStepClicked({stepIndex, stepId});\n }\n }\n\n /**\n * Render a card ref for the active step's content (vertical mode only).\n * We look up the original `steps` array to get the `contentCard` reference.\n */\n function renderStepContent(stepIndex: number): React.ReactNode {\n if (!showContent || orientation !== \"vertical\") return null;\n const step = steps[stepIndex];\n if (!step?.contentCard) return null;\n return <Card cardName={step.contentCard} parentCard={cardName} />;\n }\n\n return (\n <div data-pihanga={cardName}>\n <StepperUI\n steps={stepData}\n activeStep={activeStep}\n orientation={orientation}\n size={size}\n className={className}\n onStepClick={handleStepClick}\n renderStepContent={renderStepContent}\n />\n </div>\n );\n};\n",
23
- "type": "registry:component",
24
- "target": "src/cards/stepper/stepper.component.tsx"
25
- },
26
- {
27
- "path": "cards/stepper/stepper.css",
28
- "content": "/* ============================================================\n pi-stepper — default visual styles\n src/cards/shadcn/stepper/stepper.css\n\n All visual properties live here so consumers can override\n individual rules by targeting the same BEM class names in\n a stylesheet loaded after this one (or by replacing this\n file entirely).\n\n Structural / layout Tailwind classes stay in the JSX and\n are not repeated here.\n\n Color tokens: the project uses Tailwind v4 with oklch values.\n Variables like --primary, --border etc. are full oklch() color\n values — use them directly with var(--primary), NOT with\n hsl(var(--primary)) which would be invalid in this setup.\n ============================================================ */\n\n/* ── CSS size variables (override per stepper or globally) ─── */\n.pi-stepper {\n --pi-stepper-indicator-size-sm: 1.5rem;\n /* 24 px */\n --pi-stepper-indicator-size-md: 2rem;\n /* 32 px */\n --pi-stepper-indicator-size-lg: 2.5rem;\n /* 40 px */\n --pi-stepper-indicator-size: var(--pi-stepper-indicator-size-md);\n --pi-stepper-connector-thickness: 2px;\n --pi-stepper-connector-min-height: 1.5rem;\n /* vertical mode */\n --pi-stepper-label-max-width: 6rem;\n /* horizontal mode */\n}\n\n/* ── Size modifiers ─────────────────────────────────────────── */\n.pi-stepper--sm {\n --pi-stepper-indicator-size: var(--pi-stepper-indicator-size-sm);\n}\n\n.pi-stepper--md {\n --pi-stepper-indicator-size: var(--pi-stepper-indicator-size-md);\n}\n\n.pi-stepper--lg {\n --pi-stepper-indicator-size: var(--pi-stepper-indicator-size-lg);\n}\n\n/* ──────────────────────────────────────────────────────────────\n Indicator (step icon / circle)\n\n Tailwind classes in stepper.tsx provide WORKING defaults:\n bg-primary / text-primary-foreground / ring-* / border-*\n Override them here by raising specificity one level (e.g. add\n the parent class) or by using !important if needed.\n ────────────────────────────────────────────────────────────── */\n\n/* Base indicator — transitions only (no colors; Tailwind handles defaults) */\n.pi-stepper__indicator {\n transition:\n background-color 200ms ease,\n border-color 200ms ease,\n color 200ms ease,\n outline-color 200ms ease;\n}\n\n/* To override the active indicator colour, use e.g.:\n .pi-stepper__indicator--active { background-color: var(--destructive); }\n The var() tokens are direct oklch values in this project. */\n\n.pi-stepper__indicator--clickable {\n cursor: pointer;\n}\n\n.pi-stepper__indicator--clickable:hover {\n opacity: 0.8;\n}\n\n.pi-stepper__indicator--static {\n cursor: default;\n}\n\n/* ── Connector (line between steps) ─────────────────────────── */\n.pi-stepper__connector {\n transition: background-color 300ms ease;\n}\n\n/* Vertical connector segment */\n.pi-stepper__connector--vertical {\n min-height: var(--pi-stepper-connector-min-height);\n}\n\n/* ── Label title ─────────────────────────────────────────────── */\n.pi-stepper__label-title {\n transition: color 200ms ease;\n}\n\n/* ── Vertical step content slot ──────────────────────────────── */\n.pi-stepper__step-content {\n /* no special styles needed — spacing set via Tailwind in JSX */\n}",
29
- "type": "registry:component",
30
- "target": "src/cards/stepper/stepper.css"
31
- },
32
- {
33
- "path": "cards/stepper/stepper.types.ts",
34
- "content": "import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const STEPPER_CARD = \"shad/stepper\";\n\nexport const Stepper = createCardDeclaration<StepperProps, StepperEvents>(\n STEPPER_CARD,\n);\n\nexport const STEPPER_ACTION = registerActions(STEPPER_CARD, [\"stepClicked\"]);\n\nexport const onStepperStepClicked = createOnAction<StepperStepClickedEvent>(\n STEPPER_ACTION.STEPCLICKED,\n);\n\n// ─── Step definition ─────────────────────────────────────────────────────────\n\n/**\n * A single step in the stepper.\n */\nexport type StepItem = {\n /** Unique identifier used in events. */\n id: string;\n\n /** Short label shown below/beside the step indicator. */\n title: string;\n\n /** Optional secondary text shown under the title. */\n description?: string;\n\n /**\n * If true, an \"(Optional)\" suffix is appended to the title.\n * Mirrors the MUI Joy UI `optional` prop.\n */\n optional?: boolean;\n\n /**\n * Optional Pihanga card rendered **instead of** the default title/description\n * text label. Useful for rich labels (icons, custom markup, etc.).\n * The card receives no extra props — use `memo` in its own declaration if\n * it needs to read from state.\n */\n labelCard?: PiCardRef;\n\n /**\n * A card ref rendered as the body of this step when it is active\n * and the stepper is in `\"vertical\"` orientation with `showContent: true`.\n */\n contentCard?: PiCardRef;\n};\n\n// ─── Card props ───────────────────────────────────────────────────────────────\n\nexport type StepperProps = {\n /** Ordered list of step definitions. */\n steps: StepItem[];\n\n /**\n * 0-based index of the currently active step (controlled mode).\n * Steps before this index are rendered as \"completed\";\n * steps after are \"upcoming\".\n *\n * Required in controlled mode (`selfManaged: false`).\n * Used as the initial value in self-managed mode when provided\n * (otherwise defaults to `defaultActiveStep ?? 0`).\n */\n activeStep?: number;\n\n /**\n * Initial active step used only in self-managed mode (`selfManaged: true`).\n * Ignored in controlled mode.\n * @default 0\n */\n defaultActiveStep?: number;\n\n /**\n * When `true`, the component manages its own `activeStep` state internally.\n * Clicking a step immediately advances the indicator without waiting for the\n * host application to update the `activeStep` prop via a reducer.\n * The `onStepClicked` event is still dispatched on each click.\n *\n * When `false` (default), `activeStep` fully controls the display.\n * @default false\n */\n selfManaged?: boolean;\n\n /**\n * Layout orientation.\n * - `\"horizontal\"` (default): steps flow left-to-right with labels below.\n * - `\"vertical\"`: steps stack top-to-bottom with labels/content to the right.\n */\n orientation?: \"horizontal\" | \"vertical\";\n\n /**\n * Visual size of the step indicators.\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n\n /**\n * When `true` and `orientation === \"vertical\"`, the active step's\n * `contentCard` is rendered inline beneath its label.\n * @default false\n */\n showContent?: boolean;\n\n /** Additional Tailwind classes applied to the root element. */\n className?: string;\n};\n\n// ─── Events ───────────────────────────────────────────────────────────────────\n\nexport type StepperStepClickedEvent = {\n /** 0-based index of the clicked step. */\n stepIndex: number;\n /** The `id` of the clicked step definition. */\n stepId: string;\n};\n\nexport type StepperEvents = {\n /** Fired when the user clicks on a step indicator. */\n onStepClicked: StepperStepClickedEvent;\n};\n",
35
- "type": "registry:component",
36
- "target": "src/cards/stepper/stepper.types.ts"
37
- }
38
- ]
39
- }
package/r/switch.json DELETED
@@ -1,36 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "switch",
4
- "type": "registry:component",
5
- "description": "Pihanga switch card component",
6
- "dependencies": [
7
- "@radix-ui/react-switch@UNKNOWN - add to root package.json",
8
- "radix-ui@^1.4.3"
9
- ],
10
- "registryDependencies": [
11
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
12
- "switch",
13
- "label",
14
- "https://ivcap-works.github.io/pihanga-shadcn/r/form.json"
15
- ],
16
- "files": [
17
- {
18
- "path": "cards/switch/index.ts",
19
- "content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {SwitchComponent} from \"./switch.component\";\nimport {PI_SWITCH_ACTION, PI_SWITCH_CARD} from \"./switch.types\";\n\nexport * from \"./switch.types\";\n\nregisterCardComponent({\n name: PI_SWITCH_CARD,\n component: SwitchComponent,\n events: actionTypesToEvents(PI_SWITCH_ACTION),\n});\n",
20
- "type": "registry:component",
21
- "target": "src/cards/switch/index.ts"
22
- },
23
- {
24
- "path": "cards/switch/switch.component.tsx",
25
- "content": "import React from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {Switch as SwitchUI} from \"@/components/ui/switch\";\nimport {Label} from \"@/components/ui/label\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport type {PiSwitchEvents, PiSwitchProps} from \"./switch.types\";\n\nexport const SwitchComponent = (\n props: PiCardProps<PiSwitchProps, PiSwitchEvents>,\n): React.ReactNode => {\n const {\n name,\n checked: propChecked = false,\n disabled,\n label,\n className,\n cardName,\n onChanged,\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\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 const checked = useFormData\n ? Boolean(form.formData[name!] ?? false)\n : propChecked;\n\n function handleCheckedChange(newChecked: boolean) {\n if (useFormData) {\n form.handleChange(name!, newChecked);\n } else {\n onChanged({name, checked: newChecked});\n }\n }\n\n // Use the id injected by pi/field; fall back to a local id when standalone.\n const selfId = `${cardName}-${name ?? \"switch\"}`;\n const fieldId = injectedId ?? selfId;\n\n return (\n <div\n data-pihanga={cardName}\n className={`flex items-center gap-2 ${className ?? \"\"}`}\n >\n <SwitchUI\n id={fieldId}\n checked={checked}\n onCheckedChange={handleCheckedChange}\n disabled={disabled}\n />\n {label && (\n <Label htmlFor={fieldId} className=\"cursor-pointer select-none\">\n {label}\n </Label>\n )}\n </div>\n );\n};\n",
26
- "type": "registry:component",
27
- "target": "src/cards/switch/switch.component.tsx"
28
- },
29
- {
30
- "path": "cards/switch/switch.types.ts",
31
- "content": "import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_SWITCH_CARD = \"pi/switch\";\n\nexport const Switch = createCardDeclaration<PiSwitchProps, PiSwitchEvents>(\n PI_SWITCH_CARD,\n);\n\nexport const PI_SWITCH_ACTION = registerActions(PI_SWITCH_CARD, [\"changed\"]);\n\nexport const onPiSwitchChanged = createOnAction<PiSwitchChangedEvent>(\n PI_SWITCH_ACTION.CHANGED,\n);\n\n// ---------------------------------------------------------------------------\n// Props & Events\n// ---------------------------------------------------------------------------\n\nexport type PiSwitchProps = {\n /**\n * Field name used to bind to FormContext when inside a pi/form card.\n * When provided the component reads its checked state from form data and\n * writes back via form.handleChange.\n */\n name?: string;\n\n /**\n * Controlled checked state used in standalone mode (outside a Form).\n * Ignored when `name` is set and the component is inside a pi/form.\n */\n checked?: boolean;\n\n /** When true, the switch is disabled and non-interactive. */\n disabled?: boolean;\n\n /**\n * Optional label text rendered beside the switch.\n * The label is automatically associated with the switch via `htmlFor`.\n */\n label?: string;\n\n /** Extra Tailwind / CSS classes forwarded to the root wrapper element. */\n className?: string;\n};\n\nexport type PiSwitchChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** New checked state after the toggle. */\n checked: boolean;\n};\n\nexport type PiSwitchEvents = {\n onChanged: PiSwitchChangedEvent;\n};\n",
32
- "type": "registry:component",
33
- "target": "src/cards/switch/switch.types.ts"
34
- }
35
- ]
36
- }
package/r/tabs.json DELETED
@@ -1,33 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "tabs",
4
- "type": "registry:component",
5
- "description": "Pihanga tabs card component",
6
- "dependencies": [
7
- "@radix-ui/react-tabs@^1.1.13"
8
- ],
9
- "registryDependencies": [
10
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
11
- "tabs"
12
- ],
13
- "files": [
14
- {
15
- "path": "cards/tabs/index.ts",
16
- "content": "import {registerCardComponent} from \"@pihanga2/core\";\n\nimport {TabsComponent} from \"./tabs.component\";\nimport {TABS_ACTION, TABS_CARD} from \"./tabs.types\";\n\nexport * from \"./tabs.types\";\n\nregisterCardComponent({\n name: TABS_CARD,\n component: TabsComponent,\n // Explicit mapping avoids CamelCase normalisation issues:\n // TABCHANGED → onTabChanged (lowercase 'c' would break with actionTypesToEvents)\n events: {\n onTabChanged: TABS_ACTION.TABCHANGED,\n },\n});\n",
17
- "type": "registry:component",
18
- "target": "src/cards/tabs/index.ts"
19
- },
20
- {
21
- "path": "cards/tabs/tabs.component.tsx",
22
- "content": "import React, {useState} from \"react\";\nimport {Card, isCardRef, type PiCardProps} from \"@pihanga2/core\";\nimport {Tabs, TabsContent, TabsList, TabsTrigger} from \"@/components/ui/tabs\";\nimport type {TabsEvents, TabsProps} from \"./tabs.types\";\n\nexport const TabsComponent = (\n props: PiCardProps<TabsProps, TabsEvents>,\n): React.ReactNode => {\n const {\n tabs,\n value: propValue,\n defaultValue,\n selfManaged = false,\n orientation = \"horizontal\",\n className,\n listClassName,\n contentClassName,\n cardName,\n onTabChanged,\n } = props;\n\n // Resolve the initial value for self-managed mode:\n // prefer propValue, then defaultValue, then first tab's id.\n const initialValue = propValue ?? defaultValue ?? tabs[0]?.id ?? \"\";\n\n // Internal state used only when selfManaged=true.\n const [managedValue, setManagedValue] = useState<string>(initialValue);\n\n // Effective active tab:\n // selfManaged → use internal state\n // controlled → use the prop (fall back to first tab if not set)\n const activeValue = selfManaged\n ? managedValue\n : (propValue ?? defaultValue ?? tabs[0]?.id ?? \"\");\n\n function handleValueChange(tabId: string) {\n if (selfManaged) {\n setManagedValue(tabId);\n }\n // Always dispatch the Pihanga event so reducers can observe the change\n // even in self-managed mode.\n if (typeof onTabChanged === \"function\") {\n onTabChanged({tabId});\n }\n }\n\n return (\n <div data-pihanga={cardName}>\n <Tabs\n value={activeValue}\n onValueChange={handleValueChange}\n orientation={orientation}\n className={className}\n >\n <TabsList className={listClassName}>\n {tabs.map((tab) => (\n <TabsTrigger\n key={tab.id}\n value={tab.id}\n disabled={tab.disabled}\n >\n {isCardRef(tab.title) ? (\n <Card cardName={tab.title} parentCard={cardName} />\n ) : (\n (tab.title as string)\n )}\n </TabsTrigger>\n ))}\n </TabsList>\n\n {tabs.map((tab) => (\n <TabsContent\n key={tab.id}\n value={tab.id}\n className={contentClassName}\n >\n <Card cardName={tab.contentCard} parentCard={cardName} />\n </TabsContent>\n ))}\n </Tabs>\n </div>\n );\n};\n",
23
- "type": "registry:component",
24
- "target": "src/cards/tabs/tabs.component.tsx"
25
- },
26
- {
27
- "path": "cards/tabs/tabs.types.ts",
28
- "content": "import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const TABS_CARD = \"shad/tabs\";\n\nexport const SdTabs = createCardDeclaration<TabsProps, TabsEvents>(TABS_CARD);\n\nexport const TABS_ACTION = registerActions(TABS_CARD, [\"tabChanged\"]);\n\nexport const onTabsTabChanged = createOnAction<TabsTabChangedEvent>(\n TABS_ACTION.TABCHANGED,\n);\n\n// ─── Tab item definition ──────────────────────────────────────────────────────\n\n/**\n * A single tab entry in the tabs component.\n */\nexport type TabItem = {\n /** Unique identifier used for controlled value and events. */\n id: string;\n\n /**\n * The label displayed on the tab trigger.\n * Can be a plain string or a PiCardRef for custom/rich tab labels.\n */\n title: string | PiCardRef;\n\n /**\n * The Pihanga card rendered as the body of this tab when it is active.\n */\n contentCard: PiCardRef;\n\n /** When true the tab trigger is rendered as disabled. */\n disabled?: boolean;\n};\n\n// ─── Card props ───────────────────────────────────────────────────────────────\n\nexport type TabsProps = {\n /** Ordered list of tab definitions. */\n tabs: TabItem[];\n\n /**\n * Controlled: the currently active tab id.\n * Must be provided (or supplied via a state mapper) when selfManaged is\n * false (the default).\n */\n value?: string;\n\n /**\n * Uncontrolled initial active tab id.\n * Used as the initial value in self-managed mode when value is not set;\n * falls back to the first tab id if omitted.\n */\n defaultValue?: string;\n\n /**\n * When true, the component manages its own active tab state internally.\n * The onTabChanged event is still dispatched on every change so external\n * reducers can observe the selection.\n *\n * When false (default), value fully controls the displayed tab.\n * @default false\n */\n selfManaged?: boolean;\n\n /**\n * Layout orientation of the tab list.\n * Passed directly to the Radix Tabs primitive.\n * @default \"horizontal\"\n */\n orientation?: \"horizontal\" | \"vertical\";\n\n /** Additional Tailwind/CSS classes applied to the root Tabs element. */\n className?: string;\n\n /** Additional classes applied to the TabsList element. */\n listClassName?: string;\n\n /** Additional classes applied to each TabsContent element. */\n contentClassName?: string;\n};\n\n// ─── Events ───────────────────────────────────────────────────────────────────\n\nexport type TabsTabChangedEvent = {\n /** The id of the newly selected tab. */\n tabId: string;\n};\n\nexport type TabsEvents = {\n /** Fired whenever the user selects a different tab. */\n onTabChanged: TabsTabChangedEvent;\n};\n",
29
- "type": "registry:component",
30
- "target": "src/cards/tabs/tabs.types.ts"
31
- }
32
- ]
33
- }
package/r/textField.json DELETED
@@ -1,32 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "textField",
4
- "type": "registry:component",
5
- "description": "Pihanga textField card component",
6
- "dependencies": [],
7
- "registryDependencies": [
8
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
9
- "input",
10
- "https://ivcap-works.github.io/pihanga-shadcn/r/form.json"
11
- ],
12
- "files": [
13
- {
14
- "path": "cards/textField/index.ts",
15
- "content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {TextFieldComponent} from \"./textField.component\";\nimport {PI_TEXT_FIELD_ACTION, PI_TEXT_FIELD_CARD} from \"./textField.types\";\n\nexport * from \"./textField.types\";\n\nregisterCardComponent({\n name: PI_TEXT_FIELD_CARD,\n component: TextFieldComponent,\n events: actionTypesToEvents(PI_TEXT_FIELD_ACTION),\n});\n",
16
- "type": "registry:component",
17
- "target": "src/cards/textField/index.ts"
18
- },
19
- {
20
- "path": "cards/textField/textField.component.tsx",
21
- "content": "import React from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {Input} from \"@/components/ui/input\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport type {PiTextFieldEvents, PiTextFieldProps} from \"./textField.types\";\n\nexport const TextFieldComponent = (\n props: PiCardProps<PiTextFieldProps, PiTextFieldEvents>,\n): React.ReactNode => {\n const {\n name,\n value: propValue = \"\",\n type = \"text\",\n placeholder,\n disabled,\n cardName,\n onChanged,\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 const value = useFormData\n ? ((form.formData[name!] as string | undefined) ?? \"\")\n : propValue;\n\n function handleChange(e: React.ChangeEvent<HTMLInputElement>) {\n const newValue = e.target.value;\n if (useFormData) {\n form.handleChange(name!, newValue);\n } else {\n onChanged({name, value: newValue});\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 ?? \"field\"}`;\n const fieldId = injectedId ?? selfId;\n\n return (\n <Input\n id={fieldId}\n type={type}\n value={value}\n onChange={handleChange}\n placeholder={placeholder}\n disabled={disabled}\n aria-invalid={invalid || undefined}\n />\n );\n};\n",
22
- "type": "registry:component",
23
- "target": "src/cards/textField/textField.component.tsx"
24
- },
25
- {
26
- "path": "cards/textField/textField.types.ts",
27
- "content": "import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_TEXT_FIELD_CARD = \"pi/text-field\";\n\nexport const TextField = createCardDeclaration<\n PiTextFieldProps,\n PiTextFieldEvents\n>(PI_TEXT_FIELD_CARD);\n\nexport const PI_TEXT_FIELD_ACTION = registerActions(PI_TEXT_FIELD_CARD, [\n \"changed\",\n]);\n\nexport const onPiTextFieldChanged = createOnAction<PiTextFieldChangedEvent>(\n PI_TEXT_FIELD_ACTION.CHANGED,\n);\n\nexport type PiTextFieldProps = {\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 /** HTML input type (e.g. \"text\", \"email\", \"password\", \"number\"). */\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\nexport type PiTextFieldChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** New value after the change. */\n value: string;\n};\n\nexport type PiTextFieldEvents = {\n onChanged: PiTextFieldChangedEvent;\n};\n",
28
- "type": "registry:component",
29
- "target": "src/cards/textField/textField.types.ts"
30
- }
31
- ]
32
- }
package/r/toast.json DELETED
@@ -1,32 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "toast",
4
- "type": "registry:component",
5
- "description": "Pihanga toast card component",
6
- "dependencies": [
7
- "sonner@^2.0.7"
8
- ],
9
- "registryDependencies": [
10
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json"
11
- ],
12
- "files": [
13
- {
14
- "path": "cards/toast/index.ts",
15
- "content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {ToastComponent} from \"./toast.component\";\nimport {PI_TOAST_ACTION, PI_TOAST_CARD} from \"./toast.types\";\n\nexport * from \"./toast.types\";\n\nregisterCardComponent({\n name: PI_TOAST_CARD,\n component: ToastComponent,\n events: actionTypesToEvents(PI_TOAST_ACTION),\n});\n",
16
- "type": "registry:component",
17
- "target": "src/cards/toast/index.ts"
18
- },
19
- {
20
- "path": "cards/toast/toast.component.tsx",
21
- "content": "import React, {useEffect, useRef, useState} from \"react\";\nimport {\n Card,\n type PiCardProps,\n type PiCardRef,\n usePiReducer,\n type ReduxAction,\n type ReduxState,\n} from \"@pihanga2/core\";\nimport {toast} from \"sonner\";\n\nimport {\n ShowToastEvent,\n TOAST_OP_ACTION,\n ToastPosition,\n ToastType,\n type PiToastEvents,\n type PiToastProps,\n} from \"./toast.types\";\n\nexport const ToastComponent = (\n props: PiCardProps<PiToastProps, PiToastEvents>,\n): React.ReactNode => {\n const {\n variant: defaultVariant = \"default\",\n duration: defaultDuration = 4000,\n dismissible: defaultDismissible = true,\n className: defaultClassName,\n onClosed,\n cardName,\n } = props;\n\n // State for tracking Redux-triggered toast messages\n const [currentToast, setCurrentToast] = useState<{\n message?: string;\n description?: string;\n contentCard?: PiCardRef;\n variant: ToastType;\n position?: ToastPosition;\n duration: number;\n } | null>(null);\n\n // Listen for showToast Redux messages\n // This allows triggering toasts from anywhere via dispatchShowToast\n usePiReducer<ReduxState, ShowToastEvent & ReduxAction>(\n TOAST_OP_ACTION.SHOW,\n (_, action) => {\n // Only respond if message is for this card or no specific cardName is set\n if (action.cardName && action.cardName !== cardName) {\n return;\n }\n setCurrentToast({\n message: action.message,\n description: action.description,\n contentCard: action.contentCard,\n variant: action.variant ?? defaultVariant,\n position: action.position,\n duration: action.duration ?? defaultDuration,\n });\n },\n cardName,\n );\n\n // Setup hooks before conditional rendering\n const toastIdRef = useRef<string | number | undefined>(undefined);\n\n useEffect(() => {\n // Only show toast if Redux message was received\n if (!currentToast) {\n return;\n }\n\n // Map our toast types to Sonner's toast function\n const toastFn =\n {\n success: toast.success,\n error: toast.error,\n info: toast.info,\n warning: toast.warning,\n default: toast,\n }[currentToast.variant] || toast;\n\n // Build className: combine default + variant-based class\n const className = [defaultClassName, `toast-${currentToast.variant}`]\n .filter(Boolean)\n .join(\" \");\n\n // If contentCard is provided, render it as custom JSX content\n if (currentToast.contentCard) {\n const contentCard = currentToast.contentCard;\n const CustomToastContent = () => (\n <Card cardName={contentCard} parentCard={cardName} />\n );\n\n // Show the toast with custom card content\n toastIdRef.current = toastFn(CustomToastContent, {\n duration: currentToast.duration,\n position: currentToast.position || \"bottom-right\",\n dismissible: defaultDismissible,\n className,\n onDismiss: () => {\n setCurrentToast(null);\n onClosed({});\n },\n onAutoClose: () => {\n setCurrentToast(null);\n onClosed({});\n },\n });\n } else {\n // Show the toast with message and optional description\n toastIdRef.current = toastFn(currentToast.message, {\n description: currentToast.description,\n duration: currentToast.duration,\n position: currentToast.position || \"bottom-right\",\n dismissible: defaultDismissible,\n className,\n onDismiss: () => {\n setCurrentToast(null);\n onClosed({});\n },\n onAutoClose: () => {\n setCurrentToast(null);\n onClosed({});\n },\n });\n }\n\n // Cleanup on unmount\n return () => {\n if (toastIdRef.current !== undefined) {\n toast.dismiss(toastIdRef.current);\n }\n };\n }, [currentToast, defaultDismissible, defaultClassName, onClosed, cardName]);\n\n // This component doesn't render visible DOM itself\n // Sonner renders toasts in a portal via its Toaster component\n return <div data-pihanga={cardName} style={{display: \"none\"}} />;\n};\n",
22
- "type": "registry:component",
23
- "target": "src/cards/toast/toast.component.tsx"
24
- },
25
- {
26
- "path": "cards/toast/toast.types.ts",
27
- "content": "import {\n createCardDeclaration,\n createOnAction,\n PiCardRef,\n registerActions,\n type DispatchF,\n} from \"@pihanga2/core\";\n\nexport const PI_TOAST_CARD = \"pi/toast\";\n\nexport const Toast = createCardDeclaration<PiToastProps, PiToastEvents>(\n PI_TOAST_CARD,\n);\n\nexport const TOAST_OP_ACTION = registerActions(\"toast/op\", [\"show\"]);\n\n/**\n * Dispatch helper to show a toast\n *\n * @param d - Dispatch function from Pihanga\n * @param ev - Toast event data\n *\n * @example\n * ```typescript\n * import {dispatchShowToast} from \"@/cards/toast\";\n *\n * dispatchShowToast(_dispatch, {\n * message: \"Success!\",\n * description: \"Your changes were saved.\",\n * type: \"success\",\n * duration: 3000,\n * });\n * ```\n */\nexport const dispatchShowToast = (d: DispatchF, ev: ShowToastEvent) => {\n d({...ev, type: TOAST_OP_ACTION.SHOW});\n};\n\n/**\n * Action handler for when a toast is shown\n *\n * Use this in your reducers or effects to handle toast display events:\n *\n * @example\n * ```typescript\n * import {onShowToast} from \"@/cards/toast\";\n *\n * export const handleShowToast = onShowToast((state, action) => {\n * console.log(\"Toast shown:\", action.payload.message);\n * return state;\n * });\n * ```\n */\nexport const onShowToast = createOnAction<ShowToastEvent>(TOAST_OP_ACTION.SHOW);\n\nexport const PI_TOAST_ACTION = registerActions(PI_TOAST_CARD, [\"closed\"]);\n\nexport const onPiToastClosed = createOnAction<PiToastClosedEvent>(\n PI_TOAST_ACTION.CLOSED,\n);\n\n/**\n * Toast notification type/variant.\n */\nexport type ToastType = \"default\" | \"success\" | \"error\" | \"info\" | \"warning\";\n\n/**\n * Position of the toast on the screen.\n */\nexport type ToastPosition =\n | \"top-left\"\n | \"top-center\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-center\"\n | \"bottom-right\";\n\nexport type PiToastProps = {\n /**\n * Default type/variant for toasts.\n * Can be overridden per toast via ShowToastEvent.type\n * @default 'default'\n */\n variant?: ToastType;\n\n /**\n * Default duration in milliseconds before the toast auto-closes.\n * Can be overridden per toast via ShowToastEvent.duration\n * Set to `Infinity` to disable auto-close.\n * @default 4000\n */\n duration?: number;\n\n /**\n * Default: whether toasts are dismissible.\n * Can be overridden per toast via ShowToastEvent.dismissible\n * @default true\n */\n dismissible?: boolean;\n\n /**\n * Custom CSS class to apply to all toasts from this card.\n */\n className?: string;\n};\n\nexport type PiToastClosedEvent = {\n id?: string;\n};\n\nexport type PiToastEvents = {\n onClosed: PiToastClosedEvent;\n};\n\n/**\n * Event payload for showing a toast\n */\nexport type ShowToastEvent = {\n cardName?: string;\n message: string;\n description?: string;\n contentCard?: PiCardRef; // Alternative: custom card for richer content\n variant?: ToastType;\n position?: ToastPosition; // Toast position on screen\n duration?: number;\n};\n",
28
- "type": "registry:component",
29
- "target": "src/cards/toast/toast.types.ts"
30
- }
31
- ]
32
- }
@@ -1,35 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "toggleGroup",
4
- "type": "registry:component",
5
- "description": "Pihanga toggleGroup card component",
6
- "dependencies": [
7
- "class-variance-authority@^0.7.1",
8
- "radix-ui@^1.4.3"
9
- ],
10
- "registryDependencies": [
11
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
12
- "toggle-group",
13
- "https://ivcap-works.github.io/pihanga-shadcn/r/form.json"
14
- ],
15
- "files": [
16
- {
17
- "path": "cards/toggleGroup/index.ts",
18
- "content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {ToggleGroupComponent} from \"./toggleGroup.component\";\nimport {\n PI_TOGGLE_GROUP_ACTION,\n PI_TOGGLE_GROUP_CARD,\n} from \"./toggleGroup.types\";\n\nexport * from \"./toggleGroup.types\";\n\nregisterCardComponent({\n name: PI_TOGGLE_GROUP_CARD,\n component: ToggleGroupComponent,\n events: actionTypesToEvents(PI_TOGGLE_GROUP_ACTION),\n});\n",
19
- "type": "registry:component",
20
- "target": "src/cards/toggleGroup/index.ts"
21
- },
22
- {
23
- "path": "cards/toggleGroup/toggleGroup.component.tsx",
24
- "content": "import React from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {\n ToggleGroup as ToggleGroupUI,\n ToggleGroupItem,\n} from \"@/components/ui/toggle-group\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport type {\n PiToggleGroupEvents,\n PiToggleGroupProps,\n} from \"./toggleGroup.types\";\n\nexport const ToggleGroupComponent = (\n props: PiCardProps<PiToggleGroupProps, PiToggleGroupEvents>,\n): React.ReactNode => {\n const {\n name,\n items,\n type = \"single\",\n value: propValue,\n selfManaged = false,\n variant = \"default\",\n size = \"default\",\n spacing = 0,\n disabled,\n className,\n cardName,\n onChanged,\n } = props;\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 // ---------------------------------------------------------------------------\n // Self-managed (internal) state — only active when selfManaged=true and we\n // are NOT inside a pi/form card.\n // ---------------------------------------------------------------------------\n const initialSelfValue: string | string[] =\n type === \"multiple\"\n ? Array.isArray(propValue)\n ? propValue\n : propValue != null\n ? [String(propValue)]\n : []\n : typeof propValue === \"string\"\n ? propValue\n : \"\";\n\n const [internalValue, setInternalValue] = React.useState<string | string[]>(\n initialSelfValue,\n );\n\n // ---------------------------------------------------------------------------\n // Derive the effective value from the appropriate source.\n // 1. Form context (highest priority)\n // 2. Self-managed internal state\n // 3. Externally controlled prop value\n // ---------------------------------------------------------------------------\n const formRawValue = useFormData ? form.formData[name!] : undefined;\n const value: string | string[] = useFormData\n ? type === \"multiple\"\n ? Array.isArray(formRawValue)\n ? (formRawValue as string[])\n : formRawValue != null\n ? [String(formRawValue)]\n : []\n : formRawValue != null\n ? String(formRawValue)\n : \"\"\n : selfManaged\n ? internalValue\n : (propValue ?? (type === \"multiple\" ? [] : \"\"));\n\n function handleSingleChange(newValue: string) {\n if (useFormData) {\n form.handleChange(name!, newValue);\n } else {\n if (selfManaged) {\n setInternalValue(newValue);\n }\n onChanged({name: name ?? cardName, value: newValue});\n }\n }\n\n function handleMultipleChange(newValue: string[]) {\n if (useFormData) {\n form.handleChange(name!, newValue);\n } else {\n if (selfManaged) {\n setInternalValue(newValue);\n }\n onChanged({name: name ?? cardName, value: newValue});\n }\n }\n\n // Render single-selection group\n if (type === \"single\") {\n return (\n <ToggleGroupUI\n data-pihanga={cardName}\n type=\"single\"\n value={typeof value === \"string\" ? value : \"\"}\n onValueChange={handleSingleChange}\n variant={variant}\n size={size}\n spacing={spacing}\n disabled={disabled}\n className={className}\n >\n {items.map((item) => (\n <ToggleGroupItem\n key={item.value}\n value={item.value}\n disabled={item.disabled}\n >\n {item.label}\n </ToggleGroupItem>\n ))}\n </ToggleGroupUI>\n );\n }\n\n // Render multiple-selection group\n return (\n <ToggleGroupUI\n data-pihanga={cardName}\n type=\"multiple\"\n value={Array.isArray(value) ? value : value ? [value] : []}\n onValueChange={handleMultipleChange}\n variant={variant}\n size={size}\n spacing={spacing}\n disabled={disabled}\n className={className}\n >\n {items.map((item) => (\n <ToggleGroupItem\n key={item.value}\n value={item.value}\n disabled={item.disabled}\n >\n {item.label}\n </ToggleGroupItem>\n ))}\n </ToggleGroupUI>\n );\n};\n",
25
- "type": "registry:component",
26
- "target": "src/cards/toggleGroup/toggleGroup.component.tsx"
27
- },
28
- {
29
- "path": "cards/toggleGroup/toggleGroup.types.ts",
30
- "content": "import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_TOGGLE_GROUP_CARD = \"pi/toggle-group\";\n\nexport const ToggleGroup = createCardDeclaration<\n PiToggleGroupProps,\n PiToggleGroupEvents\n>(PI_TOGGLE_GROUP_CARD);\n\nexport const PI_TOGGLE_GROUP_ACTION = registerActions(PI_TOGGLE_GROUP_CARD, [\n \"changed\",\n]);\n\nexport const onPiToggleGroupChanged = createOnAction<PiToggleGroupChangedEvent>(\n PI_TOGGLE_GROUP_ACTION.CHANGED,\n);\n\n// ---------------------------------------------------------------------------\n// Item type\n// ---------------------------------------------------------------------------\n\nexport type PiToggleGroupItem = {\n /** The value associated with this toggle item. */\n value: string;\n /** Human-readable label rendered inside the toggle button. */\n label: string;\n /** When true, this individual item is disabled. */\n disabled?: boolean;\n};\n\n// ---------------------------------------------------------------------------\n// Props & Events\n// ---------------------------------------------------------------------------\n\nexport type PiToggleGroupProps = {\n /**\n * Field name used to bind to FormContext when inside a pi/form card.\n * When provided the component reads its selected value from form data and\n * writes back via form.handleChange.\n */\n name?: string;\n\n /**\n * The list of toggle items to display.\n */\n items: PiToggleGroupItem[];\n\n /**\n * Selection mode.\n * - `\"single\"` — at most one item can be active at a time.\n * - `\"multiple\"` — any number of items can be active simultaneously.\n *\n * @default \"single\"\n */\n type?: \"single\" | \"multiple\";\n\n /**\n * Controlled selected value(s).\n * - In `\"single\"` mode: a `string` (or `undefined` for nothing selected).\n * - In `\"multiple\"` mode: an array of `string`.\n *\n * Ignored when `name` is set and the component is inside a pi/form.\n * Also ignored when `selfManaged` is `true`.\n */\n value?: string | string[];\n\n /**\n * When `true`, the component manages its own selection state internally\n * using `React.useState`. The `value` prop is used only as the initial\n * value (i.e. `defaultValue`) and subsequent changes do not require an\n * external state update. `onChanged` is still fired so callers can\n * observe changes without owning the state.\n *\n * Has no effect when inside a pi/form (form context always takes\n * precedence).\n *\n * @default false\n */\n selfManaged?: boolean;\n\n /**\n * Visual style variant of the toggle buttons.\n * - `\"default\"` — transparent background, highlights on active.\n * - `\"outline\"` — bordered buttons, grouped or spaced.\n *\n * @default \"default\"\n */\n variant?: \"default\" | \"outline\";\n\n /**\n * Size of each toggle button.\n * @default \"default\"\n */\n size?: \"default\" | \"sm\" | \"lg\";\n\n /**\n * Gap between toggle buttons (in Tailwind spacing units).\n * `0` = buttons are joined (no gap, rounded ends only).\n * Positive values add space between each button.\n *\n * @default 0\n */\n spacing?: number;\n\n /** When true, the entire group is disabled and non-interactive. */\n disabled?: boolean;\n\n /** Extra Tailwind / CSS classes forwarded to the root element. */\n className?: string;\n};\n\nexport type PiToggleGroupChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /**\n * New selected value(s).\n * - In `\"single\"` mode: a `string` (empty string when nothing selected).\n * - In `\"multiple\"` mode: an array of `string`.\n */\n value: string | string[];\n};\n\nexport type PiToggleGroupEvents = {\n onChanged: PiToggleGroupChangedEvent;\n};\n",
31
- "type": "registry:component",
32
- "target": "src/cards/toggleGroup/toggleGroup.types.ts"
33
- }
34
- ]
35
- }
package/r/typography.json DELETED
@@ -1,31 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "typography",
4
- "type": "registry:component",
5
- "description": "Pihanga typography 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/typography/index.ts",
14
- "content": "import {registerCardComponent} from \"@pihanga2/core\";\n\nimport {TypographyComponent} from \"./typography.component\";\nimport {TYPOGRAPHY_CARD} from \"./typography.types\";\n\nexport * from \"./typography.types\";\n\nregisterCardComponent({\n name: TYPOGRAPHY_CARD,\n component: TypographyComponent,\n});\n",
15
- "type": "registry:component",
16
- "target": "src/cards/typography/index.ts"
17
- },
18
- {
19
- "path": "cards/typography/typography.component.tsx",
20
- "content": "import * as React from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport {cn} from \"@/lib/utils\";\nimport type {TypographyLevel, TypographyProps} from \"./typography.types\";\n\n/** Maps each level to [html element, tailwind classes] */\nconst LEVEL_CONFIG: Record<\n TypographyLevel,\n [keyof React.JSX.IntrinsicElements, string]\n> = {\n h1: [\"h1\", \"scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl\"],\n h2: [\n \"h2\",\n \"scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0\",\n ],\n h3: [\"h3\", \"scroll-m-20 text-2xl font-semibold tracking-tight\"],\n h4: [\"h4\", \"scroll-m-20 text-xl font-semibold tracking-tight\"],\n p: [\"p\", \"leading-7 [&:not(:first-child)]:mt-6\"],\n blockquote: [\"blockquote\", \"mt-6 border-l-2 pl-6 italic\"],\n code: [\n \"code\",\n \"relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold\",\n ],\n lead: [\"p\", \"text-xl text-muted-foreground\"],\n large: [\"div\", \"text-lg font-semibold\"],\n small: [\"small\", \"text-sm font-medium leading-none\"],\n muted: [\"p\", \"text-sm text-muted-foreground\"],\n};\n\nexport const TypographyComponent = (\n props: PiCardProps<TypographyProps>,\n): React.ReactNode => {\n const {\n text,\n childCard,\n paragraph,\n level,\n className,\n style,\n cardName,\n _dispatch,\n _cls,\n } = props;\n\n const [el, levelClass] = level ? LEVEL_CONFIG[level] : [\"div\", \"\"];\n\n function renderChildren(): React.ReactNode {\n if (childCard) {\n return <Card cardName={childCard} parentCard={cardName} />;\n }\n if (paragraph) {\n return <>{paragraph.map((item, i) => renderParaItem(item, i))}</>;\n }\n return text;\n }\n\n function renderParaItem(\n item: string | TypographyProps,\n key: number,\n ): React.ReactNode {\n if (typeof item === \"string\") {\n return item;\n }\n if (item && typeof item === \"object\") {\n return TypographyComponent({\n ...item,\n _dispatch,\n _cls,\n cardName,\n key,\n } as PiCardProps<TypographyProps> & {key: number});\n }\n return null;\n }\n\n return React.createElement(\n el,\n {\n className: cn(levelClass, className),\n style,\n \"data-pihanga\": cardName,\n },\n renderChildren(),\n );\n};\n",
21
- "type": "registry:component",
22
- "target": "src/cards/typography/typography.component.tsx"
23
- },
24
- {
25
- "path": "cards/typography/typography.types.ts",
26
- "content": "import * as React from \"react\";\nimport {PiCardRef, createCardDeclaration} from \"@pihanga2/core\";\n\nexport const TYPOGRAPHY_CARD = \"shad/typography\";\n\nexport const Typography =\n createCardDeclaration<TypographyProps>(TYPOGRAPHY_CARD);\n\n/**\n * Semantic level controls both the rendered HTML element and default\n * Tailwind prose styling (following shadcn/ui typography conventions).\n */\nexport type TypographyLevel =\n | \"h1\"\n | \"h2\"\n | \"h3\"\n | \"h4\"\n | \"p\"\n | \"blockquote\"\n | \"code\"\n | \"lead\" // large muted intro paragraph\n | \"large\" // large text, semibold\n | \"small\" // small, medium-weight\n | \"muted\"; // small, muted-foreground\n\nexport type TypographyProps = {\n /** Plain text content. */\n text?: string;\n /** A child card rendered as content. */\n childCard?: PiCardRef;\n /** Inline paragraph: mix of literal strings and nested TypographyProps. */\n paragraph?: (string | TypographyProps)[];\n\n /** Semantic level — controls the HTML element and default prose styling. */\n level?: TypographyLevel;\n\n /** Additional CSS classes applied directly to the element. */\n className?: string;\n /** Inline styles applied directly to the element. */\n style?: React.CSSProperties;\n};\n",
27
- "type": "registry:component",
28
- "target": "src/cards/typography/typography.types.ts"
29
- }
30
- ]
31
- }