@pihanga2/shadcn 0.2.20 → 0.2.21

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 (42) hide show
  1. package/AGENT.using-cards.md +2 -2
  2. package/cards/button/button.component.js +56 -54
  3. package/cards/button/button.component.js.map +1 -1
  4. package/cards/button/button.types.d.ts +24 -0
  5. package/cards/button/button.types.js.map +1 -1
  6. package/cards/checkbox/checkbox.types.d.ts +2 -7
  7. package/cards/checkbox/checkbox.types.js.map +1 -1
  8. package/cards/field/field.component.js +1 -1
  9. package/cards/field/field.component.js.map +1 -1
  10. package/cards/form/form.component.js +57 -32
  11. package/cards/form/form.component.js.map +1 -1
  12. package/cards/form/form.context.d.ts +11 -0
  13. package/cards/form/form.context.js +1 -0
  14. package/cards/form/form.context.js.map +1 -1
  15. package/cards/form/form.types.d.ts +75 -0
  16. package/cards/form/form.types.js +6 -2
  17. package/cards/form/form.types.js.map +1 -1
  18. package/cards/form/index.js +5 -5
  19. package/cards/input/index.js +6 -6
  20. package/cards/input/input.component.js +34 -28
  21. package/cards/input/input.component.js.map +1 -1
  22. package/cards/input/input.types.d.ts +14 -7
  23. package/cards/input/input.types.js +2 -2
  24. package/cards/input/input.types.js.map +1 -1
  25. package/cards/pageWithNavbarMeta/index.js +35 -25
  26. package/cards/pageWithNavbarMeta/index.js.map +1 -1
  27. package/cards/pageWithNavbarMeta/pageWithNavbarMeta.types.d.ts +1 -4
  28. package/cards/select/select.types.d.ts +2 -7
  29. package/cards/select/select.types.js.map +1 -1
  30. package/cards/switch/switch.types.d.ts +2 -7
  31. package/cards/switch/switch.types.js.map +1 -1
  32. package/cards/textArea/textArea.component.js +1 -1
  33. package/cards/textArea/textArea.component.js.map +1 -1
  34. package/cards/textArea/textArea.types.d.ts +15 -11
  35. package/cards/textArea/textArea.types.js.map +1 -1
  36. package/cards/textField/textField.component.js +13 -9
  37. package/cards/textField/textField.component.js.map +1 -1
  38. package/cards/textField/textField.types.d.ts +12 -7
  39. package/cards/textField/textField.types.js.map +1 -1
  40. package/cards/toggleGroup/toggleGroup.types.d.ts +2 -7
  41. package/cards/toggleGroup/toggleGroup.types.js.map +1 -1
  42. package/package.json +2 -2
@@ -976,7 +976,7 @@ search box, a settings field) is **`pi/input`** — *not* `pi/text-input`, which
976
976
  does not exist. You do **not** need to write your own local card.
977
977
 
