@pihanga2/shadcn 0.2.19 → 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.
- package/AGENT.using-cards.md +2 -2
- package/cards/button/button.component.js +56 -54
- package/cards/button/button.component.js.map +1 -1
- package/cards/button/button.types.d.ts +24 -0
- package/cards/button/button.types.js.map +1 -1
- package/cards/checkbox/checkbox.types.d.ts +2 -7
- package/cards/checkbox/checkbox.types.js.map +1 -1
- package/cards/field/field.component.js +1 -1
- package/cards/field/field.component.js.map +1 -1
- package/cards/form/form.component.js +57 -32
- package/cards/form/form.component.js.map +1 -1
- package/cards/form/form.context.d.ts +11 -0
- package/cards/form/form.context.js +1 -0
- package/cards/form/form.context.js.map +1 -1
- package/cards/form/form.types.d.ts +75 -0
- package/cards/form/form.types.js +6 -2
- package/cards/form/form.types.js.map +1 -1
- package/cards/form/index.js +5 -5
- package/cards/input/index.js +6 -6
- package/cards/input/input.component.js +34 -28
- package/cards/input/input.component.js.map +1 -1
- package/cards/input/input.types.d.ts +14 -7
- package/cards/input/input.types.js +2 -2
- package/cards/input/input.types.js.map +1 -1
- package/cards/pageWithNavbarMeta/index.js +35 -25
- package/cards/pageWithNavbarMeta/index.js.map +1 -1
- package/cards/pageWithNavbarMeta/pageWithNavbarMeta.types.d.ts +1 -4
- package/cards/select/select.types.d.ts +2 -7
- package/cards/select/select.types.js.map +1 -1
- package/cards/switch/switch.types.d.ts +2 -7
- package/cards/switch/switch.types.js.map +1 -1
- package/cards/textArea/textArea.component.js +1 -1
- package/cards/textArea/textArea.component.js.map +1 -1
- package/cards/textArea/textArea.types.d.ts +15 -11
- package/cards/textArea/textArea.types.js.map +1 -1
- package/cards/textField/textField.component.js +13 -9
- package/cards/textField/textField.component.js.map +1 -1
- package/cards/textField/textField.types.d.ts +12 -7
- package/cards/textField/textField.types.js.map +1 -1
- package/cards/toggleGroup/toggleGroup.types.d.ts +2 -7
- package/cards/toggleGroup/toggleGroup.types.js.map +1 -1
- package/package.json +2 -3
package/AGENT.using-cards.md
CHANGED
|
@@ -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 {
|
|
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",
|
|
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
|
|
7
|
-
import { Fragment as
|
|
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
|
|
10
|
+
function f(t) {
|
|
10
11
|
return t ? e(t) : null;
|
|
11
12
|
}
|
|
12
|
-
var
|
|
13
|
-
let { opts:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
cardName:
|
|
17
|
-
parentCard:
|
|
18
|
-
}) :
|
|
19
|
-
|
|
20
|
-
] }),
|
|
21
|
-
active:
|
|
22
|
-
focused:
|
|
23
|
-
isMenu:
|
|
24
|
-
truncate:
|
|
25
|
-
variant:
|
|
26
|
-
size:
|
|
27
|
-
disabled:
|
|
28
|
-
loading:
|
|
29
|
-
isPending:
|
|
30
|
-
loadingClassName:
|
|
31
|
-
className: `${
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
...
|
|
36
|
-
href:
|
|
37
|
-
target:
|
|
38
|
-
"data-pihanga":
|
|
39
|
-
label:
|
|
40
|
-
onClick:
|
|
41
|
-
children:
|
|
42
|
-
}) : /* @__PURE__ */
|
|
43
|
-
...
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 (!
|
|
50
|
-
let
|
|
51
|
-
cardName:
|
|
52
|
-
parentCard:
|
|
53
|
-
}) : /* @__PURE__ */
|
|
54
|
-
"data-pihanga": `${
|
|
55
|
-
children:
|
|
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__ */
|
|
58
|
-
open:
|
|
59
|
-
children: [/* @__PURE__ */
|
|
59
|
+
return /* @__PURE__ */ d(r, {
|
|
60
|
+
open: G,
|
|
61
|
+
children: [/* @__PURE__ */ u(a, {
|
|
60
62
|
asChild: !0,
|
|
61
|
-
children:
|
|
62
|
-
}), /* @__PURE__ */
|
|
63
|
-
side:
|
|
64
|
-
children:
|
|
63
|
+
children: W()
|
|
64
|
+
}), /* @__PURE__ */ u(i, {
|
|
65
|
+
side: C,
|
|
66
|
+
children: K
|
|
65
67
|
})]
|
|
66
68
|
});
|
|
67
69
|
};
|
|
68
70
|
//#endregion
|
|
69
|
-
export {
|
|
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":"
|
|
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":";;
|
|
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 =
|
|
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"}
|
|
@@ -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
|
|
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,
|
|
4
|
-
import { Card as
|
|
5
|
-
import { jsx as
|
|
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
|
|
8
|
-
let { id:
|
|
9
|
-
|
|
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
|
-
})),
|
|
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
|
-
}, []),
|
|
18
|
-
|
|
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
|
-
}, []),
|
|
29
|
-
|
|
30
|
-
id:
|
|
31
|
-
formData:
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
]),
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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__ */
|
|
45
|
-
value:
|
|
46
|
-
children: /* @__PURE__ */
|
|
47
|
-
onSubmit:
|
|
48
|
-
className: e("flex flex-col gap-4",
|
|
49
|
-
"data-pihanga":
|
|
50
|
-
children: [
|
|
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:
|
|
53
|
-
}, t)), /* @__PURE__ */
|
|
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:
|
|
81
|
+
children: p
|
|
57
82
|
})]
|
|
58
83
|
})
|
|
59
84
|
});
|
|
60
85
|
};
|
|
61
86
|
//#endregion
|
|
62
|
-
export {
|
|
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
|
|
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>;
|
|
@@ -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":";;
|
|
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
|
};
|
package/cards/form/form.types.js
CHANGED
|
@@ -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, [
|
|
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
|