978
978
  ```ts
979
- import {PiInput, onPiInputChanged} from "@/cards/input";
979
+ import {Input, onPiInputChanged} from "@/cards/input";
980
980
  import type {AppState} from "@/app.state";
981
981
 
982
982
  register((r) => {
@@ -985,7 +985,7 @@ register((r) => {
985
985
  });
986
986
  });
987
987
 
988
- registerCard("myApp/jwtField", PiInput({
988
+ registerCard("myApp/jwtField", Input({
989
989
  label: "JWT token",
990
990
  value: memo((s: AppState) => s.jwtToken), // ← bound to Redux state
991
991
  placeholder: "Paste your bearer token here…",
@@ -2,70 +2,72 @@ import { getIcon as e } from "../icons.js";
2
2
  import { Button as t, LinkButton as n } from "../../components/ui/button.js";
3
3
  import { Tooltip as r, TooltipContent as i, TooltipTrigger as a } from "../../components/ui/tooltip.js";
4
4
  import { useDropdownOpen as o } from "../dropDownMenu/dropdown-context.js";
5
+ import { useFormContext as s } from "../form/form.context.js";
5
6
  import "react";
6
- import { Card as s } from "@pihanga2/core";
7
- import { Fragment as c, jsx as l, jsxs as u } from "react/jsx-runtime";
7
+ import { Card as c } from "@pihanga2/core";
8
+ import { Fragment as l, jsx as u, jsxs as d } from "react/jsx-runtime";
8
9
  //#region src/cards/button/button.component.tsx
9
- function d(t) {
10
+ function f(t) {
10
11
  return t ? e(t) : null;
11
12
  }
12
- var f = (e) => {
13
- let { opts: f, ariaLabel: p, label: m, iconLabel: h, contentCard: g, href: _, target: v, tooltip: y, tooltipCard: b, tooltipMode: x, tooltipPlacement: S, className: C, id: w, onClicked: T, cardName: E, _cls: D, active: O, focused: k, disabled: A, loading: j, isPending: M } = e, N = o(), P = d(f?.beforeIcon), F = d(f?.afterIcon), I = /* @__PURE__ */ u(c, { children: [
14
- P,
15
- h ? d(h) : g ? /* @__PURE__ */ l(s, {
16
- cardName: g,
17
- parentCard: E
18
- }) : m,
19
- F
20
- ] }), L = {
21
- active: O,
22
- focused: k,
23
- isMenu: f?.isMenu,
24
- truncate: f?.truncate,
25
- variant: f?.variant,
26
- size: f?.size,
27
- disabled: A,
28
- loading: j,
29
- isPending: M,
30
- loadingClassName: f?.loadingClassName,
31
- className: `${D("root")} ${C || ""}`.trim()
32
- }, R = () => {
33
- T({ id: w });
34
- }, z = () => _ ? /* @__PURE__ */ l(n, {
35
- ...L,
36
- href: _,
37
- target: v,
38
- "data-pihanga": E,
39
- label: p ?? m,
40
- onClick: R,
41
- children: I
42
- }) : /* @__PURE__ */ l(t, {
43
- ...L,
44
- "data-pihanga": E,
45
- label: p ?? m,
46
- onClick: R,
47
- children: I
13
+ var p = (e) => {
14
+ let { opts: p, ariaLabel: m, label: h, iconLabel: g, contentCard: _, href: v, target: y, tooltip: b, tooltipCard: x, tooltipMode: S, tooltipPlacement: C, className: w, id: T, onClicked: E, cardName: D, _cls: O, active: k, focused: A, disabled: j, loading: M, isPending: N, formAction: P } = e, F = o(), I = s(), L = f(p?.beforeIcon), R = f(p?.afterIcon), z = /* @__PURE__ */ d(l, { children: [
15
+ L,
16
+ g ? f(g) : _ ? /* @__PURE__ */ u(c, {
17
+ cardName: _,
18
+ parentCard: D
19
+ }) : h,
20
+ R
21
+ ] }), B = {
22
+ active: k,
23
+ focused: A,
24
+ isMenu: p?.isMenu,
25
+ truncate: p?.truncate,
26
+ variant: p?.variant,
27
+ size: p?.size,
28
+ disabled: j,
29
+ loading: M,
30
+ isPending: N,
31
+ loadingClassName: p?.loadingClassName,
32
+ className: `${O("root")} ${w || ""}`.trim()
33
+ }, V = !!P && I.isInForm, H = () => {
34
+ V ? I.requestAction(P) : E({ id: T });
35
+ }, U = P === "submit" ? "submit" : "button", W = () => v ? /* @__PURE__ */ u(n, {
36
+ ...B,
37
+ href: v,
38
+ target: y,
39
+ "data-pihanga": D,
40
+ label: m ?? h,
41
+ onClick: H,
42
+ children: z
43
+ }) : /* @__PURE__ */ u(t, {
44
+ ...B,
45
+ type: U,
46
+ "data-pihanga": D,
47
+ label: m ?? h,
48
+ onClick: H,
49
+ children: z
48
50
  });
49
- if (!y && !b) return z();
50
- let B = N ? !1 : x === "open" ? !0 : x === "closed" ? !1 : void 0, V = b ? /* @__PURE__ */ l(s, {
51
- cardName: b,
52
- parentCard: E
53
- }) : /* @__PURE__ */ l("p", {
54
- "data-pihanga": `${E}-tooltip`,
55
- children: y
51
+ if (!b && !x) return W();
52
+ let G = F ? !1 : S === "open" ? !0 : S === "closed" ? !1 : void 0, K = x ? /* @__PURE__ */ u(c, {
53
+ cardName: x,
54
+ parentCard: D
55
+ }) : /* @__PURE__ */ u("p", {
56
+ "data-pihanga": `${D}-tooltip`,
57
+ children: b
56
58
  });
57
- return /* @__PURE__ */ u(r, {
58
- open: B,
59
- children: [/* @__PURE__ */ l(a, {
59
+ return /* @__PURE__ */ d(r, {
60
+ open: G,
61
+ children: [/* @__PURE__ */ u(a, {
60
62
  asChild: !0,
61
- children: z()
62
- }), /* @__PURE__ */ l(i, {
63
- side: S,
64
- children: V
63
+ children: W()
64
+ }), /* @__PURE__ */ u(i, {
65
+ side: C,
66
+ children: K
65
67
  })]
66
68
  });
67
69
  };
68
70
  //#endregion
69
- export { f as ButtonComponent };
71
+ export { p as ButtonComponent };
70
72
 
71
73
  //# sourceMappingURL=button.component.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"button.component.js","names":[],"sources":["../../../src/cards/button/button.component.tsx"],"sourcesContent":["import React from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\n\nimport {Button, LinkButton} from \"@/registry/ui/button\";\nimport {Tooltip, TooltipContent, TooltipTrigger} from \"@/registry/ui/tooltip\";\nimport {getIcon} from \"@/cards/icons\";\nimport {useDropdownOpen} from \"@/cards/dropDownMenu/dropdown-context\";\n\nimport type {PiButtonEvents, PiButtonProps} from \"./button.types\";\n\nfunction resolveIconByName(name?: string): React.ReactNode {\n if (!name) return null;\n return getIcon(name);\n}\n\nexport const ButtonComponent = (\n props: PiCardProps<PiButtonProps, PiButtonEvents>,\n): React.ReactNode => {\n const {\n opts,\n ariaLabel,\n label,\n iconLabel,\n contentCard: content,\n href,\n target,\n tooltip,\n tooltipCard,\n tooltipMode,\n tooltipPlacement,\n className,\n id,\n onClicked,\n cardName,\n _cls,\n active,\n focused,\n disabled,\n loading,\n isPending,\n } = props;\n\n // Check if this button is inside an open dropdown menu\n const isInsideOpenDropdown = useDropdownOpen();\n\n const beforeIcon = resolveIconByName(opts?.beforeIcon);\n const afterIcon = resolveIconByName(opts?.afterIcon);\n\n // Determine base content: iconLabel takes precedence, then contentCard, then label\n const baseContent = iconLabel ? (\n resolveIconByName(iconLabel)\n ) : content ? (\n <Card cardName={content} parentCard={cardName} />\n ) : (\n label\n );\n\n // Render icons as children following shadcn/ui pattern\n const children = (\n <>\n {beforeIcon}\n {baseContent}\n {afterIcon}\n </>\n );\n\n const common = {\n active,\n focused,\n isMenu: opts?.isMenu,\n truncate: opts?.truncate,\n variant: opts?.variant,\n size: opts?.size,\n\n disabled,\n loading,\n isPending,\n loadingClassName: opts?.loadingClassName,\n\n className: `${_cls(\"root\")} ${className || \"\"}`.trim(),\n };\n\n const handleClick = () => {\n onClicked({id});\n };\n\n const renderButton = () => {\n if (href) {\n return (\n <LinkButton\n {...common}\n href={href}\n target={target}\n data-pihanga={cardName}\n label={ariaLabel ?? label}\n onClick={handleClick}\n >\n {children}\n </LinkButton>\n );\n }\n\n return (\n <Button\n {...common}\n data-pihanga={cardName}\n label={ariaLabel ?? label}\n onClick={handleClick}\n >\n {children}\n </Button>\n );\n };\n\n if (!tooltip && !tooltipCard) return renderButton();\n\n // Determine tooltip open state based on mode.\n // If the button is inside an open dropdown, force the tooltip closed\n // to avoid UI conflicts (dropdown menu should take precedence).\n const tooltipOpen = isInsideOpenDropdown\n ? false\n : tooltipMode === \"open\"\n ? true\n : tooltipMode === \"closed\"\n ? false\n : undefined; // 'auto' or undefined = uncontrolled\n\n // Render tooltip content: use tooltipCard if provided, otherwise use tooltip string\n const tooltipContent = tooltipCard ? (\n <Card cardName={tooltipCard} parentCard={cardName} />\n ) : (\n <p data-pihanga={`${cardName}-tooltip`}>{tooltip}</p>\n );\n\n return (\n <Tooltip open={tooltipOpen}>\n <TooltipTrigger asChild>{renderButton()}</TooltipTrigger>\n <TooltipContent side={tooltipPlacement}>{tooltipContent}</TooltipContent>\n </Tooltip>\n );\n};\n"],"mappings":";;;;;;;;AAUA,SAAS,EAAkB,GAAgC;CAEzD,OADK,IACE,EAAQ,CAAI,IADD;AAEpB;AAEA,IAAa,KACX,MACoB;CACpB,IAAM,EACJ,SACA,cACA,UACA,cACA,aAAa,GACb,SACA,WACA,YACA,gBACA,gBACA,qBACA,cACA,OACA,cACA,aACA,SACA,WACA,YACA,aACA,YACA,iBACE,GAGE,IAAuB,EAAgB,GAEvC,IAAa,EAAkB,GAAM,UAAU,GAC/C,IAAY,EAAkB,GAAM,SAAS,GAY7C,IACJ,kBAAA,GAAA,EAAA,UAAA;EACG;EAXe,IAClB,EAAkB,CAAS,IACzB,IACF,kBAAC,GAAD;GAAM,UAAU;GAAS,YAAY;EAAW,CAAA,IAEhD;EAQG;CACD,EAAA,CAAA,GAGE,IAAS;EACb;EACA;EACA,QAAQ,GAAM;EACd,UAAU,GAAM;EAChB,SAAS,GAAM;EACf,MAAM,GAAM;EAEZ;EACA;EACA;EACA,kBAAkB,GAAM;EAExB,WAAW,GAAG,EAAK,MAAM,EAAE,GAAG,KAAa,KAAK,KAAK;CACvD,GAEM,UAAoB;EACxB,EAAU,EAAC,MAAE,CAAC;CAChB,GAEM,UACA,IAEA,kBAAC,GAAD;EACE,GAAI;EACE;EACE;EACR,gBAAc;EACd,OAAO,KAAa;EACpB,SAAS;EAER;CACS,CAAA,IAKd,kBAAC,GAAD;EACE,GAAI;EACJ,gBAAc;EACd,OAAO,KAAa;EACpB,SAAS;EAER;CACK,CAAA;CAIZ,IAAI,CAAC,KAAW,CAAC,GAAa,OAAO,EAAa;CAKlD,IAAM,IAAc,IAChB,KACA,MAAgB,SACd,KACA,MAAgB,WACd,KACA,KAAA,GAGF,IAAiB,IACrB,kBAAC,GAAD;EAAM,UAAU;EAAa,YAAY;CAAW,CAAA,IAEpD,kBAAC,KAAD;EAAG,gBAAc,GAAG,EAAS;YAAY;CAAW,CAAA;CAGtD,OACE,kBAAC,GAAD;EAAS,MAAM;YAAf,CACE,kBAAC,GAAD;GAAgB,SAAA;aAAS,EAAa;EAAkB,CAAA,GACxD,kBAAC,GAAD;GAAgB,MAAM;aAAmB;EAA+B,CAAA,CACjE;;AAEb"}
1
+ {"version":3,"file":"button.component.js","names":[],"sources":["../../../src/cards/button/button.component.tsx"],"sourcesContent":["import React from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\n\nimport {Button, LinkButton} from \"@/registry/ui/button\";\nimport {Tooltip, TooltipContent, TooltipTrigger} from \"@/registry/ui/tooltip\";\nimport {getIcon} from \"@/cards/icons\";\nimport {useDropdownOpen} from \"@/cards/dropDownMenu/dropdown-context\";\nimport {useFormContext} from \"@/cards/form/form.context\";\n\nimport type {PiButtonEvents, PiButtonProps} from \"./button.types\";\n\nfunction resolveIconByName(name?: string): React.ReactNode {\n if (!name) return null;\n return getIcon(name);\n}\n\nexport const ButtonComponent = (\n props: PiCardProps<PiButtonProps, PiButtonEvents>,\n): React.ReactNode => {\n const {\n opts,\n ariaLabel,\n label,\n iconLabel,\n contentCard: content,\n href,\n target,\n tooltip,\n tooltipCard,\n tooltipMode,\n tooltipPlacement,\n className,\n id,\n onClicked,\n cardName,\n _cls,\n active,\n focused,\n disabled,\n loading,\n isPending,\n formAction,\n } = props;\n\n // Check if this button is inside an open dropdown menu\n const isInsideOpenDropdown = useDropdownOpen();\n\n // Detect if we are inside a pi/form card via React context, so\n // `formAction` can request a form-level action instead of onClicked.\n const form = useFormContext();\n\n const beforeIcon = resolveIconByName(opts?.beforeIcon);\n const afterIcon = resolveIconByName(opts?.afterIcon);\n\n // Determine base content: iconLabel takes precedence, then contentCard, then label\n const baseContent = iconLabel ? (\n resolveIconByName(iconLabel)\n ) : content ? (\n <Card cardName={content} parentCard={cardName} />\n ) : (\n label\n );\n\n // Render icons as children following shadcn/ui pattern\n const children = (\n <>\n {beforeIcon}\n {baseContent}\n {afterIcon}\n </>\n );\n\n const common = {\n active,\n focused,\n isMenu: opts?.isMenu,\n truncate: opts?.truncate,\n variant: opts?.variant,\n size: opts?.size,\n\n disabled,\n loading,\n isPending,\n loadingClassName: opts?.loadingClassName,\n\n className: `${_cls(\"root\")} ${className || \"\"}`.trim(),\n };\n\n // When `formAction` is set and this button is inside a pi/form, clicking\n // it requests that form-level action instead of firing this button's own\n // onClicked.\n const useFormAction = Boolean(formAction) && form.isInForm;\n\n const handleClick = () => {\n if (useFormAction) {\n form.requestAction(formAction!);\n } else {\n onClicked({id});\n }\n };\n\n // Native <button type> mirrors `formAction` so the button can never\n // accidentally submit a form it isn't supposed to: \"submit\" is the only\n // value that should trigger the browser's native form submit; everything\n // else (including no formAction at all) is a plain, non-submitting button.\n const htmlType = formAction === \"submit\" ? \"submit\" : \"button\";\n\n const renderButton = () => {\n if (href) {\n return (\n <LinkButton\n {...common}\n href={href}\n target={target}\n data-pihanga={cardName}\n label={ariaLabel ?? label}\n onClick={handleClick}\n >\n {children}\n </LinkButton>\n );\n }\n\n return (\n <Button\n {...common}\n type={htmlType}\n data-pihanga={cardName}\n label={ariaLabel ?? label}\n onClick={handleClick}\n >\n {children}\n </Button>\n );\n };\n\n if (!tooltip && !tooltipCard) return renderButton();\n\n // Determine tooltip open state based on mode.\n // If the button is inside an open dropdown, force the tooltip closed\n // to avoid UI conflicts (dropdown menu should take precedence).\n const tooltipOpen = isInsideOpenDropdown\n ? false\n : tooltipMode === \"open\"\n ? true\n : tooltipMode === \"closed\"\n ? false\n : undefined; // 'auto' or undefined = uncontrolled\n\n // Render tooltip content: use tooltipCard if provided, otherwise use tooltip string\n const tooltipContent = tooltipCard ? (\n <Card cardName={tooltipCard} parentCard={cardName} />\n ) : (\n <p data-pihanga={`${cardName}-tooltip`}>{tooltip}</p>\n );\n\n return (\n <Tooltip open={tooltipOpen}>\n <TooltipTrigger asChild>{renderButton()}</TooltipTrigger>\n <TooltipContent side={tooltipPlacement}>{tooltipContent}</TooltipContent>\n </Tooltip>\n );\n};\n"],"mappings":";;;;;;;;;AAWA,SAAS,EAAkB,GAAgC;CAEzD,OADK,IACE,EAAQ,CAAI,IADD;AAEpB;AAEA,IAAa,KACX,MACoB;CACpB,IAAM,EACJ,SACA,cACA,UACA,cACA,aAAa,GACb,SACA,WACA,YACA,gBACA,gBACA,qBACA,cACA,OACA,cACA,aACA,SACA,WACA,YACA,aACA,YACA,cACA,kBACE,GAGE,IAAuB,EAAgB,GAIvC,IAAO,EAAe,GAEtB,IAAa,EAAkB,GAAM,UAAU,GAC/C,IAAY,EAAkB,GAAM,SAAS,GAY7C,IACJ,kBAAA,GAAA,EAAA,UAAA;EACG;EAXe,IAClB,EAAkB,CAAS,IACzB,IACF,kBAAC,GAAD;GAAM,UAAU;GAAS,YAAY;EAAW,CAAA,IAEhD;EAQG;CACD,EAAA,CAAA,GAGE,IAAS;EACb;EACA;EACA,QAAQ,GAAM;EACd,UAAU,GAAM;EAChB,SAAS,GAAM;EACf,MAAM,GAAM;EAEZ;EACA;EACA;EACA,kBAAkB,GAAM;EAExB,WAAW,GAAG,EAAK,MAAM,EAAE,GAAG,KAAa,KAAK,KAAK;CACvD,GAKM,IAAgB,EAAQ,KAAe,EAAK,UAE5C,UAAoB;EACxB,AAAI,IACF,EAAK,cAAc,CAAW,IAE9B,EAAU,EAAC,MAAE,CAAC;CAElB,GAMM,IAAW,MAAe,WAAW,WAAW,UAEhD,UACA,IAEA,kBAAC,GAAD;EACE,GAAI;EACE;EACE;EACR,gBAAc;EACd,OAAO,KAAa;EACpB,SAAS;EAER;CACS,CAAA,IAKd,kBAAC,GAAD;EACE,GAAI;EACJ,MAAM;EACN,gBAAc;EACd,OAAO,KAAa;EACpB,SAAS;EAER;CACK,CAAA;CAIZ,IAAI,CAAC,KAAW,CAAC,GAAa,OAAO,EAAa;CAKlD,IAAM,IAAc,IAChB,KACA,MAAgB,SACd,KACA,MAAgB,WACd,KACA,KAAA,GAGF,IAAiB,IACrB,kBAAC,GAAD;EAAM,UAAU;EAAa,YAAY;CAAW,CAAA,IAEpD,kBAAC,KAAD;EAAG,gBAAc,GAAG,EAAS;YAAY;CAAW,CAAA;CAGtD,OACE,kBAAC,GAAD;EAAS,MAAM;YAAf,CACE,kBAAC,GAAD;GAAgB,SAAA;aAAS,EAAa;EAAkB,CAAA,GACxD,kBAAC,GAAD;GAAgB,MAAM;aAAmB;EAA+B,CAAA,CACjE;;AAEb"}
@@ -1,4 +1,5 @@
1
1
  import { PiCardRef } from '@pihanga2/core';
2
+ import { FormAction } from '../form/form.types';
2
3
  export declare const PI_BUTTON_CARD = "pi/button";
3
4
  export declare const Button: <S extends import('@pihanga2/core').ReduxState>(p: import('@pihanga2/core').PiMapProps<PiButtonProps, S, PiButtonEvents>) => import('@pihanga2/core').PiCardDef;
4
5
  export declare const PI_BUTTON_ACTION: {
@@ -100,6 +101,29 @@ export type PiButtonProps = {
100
101
  loading?: boolean;
101
102
  /** If true, indicates an async operation is pending (similar to loading but distinct). */
102
103
  isPending?: boolean;
104
+ /**
105
+ * When set and this button is rendered inside a `pi/form` card, clicking
106
+ * it requests that top-level form action instead of firing this button's
107
+ * own `onClicked`:
108
+ *
109
+ * - `"submit"` — same as pressing the form's native submit button; fires
110
+ * the Form's `onSubmitted` event with a snapshot of the current field
111
+ * values.
112
+ * - `"cancel"` — fires the Form's `onCancelled` event. Never submits or
113
+ * resets the form.
114
+ *
115
+ * The button's native `<button type>` is derived from this prop so it can
116
+ * never accidentally submit a form it isn't supposed to: `"submit"` →
117
+ * `type="submit"`, `"cancel"` (or unset) → `type="button"`.
118
+ *
119
+ * Has no effect when the button is used outside a `pi/form` — in that case
120
+ * `onClicked` always fires as usual, and the native type is `"button"`.
121
+ *
122
+ * This is the button-specific counterpart of `FormFieldProps`/`name` used
123
+ * by form-bindable fields (`pi/input`, `pi/switch`, …) — together they
124
+ * form the complete `pi/form` interaction contract.
125
+ */
126
+ formAction?: FormAction;
103
127
  className?: string;
104
128
  };
105
129
  export type PiButtonClickedEvent = {
@@ -1 +1 @@
1
- {"version":3,"file":"button.types.js","names":[],"sources":["../../../src/cards/button/button.types.ts"],"sourcesContent":["import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_BUTTON_CARD = \"pi/button\";\n\nexport const Button = createCardDeclaration<PiButtonProps, PiButtonEvents>(\n PI_BUTTON_CARD,\n);\n\nexport const PI_BUTTON_ACTION = registerActions(PI_BUTTON_CARD, [\"clicked\"]);\n\nexport const onButtonClicked = createOnAction<PiButtonClickedEvent>(\n PI_BUTTON_ACTION.CLICKED,\n);\n\n/** @deprecated Use `onButtonClicked` instead. */\nexport const onPiButtonClicked = onButtonClicked;\n\n/**\n * Declarative \"opts\" subset based on `src/registry/ui/button.tsx`.\n *\n * Notes:\n * - Any icon is referenced by name and resolved via `src/pihanga/icons.ts`.\n * - `children` are provided via `label` (simple) or `content` (card ref).\n */\nexport type PiButtonOpts = {\n /**\n * Visual style variant of the button.\n * Determines the button's appearance (colors, borders, etc.).\n */\n variant?:\n | \"default\"\n | \"destructive\"\n | \"outline\"\n | \"secondary\"\n | \"ghost\"\n | \"ghost2\"\n | \"ghost3\"\n | \"ghostActive\"\n | \"brand\"\n | \"nav\"\n | \"navAction\"\n | \"menuAction\"\n | \"blockAction\"\n | \"blockActionSecondary\"\n | \"primaryOutline\"\n | \"radio\"\n | \"none\";\n\n /**\n * Size of the button.\n * Controls padding, font size, and overall dimensions.\n */\n size?:\n | \"default\"\n | \"xs\"\n | \"sm\"\n | \"md\"\n | \"lg\"\n | \"icon\"\n | \"iconSm\"\n | \"navAction\"\n | \"menuAction\"\n | \"blockAction\"\n | \"none\";\n\n /** Truncate button content with ellipsis if it overflows. */\n truncate?: boolean;\n\n /** If true, button is rendered as a menu trigger button. */\n isMenu?: boolean;\n\n /** Custom CSS class to apply to the loading spinner/indicator. */\n loadingClassName?: string;\n\n /** Icon to display before the button content. Set 'size' to 'icon' if ONLY show icon */\n beforeIcon?: string;\n\n /** Icon to display after the button content */\n afterIcon?: string;\n};\n\nexport type PiButtonProps = {\n id?: string; // optional 'id' to be added to events\n\n /**\n * Optional explicit aria-label.\n *\n * Useful for icon-only / letter-only buttons where the visible content\n * shouldn't be used as the accessible name.\n */\n ariaLabel?: string;\n\n /**\n * Declarative options; maps closely to `@/registry/ui/button`.\n */\n opts?: PiButtonOpts;\n\n /** Simple label; used as children and aria-label fallback. */\n label?: string;\n\n /**\n * Icon name to use as the main button content (instead of text label).\n * Resolved via `src/pihanga/icons.ts`.\n * Takes precedence over `label` and `contentCard`.\n */\n iconLabel?: string;\n\n /**\n * If set, renders this card as the content instead of `label`.\n * (Useful for rich content.)\n */\n contentCard?: PiCardRef;\n\n /** When provided, button will render as an anchor. */\n href?: string;\n target?: string;\n\n tooltip?: string;\n\n /**\n * If set, renders this card as the tooltip content instead of `tooltip`.\n * (Useful for rich tooltip content.)\n */\n tooltipCard?: PiCardRef;\n\n /**\n * Controls tooltip visibility mode.\n * - 'auto' (default): Normal hover behavior\n * - 'open': Force tooltip to be visible\n * - 'closed': Force tooltip to be hidden\n */\n tooltipMode?: \"auto\" | \"open\" | \"closed\";\n\n /**\n * Controls tooltip placement/position.\n * - 'top': Tooltip appears above the button (default)\n * - 'right': Tooltip appears to the right of the button\n * - 'bottom': Tooltip appears below the button\n * - 'left': Tooltip appears to the left of the button\n */\n tooltipPlacement?: \"top\" | \"right\" | \"bottom\" | \"left\";\n\n /** Indicates whether the button is in an active/selected state. */\n active?: boolean;\n\n /** Indicates whether the button has focus state styling applied. */\n focused?: boolean;\n\n /** If true, disables the button and prevents interaction. */\n disabled?: boolean;\n\n /** If true, shows loading state and typically disables interaction. */\n loading?: boolean;\n\n /** If true, indicates an async operation is pending (similar to loading but distinct). */\n isPending?: boolean;\n\n className?: string;\n};\n\nexport type PiButtonClickedEvent = {\n id?: string;\n};\n\nexport type PiButtonEvents = {\n onClicked: PiButtonClickedEvent;\n};\n"],"mappings":";;AAOA,IAAa,IAAiB,aAEjB,IAAS,EACpB,CACF,GAEa,IAAmB,EAAgB,GAAgB,CAAC,SAAS,CAAC,GAE9D,IAAkB,EAC7B,EAAiB,OACnB,GAGa,IAAoB"}
1
+ {"version":3,"file":"button.types.js","names":[],"sources":["../../../src/cards/button/button.types.ts"],"sourcesContent":["import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\nimport type {FormAction} from \"@/cards/form/form.types\";\n\nexport const PI_BUTTON_CARD = \"pi/button\";\n\nexport const Button = createCardDeclaration<PiButtonProps, PiButtonEvents>(\n PI_BUTTON_CARD,\n);\n\nexport const PI_BUTTON_ACTION = registerActions(PI_BUTTON_CARD, [\"clicked\"]);\n\nexport const onButtonClicked = createOnAction<PiButtonClickedEvent>(\n PI_BUTTON_ACTION.CLICKED,\n);\n\n/** @deprecated Use `onButtonClicked` instead. */\nexport const onPiButtonClicked = onButtonClicked;\n\n/**\n * Declarative \"opts\" subset based on `src/registry/ui/button.tsx`.\n *\n * Notes:\n * - Any icon is referenced by name and resolved via `src/pihanga/icons.ts`.\n * - `children` are provided via `label` (simple) or `content` (card ref).\n */\nexport type PiButtonOpts = {\n /**\n * Visual style variant of the button.\n * Determines the button's appearance (colors, borders, etc.).\n */\n variant?:\n | \"default\"\n | \"destructive\"\n | \"outline\"\n | \"secondary\"\n | \"ghost\"\n | \"ghost2\"\n | \"ghost3\"\n | \"ghostActive\"\n | \"brand\"\n | \"nav\"\n | \"navAction\"\n | \"menuAction\"\n | \"blockAction\"\n | \"blockActionSecondary\"\n | \"primaryOutline\"\n | \"radio\"\n | \"none\";\n\n /**\n * Size of the button.\n * Controls padding, font size, and overall dimensions.\n */\n size?:\n | \"default\"\n | \"xs\"\n | \"sm\"\n | \"md\"\n | \"lg\"\n | \"icon\"\n | \"iconSm\"\n | \"navAction\"\n | \"menuAction\"\n | \"blockAction\"\n | \"none\";\n\n /** Truncate button content with ellipsis if it overflows. */\n truncate?: boolean;\n\n /** If true, button is rendered as a menu trigger button. */\n isMenu?: boolean;\n\n /** Custom CSS class to apply to the loading spinner/indicator. */\n loadingClassName?: string;\n\n /** Icon to display before the button content. Set 'size' to 'icon' if ONLY show icon */\n beforeIcon?: string;\n\n /** Icon to display after the button content */\n afterIcon?: string;\n};\n\nexport type PiButtonProps = {\n id?: string; // optional 'id' to be added to events\n\n /**\n * Optional explicit aria-label.\n *\n * Useful for icon-only / letter-only buttons where the visible content\n * shouldn't be used as the accessible name.\n */\n ariaLabel?: string;\n\n /**\n * Declarative options; maps closely to `@/registry/ui/button`.\n */\n opts?: PiButtonOpts;\n\n /** Simple label; used as children and aria-label fallback. */\n label?: string;\n\n /**\n * Icon name to use as the main button content (instead of text label).\n * Resolved via `src/pihanga/icons.ts`.\n * Takes precedence over `label` and `contentCard`.\n */\n iconLabel?: string;\n\n /**\n * If set, renders this card as the content instead of `label`.\n * (Useful for rich content.)\n */\n contentCard?: PiCardRef;\n\n /** When provided, button will render as an anchor. */\n href?: string;\n target?: string;\n\n tooltip?: string;\n\n /**\n * If set, renders this card as the tooltip content instead of `tooltip`.\n * (Useful for rich tooltip content.)\n */\n tooltipCard?: PiCardRef;\n\n /**\n * Controls tooltip visibility mode.\n * - 'auto' (default): Normal hover behavior\n * - 'open': Force tooltip to be visible\n * - 'closed': Force tooltip to be hidden\n */\n tooltipMode?: \"auto\" | \"open\" | \"closed\";\n\n /**\n * Controls tooltip placement/position.\n * - 'top': Tooltip appears above the button (default)\n * - 'right': Tooltip appears to the right of the button\n * - 'bottom': Tooltip appears below the button\n * - 'left': Tooltip appears to the left of the button\n */\n tooltipPlacement?: \"top\" | \"right\" | \"bottom\" | \"left\";\n\n /** Indicates whether the button is in an active/selected state. */\n active?: boolean;\n\n /** Indicates whether the button has focus state styling applied. */\n focused?: boolean;\n\n /** If true, disables the button and prevents interaction. */\n disabled?: boolean;\n\n /** If true, shows loading state and typically disables interaction. */\n loading?: boolean;\n\n /** If true, indicates an async operation is pending (similar to loading but distinct). */\n isPending?: boolean;\n\n /**\n * When set and this button is rendered inside a `pi/form` card, clicking\n * it requests that top-level form action instead of firing this button's\n * own `onClicked`:\n *\n * - `\"submit\"` — same as pressing the form's native submit button; fires\n * the Form's `onSubmitted` event with a snapshot of the current field\n * values.\n * - `\"cancel\"` — fires the Form's `onCancelled` event. Never submits or\n * resets the form.\n *\n * The button's native `<button type>` is derived from this prop so it can\n * never accidentally submit a form it isn't supposed to: `\"submit\"` →\n * `type=\"submit\"`, `\"cancel\"` (or unset) → `type=\"button\"`.\n *\n * Has no effect when the button is used outside a `pi/form` — in that case\n * `onClicked` always fires as usual, and the native type is `\"button\"`.\n *\n * This is the button-specific counterpart of `FormFieldProps`/`name` used\n * by form-bindable fields (`pi/input`, `pi/switch`, …) — together they\n * form the complete `pi/form` interaction contract.\n */\n formAction?: FormAction;\n\n className?: string;\n};\n\nexport type PiButtonClickedEvent = {\n id?: string;\n};\n\nexport type PiButtonEvents = {\n onClicked: PiButtonClickedEvent;\n};\n"],"mappings":";;AAQA,IAAa,IAAiB,aAEjB,IAAS,EACpB,CACF,GAEa,IAAmB,EAAgB,GAAgB,CAAC,SAAS,CAAC,GAE9D,IAAkB,EAC7B,EAAiB,OACnB,GAGa,IAAoB"}
@@ -1,3 +1,4 @@
1
+ import { FormFieldProps } from '../form/form.types';
1
2
  export declare const PI_CHECKBOX_CARD = "pi/checkbox";
2
3
  export declare const Checkbox: <S extends import('@pihanga2/core').ReduxState>(p: import('@pihanga2/core').PiMapProps<PiCheckboxProps, S, PiCheckboxEvents>) => import('@pihanga2/core').PiCardDef;
3
4
  export declare const PI_CHECKBOX_ACTION: {
@@ -10,13 +11,7 @@ export declare const onCheckboxChanged: <S extends import('@pihanga2/core').Redu
10
11
  export declare const onPiCheckboxChanged: <S extends import('@pihanga2/core').ReduxState>(register: import('@pihanga2/core/types').PiRegisterMinimal, f: import('@pihanga2/core').ReduceF<S, import('@pihanga2/core').ReduxAction & {
11
12
  cardID: string;
12
13
  } & PiCheckboxChangedEvent>) => void;
13
- export type PiCheckboxProps = {
14
- /**
15
- * Field name used to bind to FormContext when inside a pi/form card.
16
- * When provided the component reads its checked state from form data and
17
- * writes back via form.handleChange.
18
- */
19
- name?: string;
14
+ export type PiCheckboxProps = FormFieldProps & {
20
15
  /**
21
16
  * Controlled checked state used in standalone mode (outside a Form).
22
17
  * Ignored when `name` is set and the component is inside a pi/form.
@@ -1 +1 @@
1
- {"version":3,"file":"checkbox.types.js","names":[],"sources":["../../../src/cards/checkbox/checkbox.types.ts"],"sourcesContent":["import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_CHECKBOX_CARD = \"pi/checkbox\";\n\nexport const Checkbox = createCardDeclaration<\n PiCheckboxProps,\n PiCheckboxEvents\n>(PI_CHECKBOX_CARD);\n\nexport const PI_CHECKBOX_ACTION = registerActions(PI_CHECKBOX_CARD, [\n \"changed\",\n]);\n\nexport const onCheckboxChanged = createOnAction<PiCheckboxChangedEvent>(\n PI_CHECKBOX_ACTION.CHANGED,\n);\n\n/** @deprecated Use `onCheckboxChanged` instead. */\nexport const onPiCheckboxChanged = onCheckboxChanged;\n\nexport type PiCheckboxProps = {\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 checkbox is disabled and non-interactive. */\n disabled?: boolean;\n\n /**\n * Optional label text rendered next to the checkbox.\n * The label is automatically associated with the input via `htmlFor`.\n */\n label?: string;\n};\n\nexport type PiCheckboxChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** New checked state after the change. */\n checked: boolean;\n};\n\nexport type PiCheckboxEvents = {\n onChanged: PiCheckboxChangedEvent;\n};\n"],"mappings":";;AAMA,IAAa,IAAmB,eAEnB,IAAW,EAGtB,CAAgB,GAEL,IAAqB,EAAgB,GAAkB,CAClE,SACF,CAAC,GAEY,IAAoB,EAC/B,EAAmB,OACrB,GAGa,IAAsB"}
1
+ {"version":3,"file":"checkbox.types.js","names":[],"sources":["../../../src/cards/checkbox/checkbox.types.ts"],"sourcesContent":["import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\nimport type {FormFieldProps} from \"@/cards/form/form.types\";\n\nexport const PI_CHECKBOX_CARD = \"pi/checkbox\";\n\nexport const Checkbox = createCardDeclaration<\n PiCheckboxProps,\n PiCheckboxEvents\n>(PI_CHECKBOX_CARD);\n\nexport const PI_CHECKBOX_ACTION = registerActions(PI_CHECKBOX_CARD, [\n \"changed\",\n]);\n\nexport const onCheckboxChanged = createOnAction<PiCheckboxChangedEvent>(\n PI_CHECKBOX_ACTION.CHANGED,\n);\n\n/** @deprecated Use `onCheckboxChanged` instead. */\nexport const onPiCheckboxChanged = onCheckboxChanged;\n\nexport type PiCheckboxProps = FormFieldProps & {\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 checkbox is disabled and non-interactive. */\n disabled?: boolean;\n\n /**\n * Optional label text rendered next to the checkbox.\n * The label is automatically associated with the input via `htmlFor`.\n */\n label?: string;\n};\n\nexport type PiCheckboxChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** New checked state after the change. */\n checked: boolean;\n};\n\nexport type PiCheckboxEvents = {\n onChanged: PiCheckboxChangedEvent;\n};\n"],"mappings":";;AAOA,IAAa,IAAmB,eAEnB,IAAW,EAGtB,CAAgB,GAEL,IAAqB,EAAgB,GAAkB,CAClE,SACF,CAAC,GAEY,IAAoB,EAC/B,EAAmB,OACrB,GAGa,IAAsB"}
@@ -22,7 +22,7 @@ var d = (d) => {
22
22
  className: e(_),
23
23
  "data-pihanga": v,
24
24
  children: [
25
- /* @__PURE__ */ l(a, {
25
+ f && /* @__PURE__ */ l(a, {
26
26
  htmlFor: x,
27
27
  children: f
28
28
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"field.component.js","names":[],"sources":["../../../src/cards/field/field.component.tsx"],"sourcesContent":["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"],"mappings":";;;;;;;AAYA,IAAa,KACX,MACoB;CACpB,IAAM,EACJ,UACA,cACA,SACA,gBACA,OAAO,GACP,cACA,gBACE,GAGE,IAAO,EAAe,GACtB,IAAQ,EAAK,YAAY,IAAO,EAAK,OAAO,KAAQ,GAGpD,IAAU,EAAM,MAAM,GAKtB,IAAY,EAAQ,GAQpB,IAAiB,SACd;EACL,UAAU;EACV,YAAY;EACZ,IAAI;EACJ,SAAS;CACX,IACA;EAAC;EAAU;EAAW;EAAS;CAAS,CAC1C;CAEA,OACE,kBAAC,GAAD;EACE,gBAAc,EAAQ,KAAU,KAAA;EAChC,WAAW,EAAG,CAAS;EACvB,gBAAc;YAHhB;GAKE,kBAAC,GAAD;IAAY,SAAS;cAAU;GAAkB,CAAA;GAEjD,kBAAC,GAAD,EAAM,GAAI,EAAiB,CAAA;GAE1B,KAAe,kBAAC,GAAD,EAAA,UAAmB,EAA8B,CAAA;GACjE,kBAAC,GAAD,EAAA,UAAa,EAAkB,CAAA;EAC1B;;AAEX"}
1
+ {"version":3,"file":"field.component.js","names":[],"sources":["../../../src/cards/field/field.component.tsx"],"sourcesContent":["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 {label && <FieldLabel htmlFor={fieldId}>{label}</FieldLabel>}\n\n <Card {...fieldCardProps} />\n\n {description && <FieldDescription>{description}</FieldDescription>}\n <FieldError>{error}</FieldError>\n </Field>\n );\n};\n"],"mappings":";;;;;;;AAYA,IAAa,KACX,MACoB;CACpB,IAAM,EACJ,UACA,cACA,SACA,gBACA,OAAO,GACP,cACA,gBACE,GAGE,IAAO,EAAe,GACtB,IAAQ,EAAK,YAAY,IAAO,EAAK,OAAO,KAAQ,GAGpD,IAAU,EAAM,MAAM,GAKtB,IAAY,EAAQ,GAQpB,IAAiB,SACd;EACL,UAAU;EACV,YAAY;EACZ,IAAI;EACJ,SAAS;CACX,IACA;EAAC;EAAU;EAAW;EAAS;CAAS,CAC1C;CAEA,OACE,kBAAC,GAAD;EACE,gBAAc,EAAQ,KAAU,KAAA;EAChC,WAAW,EAAG,CAAS;EACvB,gBAAc;YAHhB;GAKG,KAAS,kBAAC,GAAD;IAAY,SAAS;cAAU;GAAkB,CAAA;GAE3D,kBAAC,GAAD,EAAM,GAAI,EAAiB,CAAA;GAE1B,KAAe,kBAAC,GAAD,EAAA,UAAmB,EAA8B,CAAA;GACjE,kBAAC,GAAD,EAAA,UAAa,EAAkB,CAAA;EAC1B;;AAEX"}
@@ -1,21 +1,32 @@
1
1
  import { cn as e } from "../../lib/utils.js";
2
2
  import { FormContext as t } from "./form.context.js";
3
- import { useCallback as n, useState as r } from "react";
4
- import { Card as i } from "@pihanga2/core";
5
- import { jsx as a, jsxs as o } from "react/jsx-runtime";
3
+ import { useCallback as n, useEffect as r, useRef as i, useState as a } from "react";
4
+ import { Card as o } from "@pihanga2/core";
5
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
6
6
  //#region src/cards/form/form.component.tsx
7
- var s = (s) => {
8
- let { id: c, content: l = [], initialValues: u = {}, submitLabel: d = "Submit", className: f, cardName: p, onSubmitted: m } = s, [h, g] = r(() => ({ ...u })), [_, v] = r({}), y = n((e, t) => {
9
- g((n) => ({
7
+ var l = (l) => {
8
+ let { id: u, content: d = [], initialValues: f = {}, submitLabel: p = "Submit", hideSubmit: m = !1, className: h, cardName: g, onSubmitted: _, onCancelled: v, onAbandoned: y } = l, [b, x] = a(() => ({ ...f })), [S, C] = a({}), w = i(!1), T = i(u);
9
+ T.current = u;
10
+ let E = i(b);
11
+ E.current = b;
12
+ let D = i(y);
13
+ D.current = y, r(() => () => {
14
+ w.current || D.current({
15
+ id: T.current,
16
+ formData: E.current
17
+ });
18
+ }, []);
19
+ let O = n((e, t) => {
20
+ x((n) => ({
10
21
  ...n,
11
22
  [e]: t
12
- })), v((t) => {
23
+ })), C((t) => {
13
24
  if (!t[e]) return t;
14
25
  let n = { ...t };
15
26
  return delete n[e], n;
16
27
  });
17
- }, []), b = n((e, t) => {
18
- v((n) => {
28
+ }, []), k = n((e, t) => {
29
+ C((n) => {
19
30
  if (t === null) {
20
31
  let t = { ...n };
21
32
  return delete t[e], t;
@@ -25,40 +36,54 @@ var s = (s) => {
25
36
  [e]: t
26
37
  };
27
38
  });
28
- }, []), x = n((e) => {
29
- e.preventDefault(), m({
30
- id: c,
31
- formData: h
39
+ }, []), A = n(() => {
40
+ w.current = !0, _({
41
+ id: u,
42
+ formData: b
43
+ });
44
+ }, [
45
+ u,
46
+ b,
47
+ _
48
+ ]), j = n(() => {
49
+ w.current = !0, v({
50
+ id: u,
51
+ formData: b
32
52
  });
33
53
  }, [
34
- c,
35
- h,
36
- m
37
- ]), S = {
38
- formData: h,
39
- errors: _,
40
- handleChange: y,
41
- setError: b,
54
+ u,
55
+ b,
56
+ v
57
+ ]), M = n((e) => {
58
+ e.preventDefault(), A();
59
+ }, [A]), N = {
60
+ formData: b,
61
+ errors: S,
62
+ handleChange: O,
63
+ setError: k,
64
+ requestAction: n((e) => {
65
+ e === "submit" ? A() : j();
66
+ }, [A, j]),
42
67
  isInForm: !0
43
68
  };
44
- return /* @__PURE__ */ a(t.Provider, {
45
- value: S,
46
- children: /* @__PURE__ */ o("form", {
47
- onSubmit: x,
48
- className: e("flex flex-col gap-4", f),
49
- "data-pihanga": p,
50
- children: [l.map((e, t) => /* @__PURE__ */ a(i, {
69
+ return /* @__PURE__ */ s(t.Provider, {
70
+ value: N,
71
+ children: /* @__PURE__ */ c("form", {
72
+ onSubmit: M,
73
+ className: e("flex flex-col gap-4", h),
74
+ "data-pihanga": g,
75
+ children: [d.map((e, t) => /* @__PURE__ */ s(o, {
51
76
  cardName: e,
52
- parentCard: p
53
- }, t)), /* @__PURE__ */ a("button", {
77
+ parentCard: g
78
+ }, t)), !m && /* @__PURE__ */ s("button", {
54
79
  type: "submit",
55
80
  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",
56
- children: d
81
+ children: p
57
82
  })]
58
83
  })
59
84
  });
60
85
  };
61
86
  //#endregion
62
- export { s as FormComponent };
87
+ export { l as FormComponent };
63
88
 
64
89
  //# sourceMappingURL=form.component.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"form.component.js","names":[],"sources":["../../../src/cards/form/form.component.tsx"],"sourcesContent":["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"],"mappings":";;;;;;AAMA,IAAa,KACX,MACoB;CACpB,IAAM,EACJ,OACA,aAAU,CAAC,GACX,mBAAgB,CAAC,GACjB,iBAAc,UACd,cACA,aACA,mBACE,GAEE,CAAC,GAAU,KAAe,SAAyC,EACvE,GAAG,EACL,EAAE,GACI,CAAC,GAAQ,KAAa,EAAiC,CAAC,CAAC,GAEzD,IAAe,GAAa,GAAmB,MAAmB;EAGtE,AAFA,GAAa,OAAU;GAAC,GAAG;IAAO,IAAY;EAAK,EAAE,GAErD,GAAW,MAAS;GAClB,IAAI,CAAC,EAAK,IAAY,OAAO;GAC7B,IAAM,IAAO,EAAC,GAAG,EAAI;GAErB,OADA,OAAO,EAAK,IACL;EACT,CAAC;CACH,GAAG,CAAC,CAAC,GAEC,IAAW,GAAa,GAAmB,MAAyB;EACxE,GAAW,MAAS;GAClB,IAAI,MAAU,MAAM;IAClB,IAAM,IAAO,EAAC,GAAG,EAAI;IAErB,OADA,OAAO,EAAK,IACL;GACT;GACA,OAAO;IAAC,GAAG;KAAO,IAAY;GAAK;EACrC,CAAC;CACH,GAAG,CAAC,CAAC,GAEC,IAAe,GAClB,MAAuB;EAEtB,AADA,EAAE,eAAe,GACjB,EAAY;GAAC;GAAI;EAAQ,CAAC;CAC5B,GACA;EAAC;EAAI;EAAU;CAAW,CAC5B,GAEM,IAAiC;EACrC;EACA;EACA;EACA;EACA,UAAU;CACZ;CAEA,OACE,kBAAC,EAAY,UAAb;EAAsB,OAAO;YAC3B,kBAAC,QAAD;GACE,UAAU;GACV,WAAW,EAAG,uBAAuB,CAAS;GAC9C,gBAAc;aAHhB,CAKG,EAAQ,KAAK,GAAM,MAClB,kBAAC,GAAD;IAAgB,UAAU;IAAM,YAAY;GAAW,GAA5C,CAA4C,CACxD,GACD,kBAAC,UAAD;IACE,MAAK;IACL,WAAU;cAET;GACK,CAAA,CACJ;;CACc,CAAA;AAE1B"}
1
+ {"version":3,"file":"form.component.js","names":[],"sources":["../../../src/cards/form/form.component.tsx"],"sourcesContent":["import React, {useState, useCallback, useRef, useEffect} from \"react\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport {cn} from \"@/lib/utils\";\nimport {FormContext, type FormContextValue} from \"./form.context\";\nimport type {FormAction, 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 hideSubmit = false,\n className,\n cardName,\n onSubmitted,\n onCancelled,\n onAbandoned,\n } = props;\n\n const [formData, setFormData] = useState<Record<string, unknown>>(() => ({\n ...initialValues,\n }));\n const [errors, setErrors] = useState<Record<string, string>>({});\n\n // Tracks whether the form reached a \"final\" state (submitted or\n // explicitly cancelled) so the unmount effect below can tell the\n // difference between that and being torn down some other way (e.g. its\n // parent pi/dialog was dismissed) — the latter fires `onAbandoned`.\n const finalizedRef = useRef(false);\n // Mirrors state in refs so the unmount effect (which only runs once) can\n // read the latest values without re-subscribing on every keystroke.\n const idRef = useRef(id);\n idRef.current = id;\n const formDataRef = useRef(formData);\n formDataRef.current = formData;\n const onAbandonedRef = useRef(onAbandoned);\n onAbandonedRef.current = onAbandoned;\n\n useEffect(() => {\n return () => {\n if (!finalizedRef.current) {\n onAbandonedRef.current({id: idRef.current, formData: formDataRef.current});\n }\n };\n // Intentionally run only on mount/unmount.\n \n }, []);\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 doSubmit = useCallback(() => {\n finalizedRef.current = true;\n onSubmitted({id, formData});\n }, [id, formData, onSubmitted]);\n\n const doCancel = useCallback(() => {\n finalizedRef.current = true;\n onCancelled({id, formData});\n }, [id, formData, onCancelled]);\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n doSubmit();\n },\n [doSubmit],\n );\n\n // Single entry point for any control nested in the form's content to\n // trigger a top-level form action (Button's `formAction`, a field's\n // `submitOnEnter`, etc.) — keeps all form-bindable controls consistent.\n const requestAction = useCallback(\n (action: FormAction) => {\n if (action === \"submit\") {\n doSubmit();\n } else {\n doCancel();\n }\n },\n [doSubmit, doCancel],\n );\n\n const contextValue: FormContextValue = {\n formData,\n errors,\n handleChange,\n setError,\n requestAction,\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 {!hideSubmit && (\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 )}\n </form>\n </FormContext.Provider>\n );\n};\n"],"mappings":";;;;;;AAMA,IAAa,KACX,MACoB;CACpB,IAAM,EACJ,OACA,aAAU,CAAC,GACX,mBAAgB,CAAC,GACjB,iBAAc,UACd,gBAAa,IACb,cACA,aACA,gBACA,gBACA,mBACE,GAEE,CAAC,GAAU,KAAe,SAAyC,EACvE,GAAG,EACL,EAAE,GACI,CAAC,GAAQ,KAAa,EAAiC,CAAC,CAAC,GAMzD,IAAe,EAAO,EAAK,GAG3B,IAAQ,EAAO,CAAE;CACvB,EAAM,UAAU;CAChB,IAAM,IAAc,EAAO,CAAQ;CACnC,EAAY,UAAU;CACtB,IAAM,IAAiB,EAAO,CAAW;CAGzC,AAFA,EAAe,UAAU,GAEzB,cACe;EACX,AAAK,EAAa,WAChB,EAAe,QAAQ;GAAC,IAAI,EAAM;GAAS,UAAU,EAAY;EAAO,CAAC;CAE7E,GAGC,CAAC,CAAC;CAEL,IAAM,IAAe,GAAa,GAAmB,MAAmB;EAGtE,AAFA,GAAa,OAAU;GAAC,GAAG;IAAO,IAAY;EAAK,EAAE,GAErD,GAAW,MAAS;GAClB,IAAI,CAAC,EAAK,IAAY,OAAO;GAC7B,IAAM,IAAO,EAAC,GAAG,EAAI;GAErB,OADA,OAAO,EAAK,IACL;EACT,CAAC;CACH,GAAG,CAAC,CAAC,GAEC,IAAW,GAAa,GAAmB,MAAyB;EACxE,GAAW,MAAS;GAClB,IAAI,MAAU,MAAM;IAClB,IAAM,IAAO,EAAC,GAAG,EAAI;IAErB,OADA,OAAO,EAAK,IACL;GACT;GACA,OAAO;IAAC,GAAG;KAAO,IAAY;GAAK;EACrC,CAAC;CACH,GAAG,CAAC,CAAC,GAEC,IAAW,QAAkB;EAEjC,AADA,EAAa,UAAU,IACvB,EAAY;GAAC;GAAI;EAAQ,CAAC;CAC5B,GAAG;EAAC;EAAI;EAAU;CAAW,CAAC,GAExB,IAAW,QAAkB;EAEjC,AADA,EAAa,UAAU,IACvB,EAAY;GAAC;GAAI;EAAQ,CAAC;CAC5B,GAAG;EAAC;EAAI;EAAU;CAAW,CAAC,GAExB,IAAe,GAClB,MAAuB;EAEtB,AADA,EAAE,eAAe,GACjB,EAAS;CACX,GACA,CAAC,CAAQ,CACX,GAgBM,IAAiC;EACrC;EACA;EACA;EACA;EACA,eAhBoB,GACnB,MAAuB;GACtB,AAAI,MAAW,WACb,EAAS,IAET,EAAS;EAEb,GACA,CAAC,GAAU,CAAQ,CAQnB;EACA,UAAU;CACZ;CAEA,OACE,kBAAC,EAAY,UAAb;EAAsB,OAAO;YAC3B,kBAAC,QAAD;GACE,UAAU;GACV,WAAW,EAAG,uBAAuB,CAAS;GAC9C,gBAAc;aAHhB,CAKG,EAAQ,KAAK,GAAM,MAClB,kBAAC,GAAD;IAAgB,UAAU;IAAM,YAAY;GAAW,GAA5C,CAA4C,CACxD,GACA,CAAC,KACA,kBAAC,UAAD;IACE,MAAK;IACL,WAAU;cAET;GACK,CAAA,CAEN;;CACc,CAAA;AAE1B"}
@@ -1,4 +1,5 @@
1
1
  import { default as React } from 'react';
2
+ import { FormAction } from './form.types';
2
3
  /**
3
4
  * Shape of the data provided by FormContext.
4
5
  * When inside a <Form> card, isInForm is true and all fields are populated.
@@ -9,6 +10,16 @@ export interface FormContextValue {
9
10
  errors: Record<string, string>;
10
11
  handleChange: (fieldName: string, value: unknown) => void;
11
12
  setError: (fieldName: string, error: string | null) => void;
13
+ /**
14
+ * Call from within form content to request that the enclosing Form
15
+ * perform a top-level action:
16
+ * - `"submit"` — same as pressing the form's native submit button; fires
17
+ * `onSubmitted`. Used by `Button` (`formAction: "submit"`) and by
18
+ * form-bindable fields' `submitOnEnter` prop.
19
+ * - `"cancel"` — fires `onCancelled` without submitting or resetting the
20
+ * form's field values. Used by `Button` (`formAction: "cancel"`).
21
+ */
22
+ requestAction: (action: FormAction) => void;
12
23
  isInForm: boolean;
13
24
  }
14
25
  export declare const FormContext: React.Context<FormContextValue | null>;
@@ -5,6 +5,7 @@ var n = {
5
5
  errors: {},
6
6
  handleChange: () => {},
7
7
  setError: () => {},
8
+ requestAction: () => {},
8
9
  isInForm: !1
9
10
  }, r = e.createContext(null), i = () => t(r) ?? n;
10
11
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"form.context.js","names":[],"sources":["../../../src/cards/form/form.context.tsx"],"sourcesContent":["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"],"mappings":";;AAeA,IAAM,IAAqC;CACzC,UAAU,CAAC;CACX,QAAQ,CAAC;CACT,oBAAoB,CAAC;CACrB,gBAAgB,CAAC;CACjB,UAAU;AACZ,GAEa,IAAc,EAAM,cAAuC,IAAI,GAU/D,UACC,EAAW,CAChB,KAAO"}
1
+ {"version":3,"file":"form.context.js","names":[],"sources":["../../../src/cards/form/form.context.tsx"],"sourcesContent":["import React, {useContext} from \"react\";\nimport type {FormAction} from \"./form.types\";\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 /**\n * Call from within form content to request that the enclosing Form\n * perform a top-level action:\n * - `\"submit\"` — same as pressing the form's native submit button; fires\n * `onSubmitted`. Used by `Button` (`formAction: \"submit\"`) and by\n * form-bindable fields' `submitOnEnter` prop.\n * - `\"cancel\"` — fires `onCancelled` without submitting or resetting the\n * form's field values. Used by `Button` (`formAction: \"cancel\"`).\n */\n requestAction: (action: FormAction) => void;\n isInForm: boolean;\n}\n\nconst FALLBACK_CONTEXT: FormContextValue = {\n formData: {},\n errors: {},\n handleChange: () => {},\n setError: () => {},\n requestAction: () => {},\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"],"mappings":";;AA0BA,IAAM,IAAqC;CACzC,UAAU,CAAC;CACX,QAAQ,CAAC;CACT,oBAAoB,CAAC;CACrB,gBAAgB,CAAC;CACjB,qBAAqB,CAAC;CACtB,UAAU;AACZ,GAEa,IAAc,EAAM,cAAuC,IAAI,GAU/D,UACC,EAAW,CAChB,KAAO"}
@@ -3,6 +3,8 @@ export declare const PI_FORM_CARD = "pi/form";
3
3
  export declare const Form: <S extends import('@pihanga2/core').ReduxState>(p: import('@pihanga2/core').PiMapProps<PiFormProps, S, PiFormEvents>) => import('@pihanga2/core').PiCardDef;
4
4
  export declare const PI_FORM_ACTION: {
5
5
  SUBMITTED: string;
6
+ CANCELLED: string;
7
+ ABANDONED: string;
6
8
  };
7
9
  export declare const onFormSubmitted: <S extends import('@pihanga2/core').ReduxState>(register: import('@pihanga2/core/types').PiRegisterMinimal, f: import('@pihanga2/core').ReduceF<S, import('@pihanga2/core').ReduxAction & {
8
10
  cardID: string;
@@ -11,6 +13,60 @@ export declare const onFormSubmitted: <S extends import('@pihanga2/core').ReduxS
11
13
  export declare const onPiFormSubmitted: <S extends import('@pihanga2/core').ReduxState>(register: import('@pihanga2/core/types').PiRegisterMinimal, f: import('@pihanga2/core').ReduceF<S, import('@pihanga2/core').ReduxAction & {
12
14
  cardID: string;
13
15
  } & PiFormSubmittedEvent>) => void;
16
+ /**
17
+ * Fires when a control inside the form's content explicitly requests
18
+ * cancellation — see `FormContext.requestAction("cancel")` (wired up
19
+ * automatically for any `Button` card with `formAction: "cancel"`).
20
+ * Does not submit the form and does not reset its field values.
21
+ */
22
+ export declare const onFormCancelled: <S extends import('@pihanga2/core').ReduxState>(register: import('@pihanga2/core/types').PiRegisterMinimal, f: import('@pihanga2/core').ReduceF<S, import('@pihanga2/core').ReduxAction & {
23
+ cardID: string;
24
+ } & PiFormCancelledEvent>) => void;
25
+ /**
26
+ * Fires automatically when the Form card is removed from the screen
27
+ * (unmounted) *without* having been submitted or explicitly cancelled first
28
+ * — e.g. a parent `pi/dialog` is dismissed via the X button, Escape, or an
29
+ * outside click while the form is still open. Use this to clean up any
30
+ * related state (drafts, optimistic UI, etc.) regardless of how the host
31
+ * container went away.
32
+ *
33
+ * Never fires after `onSubmitted` or `onCancelled` for the same form
34
+ * instance.
35
+ */
36
+ export declare const onFormAbandoned: <S extends import('@pihanga2/core').ReduxState>(register: import('@pihanga2/core/types').PiRegisterMinimal, f: import('@pihanga2/core').ReduceF<S, import('@pihanga2/core').ReduxAction & {
37
+ cardID: string;
38
+ } & PiFormAbandonedEvent>) => void;
39
+ /**
40
+ * Shared props for any card that binds a single value to a `pi/form`'s
41
+ * state (text inputs, toggles, selects, etc.).
42
+ *
43
+ * All form-bindable field cards (`pi/input`, `pi/text-field`,
44
+ * `pi/text-area`, `pi/switch`, `pi/checkbox`, `pi/toggle-group`,
45
+ * `pi/select`, …) extend this so the binding contract stays identical
46
+ * everywhere: set `name` to read/write the field via `FormContext` when
47
+ * rendered inside a `pi/form`; omit it (or render outside a form) to fall
48
+ * back to the card's own standalone `value`/`checked` prop + `onChanged`
49
+ * event.
50
+ */
51
+ export type FormFieldProps = {
52
+ /**
53
+ * Field name used to bind to FormContext when inside a pi/form card.
54
+ * When provided the component reads its value from form state and writes
55
+ * back via form.handleChange.
56
+ */
57
+ name?: string;
58
+ };
59
+ /**
60
+ * Actions a control nested inside a `pi/form` can request via
61
+ * `FormContext.requestAction()` — currently used by `Button`'s
62
+ * `formAction` prop, and by form-bindable fields' `submitOnEnter` prop.
63
+ *
64
+ * - `"submit"` — triggers the form's real submit flow (same as pressing its
65
+ * native submit button), which fires `onSubmitted`.
66
+ * - `"cancel"` — fires `onCancelled` without submitting or resetting the
67
+ * form.
68
+ */
69
+ export type FormAction = "submit" | "cancel";
14
70
  export type PiFormProps = {
15
71
  /**
16
72
  * Optional id passed through to events for identification.
@@ -36,12 +92,31 @@ export type PiFormProps = {
36
92
  * Additional CSS classes for the <form> element.
37
93
  */
38
94
  className?: string;
95
+ /**
96
+ * When true, the default footer "Submit" button is not rendered.
97
+ * Use this when the form's own content already provides a way to submit
98
+ * (e.g. a `Button` with `formAction: "submit"`).
99
+ * @default false
100
+ */
101
+ hideSubmit?: boolean;
39
102
  };
40
103
  export type PiFormSubmittedEvent = {
41
104
  id?: string;
42
105
  /** Snapshot of form state at the time of submission. */
43
106
  formData: Record<string, unknown>;
44
107
  };
108
+ export type PiFormCancelledEvent = {
109
+ id?: string;
110
+ /** Snapshot of form state at the time of cancellation. */
111
+ formData: Record<string, unknown>;
112
+ };
113
+ export type PiFormAbandonedEvent = {
114
+ id?: string;
115
+ /** Snapshot of form state at the time the form was removed. */
116
+ formData: Record<string, unknown>;
117
+ };
45
118
  export type PiFormEvents = {
46
119
  onSubmitted: PiFormSubmittedEvent;
120
+ onCancelled: PiFormCancelledEvent;
121
+ onAbandoned: PiFormAbandonedEvent;
47
122
  };
@@ -1,7 +1,11 @@
1
1
  import { createCardDeclaration as e, createOnAction as t, registerActions as n } from "@pihanga2/core";
2
2
  //#region src/cards/form/form.types.ts
3
- var r = "pi/form", i = e(r), a = n(r, ["submitted"]), o = t(a.SUBMITTED), s = o;
3
+ var r = "pi/form", i = e(r), a = n(r, [
4
+ "submitted",
5
+ "cancelled",
6
+ "abandoned"
7
+ ]), o = t(a.SUBMITTED), s = o, c = t(a.CANCELLED), l = t(a.ABANDONED);
4
8
  //#endregion
5
- export { i as Form, a as PI_FORM_ACTION, r as PI_FORM_CARD, o as onFormSubmitted, s as onPiFormSubmitted };
9
+ export { i as Form, a as PI_FORM_ACTION, r as PI_FORM_CARD, l as onFormAbandoned, c as onFormCancelled, o as onFormSubmitted, s as onPiFormSubmitted };
6
10
 
7
11
  //# sourceMappingURL=form.types.js.map