@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.
- 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 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.types.js","names":[],"sources":["../../../src/cards/form/form.types.ts"],"sourcesContent":["import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_FORM_CARD = \"pi/form\";\n\nexport const Form = createCardDeclaration<PiFormProps, PiFormEvents>(\n PI_FORM_CARD,\n);\n\nexport const PI_FORM_ACTION = registerActions(PI_FORM_CARD, [\"submitted\"]);\n\nexport const onFormSubmitted = createOnAction<PiFormSubmittedEvent>(\n PI_FORM_ACTION.SUBMITTED,\n);\n\n/** @deprecated Use `onFormSubmitted` instead. */\nexport const onPiFormSubmitted = onFormSubmitted;\n\nexport type PiFormProps = {\n /**\n * Optional id passed through to events for identification.\n */\n id?: string;\n\n /**\n * Ordered list of field card refs to render inside the form.\n * Each card (e.g. TextField, Checkbox, FormSelect) will be rendered\n * in order and wrapped by a FormContext.Provider so they can read\n * and write shared form state.\n */\n content?: PiCardRef[];\n\n /**\n * Initial values for the form fields, keyed by field name.\n */\n initialValues?: Record<string, unknown>;\n\n /**\n * Label for the submit button.\n * @default \"Submit\"\n */\n submitLabel?: string;\n\n /**\n * Additional CSS classes for the <form> element.\n */\n className?: string;\n};\n\nexport type PiFormSubmittedEvent = {\n id?: string;\n /** Snapshot of form state at the time of submission. */\n formData: Record<string, unknown>;\n};\n\nexport type PiFormEvents = {\n onSubmitted: PiFormSubmittedEvent;\n};\n"],"mappings":";;AAOA,IAAa,IAAe,WAEf,IAAO,EAClB,CACF,GAEa,IAAiB,EAAgB,GAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"form.types.js","names":[],"sources":["../../../src/cards/form/form.types.ts"],"sourcesContent":["import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_FORM_CARD = \"pi/form\";\n\nexport const Form = createCardDeclaration<PiFormProps, PiFormEvents>(\n PI_FORM_CARD,\n);\n\nexport const PI_FORM_ACTION = registerActions(PI_FORM_CARD, [\n \"submitted\",\n \"cancelled\",\n \"abandoned\",\n]);\n\nexport const onFormSubmitted = createOnAction<PiFormSubmittedEvent>(\n PI_FORM_ACTION.SUBMITTED,\n);\n\n/** @deprecated Use `onFormSubmitted` instead. */\nexport const onPiFormSubmitted = onFormSubmitted;\n\n/**\n * Fires when a control inside the form's content explicitly requests\n * cancellation — see `FormContext.requestAction(\"cancel\")` (wired up\n * automatically for any `Button` card with `formAction: \"cancel\"`).\n * Does not submit the form and does not reset its field values.\n */\nexport const onFormCancelled = createOnAction<PiFormCancelledEvent>(\n PI_FORM_ACTION.CANCELLED,\n);\n\n/**\n * Fires automatically when the Form card is removed from the screen\n * (unmounted) *without* having been submitted or explicitly cancelled first\n * — e.g. a parent `pi/dialog` is dismissed via the X button, Escape, or an\n * outside click while the form is still open. Use this to clean up any\n * related state (drafts, optimistic UI, etc.) regardless of how the host\n * container went away.\n *\n * Never fires after `onSubmitted` or `onCancelled` for the same form\n * instance.\n */\nexport const onFormAbandoned = createOnAction<PiFormAbandonedEvent>(\n PI_FORM_ACTION.ABANDONED,\n);\n\n/**\n * Shared props for any card that binds a single value to a `pi/form`'s\n * state (text inputs, toggles, selects, etc.).\n *\n * All form-bindable field cards (`pi/input`, `pi/text-field`,\n * `pi/text-area`, `pi/switch`, `pi/checkbox`, `pi/toggle-group`,\n * `pi/select`, …) extend this so the binding contract stays identical\n * everywhere: set `name` to read/write the field via `FormContext` when\n * rendered inside a `pi/form`; omit it (or render outside a form) to fall\n * back to the card's own standalone `value`/`checked` prop + `onChanged`\n * event.\n */\nexport type FormFieldProps = {\n /**\n * Field name used to bind to FormContext when inside a pi/form card.\n * When provided the component reads its value from form state and writes\n * back via form.handleChange.\n */\n name?: string;\n};\n\n/**\n * Actions a control nested inside a `pi/form` can request via\n * `FormContext.requestAction()` — currently used by `Button`'s\n * `formAction` prop, and by form-bindable fields' `submitOnEnter` prop.\n *\n * - `\"submit\"` — triggers the form's real submit flow (same as pressing its\n * native submit button), which fires `onSubmitted`.\n * - `\"cancel\"` — fires `onCancelled` without submitting or resetting the\n * form.\n */\nexport type FormAction = \"submit\" | \"cancel\";\n\nexport type PiFormProps = {\n /**\n * Optional id passed through to events for identification.\n */\n id?: string;\n\n /**\n * Ordered list of field card refs to render inside the form.\n * Each card (e.g. TextField, Checkbox, FormSelect) will be rendered\n * in order and wrapped by a FormContext.Provider so they can read\n * and write shared form state.\n */\n content?: PiCardRef[];\n\n /**\n * Initial values for the form fields, keyed by field name.\n */\n initialValues?: Record<string, unknown>;\n\n /**\n * Label for the submit button.\n * @default \"Submit\"\n */\n submitLabel?: string;\n\n /**\n * Additional CSS classes for the <form> element.\n */\n className?: string;\n\n /**\n * When true, the default footer \"Submit\" button is not rendered.\n * Use this when the form's own content already provides a way to submit\n * (e.g. a `Button` with `formAction: \"submit\"`).\n * @default false\n */\n hideSubmit?: boolean;\n};\n\nexport type PiFormSubmittedEvent = {\n id?: string;\n /** Snapshot of form state at the time of submission. */\n formData: Record<string, unknown>;\n};\n\nexport type PiFormCancelledEvent = {\n id?: string;\n /** Snapshot of form state at the time of cancellation. */\n formData: Record<string, unknown>;\n};\n\nexport type PiFormAbandonedEvent = {\n id?: string;\n /** Snapshot of form state at the time the form was removed. */\n formData: Record<string, unknown>;\n};\n\nexport type PiFormEvents = {\n onSubmitted: PiFormSubmittedEvent;\n onCancelled: PiFormCancelledEvent;\n onAbandoned: PiFormAbandonedEvent;\n};\n"],"mappings":";;AAOA,IAAa,IAAe,WAEf,IAAO,EAClB,CACF,GAEa,IAAiB,EAAgB,GAAc;CAC1D;CACA;CACA;AACF,CAAC,GAEY,IAAkB,EAC7B,EAAe,SACjB,GAGa,IAAoB,GAQpB,IAAkB,EAC7B,EAAe,SACjB,GAaa,IAAkB,EAC7B,EAAe,SACjB"}
|
package/cards/form/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { FormContext as e, useFormContext as t } from "./form.context.js";
|
|
2
2
|
import { FormComponent as n } from "./form.component.js";
|
|
3
|
-
import { Form as r, PI_FORM_ACTION as i, PI_FORM_CARD as a,
|
|
4
|
-
import { actionTypesToEvents as
|
|
3
|
+
import { Form as r, PI_FORM_ACTION as i, PI_FORM_CARD as a, onFormAbandoned as o, onFormCancelled as s, onFormSubmitted as c, onPiFormSubmitted as l } from "./form.types.js";
|
|
4
|
+
import { actionTypesToEvents as u, registerCardComponent as d } from "@pihanga2/core";
|
|
5
5
|
//#region src/cards/form/index.ts
|
|
6
|
-
|
|
6
|
+
d({
|
|
7
7
|
name: a,
|
|
8
8
|
component: n,
|
|
9
|
-
events:
|
|
9
|
+
events: u(i)
|
|
10
10
|
});
|
|
11
11
|
//#endregion
|
|
12
|
-
export { r as Form, e as FormContext, i as PI_FORM_ACTION, a as PI_FORM_CARD, o as
|
|
12
|
+
export { r as Form, e as FormContext, i as PI_FORM_ACTION, a as PI_FORM_CARD, o as onFormAbandoned, s as onFormCancelled, c as onFormSubmitted, l as onPiFormSubmitted, t as useFormContext };
|
|
13
13
|
|
|
14
14
|
//# sourceMappingURL=index.js.map
|
package/cards/input/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { InputComponent as e } from "./input.component.js";
|
|
2
|
-
import {
|
|
3
|
-
import { actionTypesToEvents as
|
|
2
|
+
import { Input as t, PI_INPUT_ACTION as n, PI_INPUT_CARD as r, PiInput as i, onInputChanged as a, onInputCommitted as o, onPiInputChanged as s, onPiInputCommitted as c } from "./input.types.js";
|
|
3
|
+
import { actionTypesToEvents as l, registerCardComponent as u } from "@pihanga2/core";
|
|
4
4
|
//#region src/cards/input/index.ts
|
|
5
|
-
|
|
6
|
-
name:
|
|
5
|
+
u({
|
|
6
|
+
name: r,
|
|
7
7
|
component: e,
|
|
8
|
-
events:
|
|
8
|
+
events: l(n)
|
|
9
9
|
});
|
|
10
10
|
//#endregion
|
|
11
|
-
export { t as
|
|
11
|
+
export { t as Input, n as PI_INPUT_ACTION, r as PI_INPUT_CARD, i as PiInput, a as onInputChanged, o as onInputCommitted, s as onPiInputChanged, c as onPiInputCommitted };
|
|
12
12
|
|
|
13
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -6,63 +6,69 @@ import "@pihanga2/core";
|
|
|
6
6
|
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
7
7
|
//#region src/cards/input/input.component.tsx
|
|
8
8
|
var o = (o) => {
|
|
9
|
-
let { name: s, value: c = "", type: l = "text", placeholder: u, disabled: d, label: f, description: p, className: m, autoFocus: h,
|
|
9
|
+
let { name: s, value: c = "", type: l = "text", placeholder: u, disabled: d, label: f, description: p, className: m, autoFocus: h, submitOnEnter: g, cardName: _, onChanged: v, onCommitted: y } = o, b = r.useRef(null);
|
|
10
10
|
r.useEffect(() => {
|
|
11
|
-
h &&
|
|
11
|
+
h && b.current?.focus();
|
|
12
12
|
}, []);
|
|
13
|
-
let
|
|
13
|
+
let x = o.id, S = !!o.invalid, C = e(), w = C.isInForm && !!s, [T, E] = r.useState(l === "file" ? "" : c);
|
|
14
14
|
r.useEffect(() => {
|
|
15
|
-
!
|
|
15
|
+
!w && l !== "file" && E(c);
|
|
16
16
|
}, [
|
|
17
17
|
c,
|
|
18
|
-
|
|
18
|
+
w,
|
|
19
19
|
l
|
|
20
20
|
]);
|
|
21
|
-
let
|
|
22
|
-
function
|
|
21
|
+
let D = l === "file" ? void 0 : w ? C.formData[s] ?? "" : T;
|
|
22
|
+
function O(e) {
|
|
23
23
|
let t = l === "file" ? e.target.files?.[0]?.name ?? "" : e.target.value;
|
|
24
|
-
|
|
24
|
+
w ? C.handleChange(s, t) : (E(t), v({
|
|
25
25
|
name: s,
|
|
26
26
|
value: t
|
|
27
27
|
}));
|
|
28
28
|
}
|
|
29
|
-
function
|
|
30
|
-
!
|
|
29
|
+
function k() {
|
|
30
|
+
!w && l !== "file" && y({
|
|
31
31
|
name: s,
|
|
32
|
-
value:
|
|
32
|
+
value: T
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
function
|
|
36
|
-
e.key
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
function A(e) {
|
|
36
|
+
if (!(e.key !== "Enter" || l === "file")) {
|
|
37
|
+
if (w) {
|
|
38
|
+
g && C.requestAction("submit");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
y({
|
|
42
|
+
name: s,
|
|
43
|
+
value: T
|
|
44
|
+
});
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
|
-
let
|
|
47
|
+
let j = `${_}-${s ?? "input"}`, M = x ?? j;
|
|
42
48
|
return /* @__PURE__ */ a("div", {
|
|
43
|
-
"data-pihanga":
|
|
49
|
+
"data-pihanga": _,
|
|
44
50
|
className: "grid w-full gap-1.5",
|
|
45
51
|
children: [
|
|
46
52
|
f && /* @__PURE__ */ i(t, {
|
|
47
|
-
htmlFor:
|
|
53
|
+
htmlFor: M,
|
|
48
54
|
children: f
|
|
49
55
|
}),
|
|
50
56
|
/* @__PURE__ */ i(n, {
|
|
51
|
-
ref:
|
|
52
|
-
id:
|
|
57
|
+
ref: b,
|
|
58
|
+
id: M,
|
|
53
59
|
type: l,
|
|
54
|
-
value:
|
|
55
|
-
onChange:
|
|
56
|
-
onBlur:
|
|
57
|
-
onKeyDown:
|
|
60
|
+
value: D,
|
|
61
|
+
onChange: O,
|
|
62
|
+
onBlur: k,
|
|
63
|
+
onKeyDown: A,
|
|
58
64
|
placeholder: u,
|
|
59
65
|
disabled: d,
|
|
60
66
|
className: m,
|
|
61
|
-
"aria-invalid":
|
|
62
|
-
"aria-describedby": p ? `${
|
|
67
|
+
"aria-invalid": S || void 0,
|
|
68
|
+
"aria-describedby": p ? `${M}-desc` : void 0
|
|
63
69
|
}),
|
|
64
70
|
p && /* @__PURE__ */ i("p", {
|
|
65
|
-
id: `${
|
|
71
|
+
id: `${M}-desc`,
|
|
66
72
|
className: "text-sm text-muted-foreground",
|
|
67
73
|
children: p
|
|
68
74
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.component.js","names":[],"sources":["../../../src/cards/input/input.component.tsx"],"sourcesContent":["import React from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {Input} from \"@/components/ui/input\";\nimport {Label} from \"@/components/ui/label\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport type {PiInputEvents, PiInputProps} from \"./input.types\";\n\nexport const InputComponent = (\n props: PiCardProps<PiInputProps, PiInputEvents>,\n): React.ReactNode => {\n const {\n name,\n value: propValue = \"\",\n type = \"text\",\n placeholder,\n disabled,\n label,\n description,\n className,\n autoFocus,\n cardName,\n onChanged,\n onCommitted,\n } = props;\n\n // Focus the input once, the first time it is mounted/shown (e.g. when a\n // dialog or drawer opens). Intentionally an empty dependency array — this\n // must not re-fire on subsequent re-renders.\n const inputRef = React.useRef<HTMLInputElement>(null);\n React.useEffect(() => {\n if (autoFocus) inputRef.current?.focus();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // `id` and `invalid` may be injected by a parent pi/field card via\n // Pihanga's extra-prop forwarding. They are not in the official type so\n // we read them through an escape hatch.\n const injectedId = (props as {id?: string}).id;\n const invalid = Boolean((props as {invalid?: boolean}).invalid);\n\n // Detect if we are inside a pi/form card via React context.\n const form = useFormContext();\n const useFormData = form.isInForm && Boolean(name);\n\n // Local display state — lets the user type freely without triggering Redux\n // on every keystroke. Only `onCommitted` (blur / Enter) writes back to\n // Redux, so callers that use `onCommitted` instead of `onChanged` avoid\n // per-keystroke panel rebuilds.\n const [localValue, setLocalValue] = React.useState<string>(\n type === \"file\" ? \"\" : propValue,\n );\n\n // Sync local value when the prop changes from outside (e.g. when a different\n // card is selected in the playground, resetting the controls panel).\n React.useEffect(() => {\n if (!useFormData && type !== \"file\") {\n setLocalValue(propValue);\n }\n }, [propValue, useFormData, type]);\n\n // Derive the effective value for the controlled <Input>:\n // - inside a form → read from form.formData[name]\n // - standalone → use localValue (updated on every keystroke locally)\n // - file inputs → uncontrolled (no value binding)\n const inputValue =\n type === \"file\"\n ? undefined\n : useFormData\n ? ((form.formData[name!] as string | undefined) ?? \"\")\n : localValue;\n\n function handleChange(e: React.ChangeEvent<HTMLInputElement>) {\n const newValue =\n type === \"file\" ? (e.target.files?.[0]?.name ?? \"\") : e.target.value;\n if (useFormData) {\n form.handleChange(name!, newValue);\n } else {\n setLocalValue(newValue);\n onChanged({name, value: newValue});\n }\n }\n\n // Fire onCommitted when the user leaves the field or presses Enter.\n function handleBlur() {\n if (!useFormData && type !== \"file\") {\n onCommitted({name, value: localValue});\n }\n }\n\n function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {\n if (e.key
|
|
1
|
+
{"version":3,"file":"input.component.js","names":[],"sources":["../../../src/cards/input/input.component.tsx"],"sourcesContent":["import React from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {Input} from \"@/components/ui/input\";\nimport {Label} from \"@/components/ui/label\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport type {PiInputEvents, PiInputProps} from \"./input.types\";\n\nexport const InputComponent = (\n props: PiCardProps<PiInputProps, PiInputEvents>,\n): React.ReactNode => {\n const {\n name,\n value: propValue = \"\",\n type = \"text\",\n placeholder,\n disabled,\n label,\n description,\n className,\n autoFocus,\n submitOnEnter,\n cardName,\n onChanged,\n onCommitted,\n } = props;\n\n // Focus the input once, the first time it is mounted/shown (e.g. when a\n // dialog or drawer opens). Intentionally an empty dependency array — this\n // must not re-fire on subsequent re-renders.\n const inputRef = React.useRef<HTMLInputElement>(null);\n React.useEffect(() => {\n if (autoFocus) inputRef.current?.focus();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // `id` and `invalid` may be injected by a parent pi/field card via\n // Pihanga's extra-prop forwarding. They are not in the official type so\n // we read them through an escape hatch.\n const injectedId = (props as {id?: string}).id;\n const invalid = Boolean((props as {invalid?: boolean}).invalid);\n\n // Detect if we are inside a pi/form card via React context.\n const form = useFormContext();\n const useFormData = form.isInForm && Boolean(name);\n\n // Local display state — lets the user type freely without triggering Redux\n // on every keystroke. Only `onCommitted` (blur / Enter) writes back to\n // Redux, so callers that use `onCommitted` instead of `onChanged` avoid\n // per-keystroke panel rebuilds.\n const [localValue, setLocalValue] = React.useState<string>(\n type === \"file\" ? \"\" : propValue,\n );\n\n // Sync local value when the prop changes from outside (e.g. when a different\n // card is selected in the playground, resetting the controls panel).\n React.useEffect(() => {\n if (!useFormData && type !== \"file\") {\n setLocalValue(propValue);\n }\n }, [propValue, useFormData, type]);\n\n // Derive the effective value for the controlled <Input>:\n // - inside a form → read from form.formData[name]\n // - standalone → use localValue (updated on every keystroke locally)\n // - file inputs → uncontrolled (no value binding)\n const inputValue =\n type === \"file\"\n ? undefined\n : useFormData\n ? ((form.formData[name!] as string | undefined) ?? \"\")\n : localValue;\n\n function handleChange(e: React.ChangeEvent<HTMLInputElement>) {\n const newValue =\n type === \"file\" ? (e.target.files?.[0]?.name ?? \"\") : e.target.value;\n if (useFormData) {\n form.handleChange(name!, newValue);\n } else {\n setLocalValue(newValue);\n onChanged({name, value: newValue});\n }\n }\n\n // Fire onCommitted when the user leaves the field or presses Enter.\n function handleBlur() {\n if (!useFormData && type !== \"file\") {\n onCommitted({name, value: localValue});\n }\n }\n\n function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {\n if (e.key !== \"Enter\" || type === \"file\") return;\n\n if (useFormData) {\n // Inside a form, form.formData is already current (every keystroke\n // wrote through handleChange), so just request the form-level submit.\n if (submitOnEnter) form.requestAction(\"submit\");\n return;\n }\n\n onCommitted({name, value: localValue});\n }\n\n // Use the id injected by pi/field (for label↔control linking); fall back\n // to a locally-generated id when used standalone.\n const selfId = `${cardName}-${name ?? \"input\"}`;\n const fieldId = injectedId ?? selfId;\n\n return (\n <div data-pihanga={cardName} className=\"grid w-full gap-1.5\">\n {label && <Label htmlFor={fieldId}>{label}</Label>}\n <Input\n ref={inputRef}\n id={fieldId}\n type={type}\n value={inputValue}\n onChange={handleChange}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled}\n className={className}\n aria-invalid={invalid || undefined}\n aria-describedby={description ? `${fieldId}-desc` : undefined}\n />\n {description && (\n <p id={`${fieldId}-desc`} className=\"text-sm text-muted-foreground\">\n {description}\n </p>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;AAOA,IAAa,KACX,MACoB;CACpB,IAAM,EACJ,SACA,OAAO,IAAY,IACnB,UAAO,QACP,gBACA,aACA,UACA,gBACA,cACA,cACA,kBACA,aACA,cACA,mBACE,GAKE,IAAW,EAAM,OAAyB,IAAI;CACpD,EAAM,gBAAgB;EACpB,AAAI,KAAW,EAAS,SAAS,MAAM;CAEzC,GAAG,CAAC,CAAC;CAKL,IAAM,IAAc,EAAwB,IACtC,IAAU,EAAS,EAA8B,SAGjD,IAAO,EAAe,GACtB,IAAc,EAAK,YAAY,EAAQ,GAMvC,CAAC,GAAY,KAAiB,EAAM,SACxC,MAAS,SAAS,KAAK,CACzB;CAIA,EAAM,gBAAgB;EACpB,AAAI,CAAC,KAAe,MAAS,UAC3B,EAAc,CAAS;CAE3B,GAAG;EAAC;EAAW;EAAa;CAAI,CAAC;CAMjC,IAAM,IACJ,MAAS,SACL,KAAA,IACA,IACI,EAAK,SAAS,MAAiC,KACjD;CAER,SAAS,EAAa,GAAwC;EAC5D,IAAM,IACJ,MAAS,SAAU,EAAE,OAAO,QAAQ,IAAI,QAAQ,KAAM,EAAE,OAAO;EACjE,AAAI,IACF,EAAK,aAAa,GAAO,CAAQ,KAEjC,EAAc,CAAQ,GACtB,EAAU;GAAC;GAAM,OAAO;EAAQ,CAAC;CAErC;CAGA,SAAS,IAAa;EACpB,AAAI,CAAC,KAAe,MAAS,UAC3B,EAAY;GAAC;GAAM,OAAO;EAAU,CAAC;CAEzC;CAEA,SAAS,EAAc,GAA0C;EAC3D,QAAE,QAAQ,WAAW,MAAS,SAElC;OAAI,GAAa;IAGf,AAAI,KAAe,EAAK,cAAc,QAAQ;IAC9C;GACF;GAEA,EAAY;IAAC;IAAM,OAAO;GAAU,CAAC;EAFrC;CAGF;CAIA,IAAM,IAAS,GAAG,EAAS,GAAG,KAAQ,WAChC,IAAU,KAAc;CAE9B,OACE,kBAAC,OAAD;EAAK,gBAAc;EAAU,WAAU;YAAvC;GACG,KAAS,kBAAC,GAAD;IAAO,SAAS;cAAU;GAAa,CAAA;GACjD,kBAAC,GAAD;IACE,KAAK;IACL,IAAI;IACE;IACN,OAAO;IACP,UAAU;IACV,QAAQ;IACR,WAAW;IACE;IACH;IACC;IACX,gBAAc,KAAW,KAAA;IACzB,oBAAkB,IAAc,GAAG,EAAQ,SAAS,KAAA;GACrD,CAAA;GACA,KACC,kBAAC,KAAD;IAAG,IAAI,GAAG,EAAQ;IAAQ,WAAU;cACjC;GACA,CAAA;EAEF;;AAET"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { FormFieldProps } from '../form/form.types';
|
|
1
2
|
export declare const PI_INPUT_CARD = "pi/input";
|
|
3
|
+
export declare const Input: <S extends import('@pihanga2/core').ReduxState>(p: import('@pihanga2/core').PiMapProps<PiInputProps, S, PiInputEvents>) => import('@pihanga2/core').PiCardDef;
|
|
4
|
+
/** @deprecated Use `Input` instead. */
|
|
2
5
|
export declare const PiInput: <S extends import('@pihanga2/core').ReduxState>(p: import('@pihanga2/core').PiMapProps<PiInputProps, S, PiInputEvents>) => import('@pihanga2/core').PiCardDef;
|
|
3
6
|
export declare const PI_INPUT_ACTION: {
|
|
4
7
|
CHANGED: string;
|
|
@@ -24,13 +27,7 @@ export declare const onInputCommitted: <S extends import('@pihanga2/core').Redux
|
|
|
24
27
|
export declare const onPiInputCommitted: <S extends import('@pihanga2/core').ReduxState>(register: import('@pihanga2/core/types').PiRegisterMinimal, f: import('@pihanga2/core').ReduceF<S, import('@pihanga2/core').ReduxAction & {
|
|
25
28
|
cardID: string;
|
|
26
29
|
} & PiInputCommittedEvent>) => void;
|
|
27
|
-
export type PiInputProps = {
|
|
28
|
-
/**
|
|
29
|
-
* Field name used to bind to FormContext when inside a pi/form card.
|
|
30
|
-
* When provided the component reads its value from form state and writes
|
|
31
|
-
* back via form.handleChange.
|
|
32
|
-
*/
|
|
33
|
-
name?: string;
|
|
30
|
+
export type PiInputProps = FormFieldProps & {
|
|
34
31
|
/**
|
|
35
32
|
* Controlled value used in standalone mode (outside a Form).
|
|
36
33
|
* Ignored when `name` is set and the component is inside a pi/form.
|
|
@@ -65,6 +62,16 @@ export type PiInputProps = {
|
|
|
65
62
|
* on mount — it does not re-focus on subsequent re-renders.
|
|
66
63
|
*/
|
|
67
64
|
autoFocus?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* When true and this input is rendered inside a `pi/form` card, pressing
|
|
67
|
+
* Enter requests the form's submit action (`FormContext.requestAction("submit")`)
|
|
68
|
+
* — equivalent to clicking a `Button` with `formAction: "submit"` — instead
|
|
69
|
+
* of (or in addition to) firing `onCommitted`.
|
|
70
|
+
*
|
|
71
|
+
* Has no effect outside a `pi/form`.
|
|
72
|
+
* @default false
|
|
73
|
+
*/
|
|
74
|
+
submitOnEnter?: boolean;
|
|
68
75
|
};
|
|
69
76
|
export type PiInputChangedEvent = {
|
|
70
77
|
/** Field name, mirrors the `name` prop if provided. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createCardDeclaration as e, createOnAction as t, registerActions as n } from "@pihanga2/core";
|
|
2
2
|
//#region src/cards/input/input.types.ts
|
|
3
|
-
var r = "pi/input", i = e(r), a = n(r, ["changed", "committed"]),
|
|
3
|
+
var r = "pi/input", i = e(r), a = i, o = n(r, ["changed", "committed"]), s = t(o.CHANGED), c = s, l = t(o.COMMITTED), u = l;
|
|
4
4
|
//#endregion
|
|
5
|
-
export {
|
|
5
|
+
export { i as Input, o as PI_INPUT_ACTION, r as PI_INPUT_CARD, a as PiInput, s as onInputChanged, l as onInputCommitted, c as onPiInputChanged, u as onPiInputCommitted };
|
|
6
6
|
|
|
7
7
|
//# sourceMappingURL=input.types.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.types.js","names":[],"sources":["../../../src/cards/input/input.types.ts"],"sourcesContent":["import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_INPUT_CARD = \"pi/input\";\n\nexport const
|
|
1
|
+
{"version":3,"file":"input.types.js","names":[],"sources":["../../../src/cards/input/input.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_INPUT_CARD = \"pi/input\";\n\nexport const Input = createCardDeclaration<PiInputProps, PiInputEvents>(\n PI_INPUT_CARD,\n);\n\n/** @deprecated Use `Input` instead. */\nexport const PiInput = Input;\n\nexport const PI_INPUT_ACTION = registerActions(PI_INPUT_CARD, [\n \"changed\",\n \"committed\",\n]);\n\nexport const onInputChanged = createOnAction<PiInputChangedEvent>(\n PI_INPUT_ACTION.CHANGED,\n);\n\n/** @deprecated Use `onInputChanged` instead. */\nexport const onPiInputChanged = onInputChanged;\n\n/**\n * Subscribe to \"committed\" events — fired when the user finishes editing\n * (blur or Enter key). Unlike `onInputChanged` (which fires on every keystroke),\n * `onInputCommitted` only fires once per editing session, making it the preferred\n * handler for controlled inputs that trigger expensive downstream work.\n */\nexport const onInputCommitted = createOnAction<PiInputCommittedEvent>(\n PI_INPUT_ACTION.COMMITTED,\n);\n\n/** @deprecated Use `onInputCommitted` instead. */\nexport const onPiInputCommitted = onInputCommitted;\n\n// ---------------------------------------------------------------------------\n// Props & Events\n// ---------------------------------------------------------------------------\n\nexport type PiInputProps = FormFieldProps & {\n /**\n * Controlled value used in standalone mode (outside a Form).\n * Ignored when `name` is set and the component is inside a pi/form.\n */\n value?: string;\n\n /**\n * HTML input type.\n * Common values: \"text\" | \"email\" | \"password\" | \"number\" | \"search\" |\n * \"url\" | \"tel\" | \"date\" | \"file\".\n * Defaults to \"text\".\n */\n type?: string;\n\n /** Placeholder text shown when the input is empty. */\n placeholder?: string;\n\n /** When true, the input is disabled and non-interactive. */\n disabled?: boolean;\n\n /**\n * Optional label text rendered in a `<label>` element above the input.\n * The label is automatically associated with the input via `htmlFor`.\n */\n label?: string;\n\n /**\n * Optional helper / description text rendered below the input.\n * Use this for hints, formatting guidance, or validation feedback.\n */\n description?: string;\n\n /** Extra Tailwind / CSS classes forwarded to the underlying <input> element. */\n className?: string;\n\n /**\n * When true, the input grabs keyboard focus the first time it is\n * mounted/shown (e.g. when a dialog or drawer opens). Only applies once,\n * on mount — it does not re-focus on subsequent re-renders.\n */\n autoFocus?: boolean;\n\n /**\n * When true and this input is rendered inside a `pi/form` card, pressing\n * Enter requests the form's submit action (`FormContext.requestAction(\"submit\")`)\n * — equivalent to clicking a `Button` with `formAction: \"submit\"` — instead\n * of (or in addition to) firing `onCommitted`.\n *\n * Has no effect outside a `pi/form`.\n * @default false\n */\n submitOnEnter?: boolean;\n};\n\nexport type PiInputChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** New value after the change. */\n value: string;\n};\n\n/**\n * Payload for the `onCommitted` event (blur / Enter key).\n * Same shape as `PiInputChangedEvent`.\n */\nexport type PiInputCommittedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** Final committed value. */\n value: string;\n};\n\nexport type PiInputEvents = {\n /** Fires on every keystroke. */\n onChanged: PiInputChangedEvent;\n /**\n * Fires once when the user finishes editing (blur or Enter key).\n * Use this instead of `onChanged` when each change triggers expensive\n * downstream work (e.g. rebuilding a panel).\n */\n onCommitted: PiInputCommittedEvent;\n};\n"],"mappings":";;AAOA,IAAa,IAAgB,YAEhB,IAAQ,EACnB,CACF,GAGa,IAAU,GAEV,IAAkB,EAAgB,GAAe,CAC5D,WACA,WACF,CAAC,GAEY,IAAiB,EAC5B,EAAgB,OAClB,GAGa,IAAmB,GAQnB,IAAmB,EAC9B,EAAgB,SAClB,GAGa,IAAqB"}
|
|
@@ -16,26 +16,36 @@ import { registerMetaCard as u } from "@pihanga2/core";
|
|
|
16
16
|
function d(e) {
|
|
17
17
|
return e ? e.charAt(0).toLocaleUpperCase() + e.slice(1) : "";
|
|
18
18
|
}
|
|
19
|
-
function f(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
function f(e) {
|
|
20
|
+
return typeof e == "function" ? e({}, {
|
|
21
|
+
cardName: "",
|
|
22
|
+
ctxtProps: {},
|
|
23
|
+
resolve: (e) => typeof e == "function" ? e({}, {
|
|
24
|
+
cardName: "",
|
|
25
|
+
ctxtProps: {}
|
|
26
|
+
}) : e
|
|
27
|
+
}) : e;
|
|
28
|
+
}
|
|
29
|
+
function p(s, c, l) {
|
|
30
|
+
let u = f(c.navLinks) ?? [], p = f(c.iconName), m = f(c.className), h = f(c.footer), g = [];
|
|
31
|
+
p && g.push(t({
|
|
32
|
+
iconLabel: p,
|
|
23
33
|
opts: {
|
|
24
34
|
variant: "none",
|
|
25
35
|
size: "icon"
|
|
26
36
|
},
|
|
27
37
|
className: "h-6 w-6 shrink-0 pointer-events-none"
|
|
28
|
-
})),
|
|
38
|
+
})), g.push(a({
|
|
29
39
|
text: c.title,
|
|
30
40
|
level: "large",
|
|
31
41
|
className: "leading-none"
|
|
32
42
|
}));
|
|
33
|
-
let
|
|
43
|
+
let _ = l("title", i({
|
|
34
44
|
direction: "row",
|
|
35
45
|
alignItems: "center",
|
|
36
46
|
spacing: 2,
|
|
37
|
-
content:
|
|
38
|
-
})),
|
|
47
|
+
content: g
|
|
48
|
+
})), v = u.map((e, n) => l(`nav-link-${n}`, t({
|
|
39
49
|
id: e.id,
|
|
40
50
|
label: e.title ?? d(e.id),
|
|
41
51
|
opts: { variant: "ghost" },
|
|
@@ -44,15 +54,15 @@ function f(s, c, l) {
|
|
|
44
54
|
type: o.NAVIGATE_TO,
|
|
45
55
|
id: e.id
|
|
46
56
|
})
|
|
47
|
-
}))),
|
|
57
|
+
}))), y = l("nav-md", n({
|
|
48
58
|
showOn: "md",
|
|
49
59
|
content: l("nav-md-inner", i({
|
|
50
60
|
direction: "row",
|
|
51
61
|
alignItems: "center",
|
|
52
62
|
spacing: 5,
|
|
53
|
-
content: [
|
|
63
|
+
content: [_, ...v]
|
|
54
64
|
}))
|
|
55
|
-
})),
|
|
65
|
+
})), b = u.length > 0 ? l("nav-sm", n({
|
|
56
66
|
showOn: "<md",
|
|
57
67
|
content: l("nav-sm-drawer", r({
|
|
58
68
|
direction: "left",
|
|
@@ -68,25 +78,25 @@ function f(s, c, l) {
|
|
|
68
78
|
direction: "column",
|
|
69
79
|
spacing: 4,
|
|
70
80
|
className: "p-4",
|
|
71
|
-
content: [
|
|
81
|
+
content: [_, ...v]
|
|
72
82
|
})),
|
|
73
83
|
footerCloseButtonText: null
|
|
74
84
|
}))
|
|
75
85
|
})) : l("nav-sm-title", n({
|
|
76
86
|
showOn: "<md",
|
|
77
|
-
content:
|
|
78
|
-
})),
|
|
79
|
-
|
|
80
|
-
let
|
|
81
|
-
|
|
87
|
+
content: _
|
|
88
|
+
})), x = f(c.headerLeftCard), S = f(c.headerRightCard), C = [];
|
|
89
|
+
x && C.push(x), S && C.push(S);
|
|
90
|
+
let w = [y, b];
|
|
91
|
+
C.length > 0 && w.push(l("header-actions", i({
|
|
82
92
|
direction: "row",
|
|
83
93
|
alignItems: "center",
|
|
84
94
|
className: "ml-auto gap-2 lg:gap-4",
|
|
85
|
-
content:
|
|
95
|
+
content: C
|
|
86
96
|
})));
|
|
87
|
-
let
|
|
97
|
+
let T = [l("header", e({
|
|
88
98
|
className: "shrink-0 flex items-center gap-4 border-b bg-background px-4 md:px-6",
|
|
89
|
-
content:
|
|
99
|
+
content: w
|
|
90
100
|
})), l("main", e({
|
|
91
101
|
className: "flex-1 min-h-0 overflow-y-auto bg-background p-4 py-2 md:p-8 md:py-4",
|
|
92
102
|
content: ((e, t) => {
|
|
@@ -94,22 +104,22 @@ function f(s, c, l) {
|
|
|
94
104
|
return n == null ? [] : [n];
|
|
95
105
|
})
|
|
96
106
|
}))];
|
|
97
|
-
return
|
|
107
|
+
return h && T.push(l("footer", e({
|
|
98
108
|
className: "shrink-0 bg-background text-muted-foreground px-4 py-2 md:px-8 md:py-4",
|
|
99
|
-
content: [
|
|
109
|
+
content: [h]
|
|
100
110
|
}))), i({
|
|
101
111
|
direction: "row",
|
|
102
|
-
className: `w-full h-full justify-center${
|
|
112
|
+
className: `w-full h-full justify-center${m ? ` ${m}` : ""}`,
|
|
103
113
|
content: [l("page", i({
|
|
104
114
|
direction: "column",
|
|
105
115
|
className: "flex-1 h-full w-full",
|
|
106
|
-
content:
|
|
116
|
+
content: T
|
|
107
117
|
}))]
|
|
108
118
|
});
|
|
109
119
|
}
|
|
110
120
|
u({
|
|
111
121
|
type: s,
|
|
112
|
-
mapper:
|
|
122
|
+
mapper: p,
|
|
113
123
|
events: o
|
|
114
124
|
});
|
|
115
125
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/cards/pageWithNavbarMeta/index.ts"],"sourcesContent":["import {\n registerMetaCard,\n type PiCardDef,\n type PiCardRef,\n type PiMetaProps,\n type PiMetaResolveCtx,\n type PiRegisterMetaCard,\n type RegisterCardF,\n} from \"@pihanga2/core\";\n\nimport {Stack} from \"@/cards/stack\";\nimport {Box} from \"@/cards/box\";\nimport {Button} from \"@/cards/button\";\nimport {Typography} from \"@/cards/typography\";\nimport {Conditional} from \"@/cards/conditional\";\nimport {Drawer} from \"@/cards/drawer\";\n\nimport {\n PAGE_WITH_NAVBAR_META_ACTION,\n PI_PAGE_WITH_NAVBAR_META_CARD,\n type NavLink,\n type PageWithNavbarMetaDynProps,\n type PageWithNavbarMetaEvents,\n type PageWithNavbarMetaStaticProps,\n} from \"./pageWithNavbarMeta.types\";\n\nexport * from \"./pageWithNavbarMeta.types\";\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction capitalize(s: string): string {\n return s ? s.charAt(0).toLocaleUpperCase() + s.slice(1) : \"\";\n}\n\n// ── Mapper ───────────────────────────────────────────────────────────────────\n\n/**\n * `StaticProps` arrive as their plain types — TypeScript refuses memo selectors.\n * `DynProps` arrive as `StateMapper<T>` functions — must be resolved via `resolve()`.\n */\ntype MapperProps = PiMetaProps<\n PageWithNavbarMetaDynProps,\n PageWithNavbarMetaStaticProps,\n PageWithNavbarMetaEvents\n>;\n\nfunction PageWithNavbarMetaMapper(\n _cardName: string,\n props: MapperProps,\n reg: RegisterCardF,\n): PiCardDef {\n // ── Static props: plain types, no resolve() needed ───────────────────────\n const navLinks = props.navLinks ?? [];\n const iconName = props.iconName;\n const wrapperClass = props.className;\n const footer = props.footer;\n\n // ── Title (icon + text) ─────────────────────────────────────────────────\n // props.title is StateMapper<string> — a function, NOT a string.\n // Typography's `text` prop is typed (via PiMapProps) as `string | StateMapper<string>`,\n // so we forward the selector function directly. The framework resolves it lazily\n // (calls props.title(state, ctx)) when Typography renders. This is different from\n // using props.title as a string, which TypeScript would reject.\n const titleContent: PiCardRef[] = [];\n if (iconName) {\n titleContent.push(\n Button({\n iconLabel: iconName,\n opts: {variant: \"none\", size: \"icon\"},\n className: \"h-6 w-6 shrink-0 pointer-events-none\",\n }),\n );\n }\n titleContent.push(\n Typography({\n text: props.title,\n level: \"large\",\n className: \"leading-none\",\n }),\n );\n\n const titleStack = reg(\n \"title\",\n Stack({\n direction: \"row\",\n alignItems: \"center\",\n spacing: 2,\n content: titleContent,\n }),\n );\n\n // ── Nav link buttons (shared between desktop and mobile) ────────────────\n const linkButtons = navLinks.map((link: NavLink, i: number) =>\n reg(\n `nav-link-${i}`,\n Button({\n id: link.id,\n label: link.title ?? capitalize(link.id),\n opts: {variant: \"ghost\"},\n className:\n \"px-0 text-muted-foreground hover:text-foreground h-auto p-0\",\n onClickedMapper: () => ({\n type: PAGE_WITH_NAVBAR_META_ACTION.NAVIGATE_TO,\n id: link.id,\n }),\n }),\n ),\n );\n\n // ── Desktop nav (md+): title + links in a row ───────────────────────────\n const desktopNav = reg(\n \"nav-md\",\n Conditional({\n showOn: \"md\",\n content: reg(\n \"nav-md-inner\",\n Stack({\n direction: \"row\",\n alignItems: \"center\",\n spacing: 5,\n content: [titleStack, ...linkButtons],\n }),\n ),\n }),\n );\n\n // ── Mobile nav (< md) ───────────────────────────────────────────────────\n // With nav links: hamburger drawer (left slide-in using pi/drawer).\n // Without nav links: just show the title inline.\n const mobileNav: PiCardRef =\n navLinks.length > 0\n ? reg(\n \"nav-sm\",\n Conditional({\n showOn: \"<md\",\n content: reg(\n \"nav-sm-drawer\",\n Drawer({\n direction: \"left\",\n trigger: Button({\n iconLabel: \"menu\",\n opts: {variant: \"outline\", size: \"icon\"},\n ariaLabel: \"Toggle navigation menu\",\n }),\n content: reg(\n \"nav-sm-content\",\n Stack({\n direction: \"column\",\n spacing: 4,\n className: \"p-4\",\n content: [titleStack, ...linkButtons],\n }),\n ),\n footerCloseButtonText: null,\n }),\n ),\n }),\n )\n : reg(\n \"nav-sm-title\",\n Conditional({\n showOn: \"<md\",\n content: titleStack,\n }),\n );\n\n // ── Header action slots ─────────────────────────────────────────────────\n // Static PiCardRefs — TypeScript already refuses memo() selectors here.\n const actionRefs: PiCardRef[] = [];\n if (props.headerLeftCard) {\n actionRefs.push(props.headerLeftCard);\n }\n if (props.headerRightCard) {\n actionRefs.push(props.headerRightCard);\n }\n\n const headerContent: PiCardRef[] = [desktopNav, mobileNav];\n if (actionRefs.length > 0) {\n headerContent.push(\n reg(\n \"header-actions\",\n Stack({\n direction: \"row\",\n alignItems: \"center\",\n className: \"ml-auto gap-2 lg:gap-4\",\n content: actionRefs,\n }),\n ),\n );\n }\n\n // ── Header ──────────────────────────────────────────────────────────────\n const header = reg(\n \"header\",\n Box({\n className:\n \"shrink-0 flex items-center gap-4 border-b bg-background px-4 md:px-6\",\n content: headerContent,\n }),\n );\n\n // ── Main ────────────────────────────────────────────────────────────────\n // props.main is a StateMapper — resolved lazily so React re-renders when\n // the referenced card changes.\n const mainBox = reg(\n \"main\",\n Box({\n className:\n \"flex-1 min-h-0 overflow-y-auto bg-background p-4 py-2 md:p-8 md:py-4\",\n content: ((_: unknown, ctx: PiMetaResolveCtx) => {\n const ref = ctx.resolve(props.main);\n return ref != null ? [ref as PiCardRef] : [];\n }) as unknown as PiCardRef[],\n }),\n );\n\n // ── Page column ─────────────────────────────────────────────────────────\n const pageContent: PiCardRef[] = [header, mainBox];\n\n // ── Footer (optional) ───────────────────────────────────────────────────\n // `footer` is a static plain PiCardRef — use directly.\n if (footer) {\n pageContent.push(\n reg(\n \"footer\",\n Box({\n className:\n \"shrink-0 bg-background text-muted-foreground px-4 py-2 md:px-8 md:py-4\",\n content: [footer],\n }),\n ),\n );\n }\n\n // ── Assemble ────────────────────────────────────────────────────────────\n return Stack({\n direction: \"row\",\n className: `w-full h-full justify-center${wrapperClass ? ` ${wrapperClass}` : \"\"}`,\n content: [\n reg(\n \"page\",\n Stack({\n direction: \"column\",\n className: \"flex-1 h-full w-full\",\n content: pageContent,\n }),\n ),\n ],\n });\n}\n\n// ── Registration ─────────────────────────────────────────────────────────────\n\nregisterMetaCard({\n type: PI_PAGE_WITH_NAVBAR_META_CARD,\n mapper: PageWithNavbarMetaMapper,\n events: PAGE_WITH_NAVBAR_META_ACTION,\n} satisfies PiRegisterMetaCard);\n"],"mappings":";;;;;;;;;;;;;;;AA8BA,SAAS,EAAW,GAAmB;CACrC,OAAO,IAAI,EAAE,OAAO,CAAC,EAAE,kBAAkB,IAAI,EAAE,MAAM,CAAC,IAAI;AAC5D;AAcA,SAAS,EACP,GACA,GACA,GACW;CAEX,IAAM,IAAW,EAAM,YAAY,CAAC,GAC9B,IAAW,EAAM,UACjB,IAAe,EAAM,WACrB,IAAS,EAAM,QAQf,IAA4B,CAAC;CAUnC,AATI,KACF,EAAa,KACX,EAAO;EACL,WAAW;EACX,MAAM;GAAC,SAAS;GAAQ,MAAM;EAAM;EACpC,WAAW;CACb,CAAC,CACH,GAEF,EAAa,KACX,EAAW;EACT,MAAM,EAAM;EACZ,OAAO;EACP,WAAW;CACb,CAAC,CACH;CAEA,IAAM,IAAa,EACjB,SACA,EAAM;EACJ,WAAW;EACX,YAAY;EACZ,SAAS;EACT,SAAS;CACX,CAAC,CACH,GAGM,IAAc,EAAS,KAAK,GAAe,MAC/C,EACE,YAAY,KACZ,EAAO;EACL,IAAI,EAAK;EACT,OAAO,EAAK,SAAS,EAAW,EAAK,EAAE;EACvC,MAAM,EAAC,SAAS,QAAO;EACvB,WACE;EACF,wBAAwB;GACtB,MAAM,EAA6B;GACnC,IAAI,EAAK;EACX;CACF,CAAC,CACH,CACF,GAGM,IAAa,EACjB,UACA,EAAY;EACV,QAAQ;EACR,SAAS,EACP,gBACA,EAAM;GACJ,WAAW;GACX,YAAY;GACZ,SAAS;GACT,SAAS,CAAC,GAAY,GAAG,CAAW;EACtC,CAAC,CACH;CACF,CAAC,CACH,GAKM,IACJ,EAAS,SAAS,IACd,EACE,UACA,EAAY;EACV,QAAQ;EACR,SAAS,EACP,iBACA,EAAO;GACL,WAAW;GACX,SAAS,EAAO;IACd,WAAW;IACX,MAAM;KAAC,SAAS;KAAW,MAAM;IAAM;IACvC,WAAW;GACb,CAAC;GACD,SAAS,EACP,kBACA,EAAM;IACJ,WAAW;IACX,SAAS;IACT,WAAW;IACX,SAAS,CAAC,GAAY,GAAG,CAAW;GACtC,CAAC,CACH;GACA,uBAAuB;EACzB,CAAC,CACH;CACF,CAAC,CACH,IACA,EACE,gBACA,EAAY;EACV,QAAQ;EACR,SAAS;CACX,CAAC,CACH,GAIA,IAA0B,CAAC;CAIjC,AAHI,EAAM,kBACR,EAAW,KAAK,EAAM,cAAc,GAElC,EAAM,mBACR,EAAW,KAAK,EAAM,eAAe;CAGvC,IAAM,IAA6B,CAAC,GAAY,CAAS;CACzD,AAAI,EAAW,SAAS,KACtB,EAAc,KACZ,EACE,kBACA,EAAM;EACJ,WAAW;EACX,YAAY;EACZ,WAAW;EACX,SAAS;CACX,CAAC,CACH,CACF;CA6BF,IAAM,IAA2B,CAzBlB,EACb,UACA,EAAI;EACF,WACE;EACF,SAAS;CACX,CAAC,CAmB+B,GAblB,EACd,QACA,EAAI;EACF,WACE;EACF,WAAW,GAAY,MAA0B;GAC/C,IAAM,IAAM,EAAI,QAAQ,EAAM,IAAI;GAClC,OAAO,KAAO,OAA4B,CAAC,IAAtB,CAAC,CAAgB;EACxC;CACF,CAAC,CAIuC,CAAO;CAkBjD,OAdI,KACF,EAAY,KACV,EACE,UACA,EAAI;EACF,WACE;EACF,SAAS,CAAC,CAAM;CAClB,CAAC,CACH,CACF,GAIK,EAAM;EACX,WAAW;EACX,WAAW,+BAA+B,IAAe,IAAI,MAAiB;EAC9E,SAAS,CACP,EACE,QACA,EAAM;GACJ,WAAW;GACX,WAAW;GACX,SAAS;EACX,CAAC,CACH,CACF;CACF,CAAC;AACH;AAIA,EAAiB;CACf,MAAM;CACN,QAAQ;CACR,QAAQ;AACV,CAA8B"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/cards/pageWithNavbarMeta/index.ts"],"sourcesContent":["import {\n registerMetaCard,\n type PiCardDef,\n type PiCardRef,\n type PiMetaProps,\n type PiMetaResolveCtx,\n type PiRegisterMetaCard,\n type RegisterCardF,\n type PiDefCtxtProps,\n type ReduxState,\n type StateMapper,\n type StateMapperContext,\n} from \"@pihanga2/core\";\n\nimport {Stack} from \"@/cards/stack\";\nimport {Box} from \"@/cards/box\";\nimport {Button} from \"@/cards/button\";\nimport {Typography} from \"@/cards/typography\";\nimport {Conditional} from \"@/cards/conditional\";\nimport {Drawer} from \"@/cards/drawer\";\n\nimport {\n PAGE_WITH_NAVBAR_META_ACTION,\n PI_PAGE_WITH_NAVBAR_META_CARD,\n type NavLink,\n type PageWithNavbarMetaDynProps,\n type PageWithNavbarMetaEvents,\n type PageWithNavbarMetaStaticProps,\n} from \"./pageWithNavbarMeta.types\";\n\nexport * from \"./pageWithNavbarMeta.types\";\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction capitalize(s: string): string {\n return s ? s.charAt(0).toLocaleUpperCase() + s.slice(1) : \"\";\n}\n\n/**\n * Resolve a `PiMetaProps`-delivered prop (always a `StateMapper<T>`, per\n * `makeLiveMetaProps` — see `@pihanga2/core`) synchronously, once, at mapper\n * run-time. Only appropriate for props this mapper reads directly in its own\n * body (as opposed to forwarding to a sub-card's own `resolve()`-based state\n * mapper) — i.e. props that control the *shape* of the expansion (how many\n * nav-link buttons, whether a footer/header-action slot exists) rather than\n * a value a sub-card should react to on every render. See the \"reactivity\n * has one hard limit\" note in AGENT.md's metacards guide.\n */\nfunction resolveOnce<T>(\n prop: StateMapper<T, ReduxState, PiDefCtxtProps> | undefined,\n): T | undefined {\n if (typeof prop !== \"function\") return prop;\n return prop({} as ReduxState, {\n cardName: \"\",\n ctxtProps: {},\n resolve: (p: unknown) =>\n typeof p === \"function\"\n ? (p as StateMapper<unknown, ReduxState, PiDefCtxtProps>)(\n {} as ReduxState,\n {cardName: \"\", ctxtProps: {}} as StateMapperContext<PiDefCtxtProps>,\n )\n : p,\n } as StateMapperContext<PiDefCtxtProps>);\n}\n\n// ── Mapper ───────────────────────────────────────────────────────────────────\n\n/**\n * `StaticProps` arrive as their plain types — TypeScript refuses memo selectors.\n * `DynProps` arrive as `StateMapper<T>` functions — must be resolved via `resolve()`.\n */\ntype MapperProps = PiMetaProps<\n PageWithNavbarMetaDynProps,\n PageWithNavbarMetaStaticProps,\n PageWithNavbarMetaEvents\n>;\n\nfunction PageWithNavbarMetaMapper(\n _cardName: string,\n props: MapperProps,\n reg: RegisterCardF,\n): PiCardDef {\n // ── Structural props: read once, synchronously, to decide the SHAPE of the\n // expansion (how many nav-link buttons, whether the footer/header-action\n // slots exist). Per PiMetaProps, every prop — dynamic or static — arrives\n // as a StateMapper; resolveOnce() unwraps it a single time at mapper\n // run-time (the mapper itself only ever runs once, so these can't become\n // reactive regardless — see AGENT.md's metacards guide).\n const navLinks = resolveOnce(props.navLinks) ?? [];\n const iconName = resolveOnce(props.iconName);\n const wrapperClass = resolveOnce(props.className);\n const footer = resolveOnce(props.footer);\n\n // ── Title (icon + text) ─────────────────────────────────────────────────\n // props.title is StateMapper<string> — a function, NOT a string.\n // Typography's `text` prop is typed (via PiMapProps) as `string | StateMapper<string>`,\n // so we forward the selector function directly. The framework resolves it lazily\n // (calls props.title(state, ctx)) when Typography renders. This is different from\n // using props.title as a string, which TypeScript would reject.\n const titleContent: PiCardRef[] = [];\n if (iconName) {\n titleContent.push(\n Button({\n iconLabel: iconName,\n opts: {variant: \"none\", size: \"icon\"},\n className: \"h-6 w-6 shrink-0 pointer-events-none\",\n }),\n );\n }\n titleContent.push(\n Typography({\n text: props.title,\n level: \"large\",\n className: \"leading-none\",\n }),\n );\n\n const titleStack = reg(\n \"title\",\n Stack({\n direction: \"row\",\n alignItems: \"center\",\n spacing: 2,\n content: titleContent,\n }),\n );\n\n // ── Nav link buttons (shared between desktop and mobile) ────────────────\n const linkButtons = navLinks.map((link: NavLink, i: number) =>\n reg(\n `nav-link-${i}`,\n Button({\n id: link.id,\n label: link.title ?? capitalize(link.id),\n opts: {variant: \"ghost\"},\n className:\n \"px-0 text-muted-foreground hover:text-foreground h-auto p-0\",\n onClickedMapper: () => ({\n type: PAGE_WITH_NAVBAR_META_ACTION.NAVIGATE_TO,\n id: link.id,\n }),\n }),\n ),\n );\n\n // ── Desktop nav (md+): title + links in a row ───────────────────────────\n const desktopNav = reg(\n \"nav-md\",\n Conditional({\n showOn: \"md\",\n content: reg(\n \"nav-md-inner\",\n Stack({\n direction: \"row\",\n alignItems: \"center\",\n spacing: 5,\n content: [titleStack, ...linkButtons],\n }),\n ),\n }),\n );\n\n // ── Mobile nav (< md) ───────────────────────────────────────────────────\n // With nav links: hamburger drawer (left slide-in using pi/drawer).\n // Without nav links: just show the title inline.\n const mobileNav: PiCardRef =\n navLinks.length > 0\n ? reg(\n \"nav-sm\",\n Conditional({\n showOn: \"<md\",\n content: reg(\n \"nav-sm-drawer\",\n Drawer({\n direction: \"left\",\n trigger: Button({\n iconLabel: \"menu\",\n opts: {variant: \"outline\", size: \"icon\"},\n ariaLabel: \"Toggle navigation menu\",\n }),\n content: reg(\n \"nav-sm-content\",\n Stack({\n direction: \"column\",\n spacing: 4,\n className: \"p-4\",\n content: [titleStack, ...linkButtons],\n }),\n ),\n footerCloseButtonText: null,\n }),\n ),\n }),\n )\n : reg(\n \"nav-sm-title\",\n Conditional({\n showOn: \"<md\",\n content: titleStack,\n }),\n );\n\n // ── Header action slots ─────────────────────────────────────────────────\n // Structural — decide whether the action-slot Stack exists at all — so\n // read once, synchronously, like the other shape-controlling props above.\n const headerLeftCard = resolveOnce(props.headerLeftCard);\n const headerRightCard = resolveOnce(props.headerRightCard);\n const actionRefs: PiCardRef[] = [];\n if (headerLeftCard) {\n actionRefs.push(headerLeftCard);\n }\n if (headerRightCard) {\n actionRefs.push(headerRightCard);\n }\n\n const headerContent: PiCardRef[] = [desktopNav, mobileNav];\n if (actionRefs.length > 0) {\n headerContent.push(\n reg(\n \"header-actions\",\n Stack({\n direction: \"row\",\n alignItems: \"center\",\n className: \"ml-auto gap-2 lg:gap-4\",\n content: actionRefs,\n }),\n ),\n );\n }\n\n // ── Header ──────────────────────────────────────────────────────────────\n const header = reg(\n \"header\",\n Box({\n className:\n \"shrink-0 flex items-center gap-4 border-b bg-background px-4 md:px-6\",\n content: headerContent,\n }),\n );\n\n // ── Main ────────────────────────────────────────────────────────────────\n // props.main is a StateMapper — resolved lazily so React re-renders when\n // the referenced card changes.\n const mainBox = reg(\n \"main\",\n Box({\n className:\n \"flex-1 min-h-0 overflow-y-auto bg-background p-4 py-2 md:p-8 md:py-4\",\n content: ((_: unknown, ctx: PiMetaResolveCtx) => {\n const ref = ctx.resolve(props.main);\n return ref != null ? [ref as PiCardRef] : [];\n }) as unknown as PiCardRef[],\n }),\n );\n\n // ── Page column ─────────────────────────────────────────────────────────\n const pageContent: PiCardRef[] = [header, mainBox];\n\n // ── Footer (optional) ───────────────────────────────────────────────────\n // `footer` is a static plain PiCardRef — use directly.\n if (footer) {\n pageContent.push(\n reg(\n \"footer\",\n Box({\n className:\n \"shrink-0 bg-background text-muted-foreground px-4 py-2 md:px-8 md:py-4\",\n content: [footer],\n }),\n ),\n );\n }\n\n // ── Assemble ────────────────────────────────────────────────────────────\n return Stack({\n direction: \"row\",\n className: `w-full h-full justify-center${wrapperClass ? ` ${wrapperClass}` : \"\"}`,\n content: [\n reg(\n \"page\",\n Stack({\n direction: \"column\",\n className: \"flex-1 h-full w-full\",\n content: pageContent,\n }),\n ),\n ],\n });\n}\n\n// ── Registration ─────────────────────────────────────────────────────────────\n\nregisterMetaCard({\n type: PI_PAGE_WITH_NAVBAR_META_CARD,\n mapper: PageWithNavbarMetaMapper,\n events: PAGE_WITH_NAVBAR_META_ACTION,\n} satisfies PiRegisterMetaCard);\n"],"mappings":"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"}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { PiCardRef } from '@pihanga2/core';
|
|
2
2
|
export declare const PI_PAGE_WITH_NAVBAR_META_CARD = "pi/pageWithNavbarMeta";
|
|
3
|
-
export declare const PageWithNavbarMeta: <S extends import('@pihanga2/core').ReduxState>(p:
|
|
4
|
-
title: string | import('@pihanga2/core').StateMapper<string, S, import('@pihanga2/core').PiDefCtxtProps>;
|
|
5
|
-
main: PiCardRef | import('@pihanga2/core').StateMapper<PiCardRef, S, import('@pihanga2/core').PiDefCtxtProps>;
|
|
6
|
-
} & import('@pihanga2/core/types').EventHandler<PageWithNavbarMetaEvents, S> & import('@pihanga2/core/types').EventMapper<PageWithNavbarMetaEvents, import('@pihanga2/core').PiDefCtxtProps>) => import('@pihanga2/core').PiCardDef;
|
|
3
|
+
export declare const PageWithNavbarMeta: <S extends import('@pihanga2/core').ReduxState>(p: import('@pihanga2/core').PiMapProps<PageWithNavbarMetaDynProps & PageWithNavbarMetaStaticProps, S, PageWithNavbarMetaEvents>) => import('@pihanga2/core').PiCardDef;
|
|
7
4
|
export declare const PAGE_WITH_NAVBAR_META_ACTION: {
|
|
8
5
|
NAVIGATE_TO: string;
|
|
9
6
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FormFieldProps } from '../form/form.types';
|
|
1
2
|
export declare const PI_SELECT_CARD = "pi/select";
|
|
2
3
|
export declare const Select: <S extends import('@pihanga2/core').ReduxState>(p: import('@pihanga2/core').PiMapProps<PiSelectProps, S, PiSelectEvents>) => import('@pihanga2/core').PiCardDef;
|
|
3
4
|
export declare const PI_SELECT_ACTION: {
|
|
@@ -34,13 +35,7 @@ export type PiSelectOption = {
|
|
|
34
35
|
/** When true, the option is shown but cannot be selected. */
|
|
35
36
|
disabled?: boolean;
|
|
36
37
|
};
|
|
37
|
-
export type PiSelectProps = {
|
|
38
|
-
/**
|
|
39
|
-
* Field name used to bind to FormContext when inside a pi/form card.
|
|
40
|
-
* When provided the component reads its selected value from form data and
|
|
41
|
-
* writes back via form.handleChange.
|
|
42
|
-
*/
|
|
43
|
-
name?: string;
|
|
38
|
+
export type PiSelectProps = FormFieldProps & {
|
|
44
39
|
/**
|
|
45
40
|
* Controlled value used in standalone mode (outside a Form).
|
|
46
41
|
* Ignored when `name` is set and the component is inside a pi/form.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.types.js","names":[],"sources":["../../../src/cards/select/select.types.ts"],"sourcesContent":["import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_SELECT_CARD = \"pi/select\";\n\nexport const Select = createCardDeclaration<PiSelectProps, PiSelectEvents>(\n PI_SELECT_CARD,\n);\n\nexport const PI_SELECT_ACTION = registerActions(PI_SELECT_CARD, [\n \"changed\",\n \"opened\",\n \"closed\",\n]);\n\nexport const onSelectChanged = createOnAction<PiSelectChangedEvent>(\n PI_SELECT_ACTION.CHANGED,\n);\n\n/** @deprecated Use `onSelectChanged` instead. */\nexport const onPiSelectChanged = onSelectChanged;\n\nexport const onSelectOpened = createOnAction<PiSelectOpenedEvent>(\n PI_SELECT_ACTION.OPENED,\n);\n\n/** @deprecated Use `onSelectOpened` instead. */\nexport const onPiSelectOpened = onSelectOpened;\n\nexport const onSelectClosed = createOnAction<PiSelectClosedEvent>(\n PI_SELECT_ACTION.CLOSED,\n);\n\n/** @deprecated Use `onSelectClosed` instead. */\nexport const onPiSelectClosed = onSelectClosed;\n\nexport type PiSelectOption = {\n /** The value stored in form state / dispatched in events. */\n value: string;\n /** Human-readable label shown in the dropdown. */\n label: string;\n /** When true, the option is shown but cannot be selected. */\n disabled?: boolean;\n};\n\nexport type PiSelectProps =
|
|
1
|
+
{"version":3,"file":"select.types.js","names":[],"sources":["../../../src/cards/select/select.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_SELECT_CARD = \"pi/select\";\n\nexport const Select = createCardDeclaration<PiSelectProps, PiSelectEvents>(\n PI_SELECT_CARD,\n);\n\nexport const PI_SELECT_ACTION = registerActions(PI_SELECT_CARD, [\n \"changed\",\n \"opened\",\n \"closed\",\n]);\n\nexport const onSelectChanged = createOnAction<PiSelectChangedEvent>(\n PI_SELECT_ACTION.CHANGED,\n);\n\n/** @deprecated Use `onSelectChanged` instead. */\nexport const onPiSelectChanged = onSelectChanged;\n\nexport const onSelectOpened = createOnAction<PiSelectOpenedEvent>(\n PI_SELECT_ACTION.OPENED,\n);\n\n/** @deprecated Use `onSelectOpened` instead. */\nexport const onPiSelectOpened = onSelectOpened;\n\nexport const onSelectClosed = createOnAction<PiSelectClosedEvent>(\n PI_SELECT_ACTION.CLOSED,\n);\n\n/** @deprecated Use `onSelectClosed` instead. */\nexport const onPiSelectClosed = onSelectClosed;\n\nexport type PiSelectOption = {\n /** The value stored in form state / dispatched in events. */\n value: string;\n /** Human-readable label shown in the dropdown. */\n label: string;\n /** When true, the option is shown but cannot be selected. */\n disabled?: boolean;\n};\n\nexport type PiSelectProps = FormFieldProps & {\n /**\n * Controlled value used in standalone mode (outside a Form).\n * Ignored when `name` is set and the component is inside a pi/form.\n */\n value?: string;\n\n /**\n * When true the component manages its own selected-value state internally.\n * Selecting an item will still dispatch the normal `onChanged` action, but\n * the UI will immediately reflect the new selection without waiting for the\n * host application to update `value` via a reducer.\n *\n * Has no effect when the component is bound to a pi/form via `name`.\n */\n selfManaged?: boolean;\n\n /**\n * Default (uncontrolled) value. Mirrors Radix `defaultValue`.\n */\n defaultValue?: string;\n\n /** Available options to display in the dropdown. */\n options: PiSelectOption[];\n\n /** Placeholder text shown when no option is selected. */\n placeholder?: string;\n\n /** When true, the select is required (HTML form attribute). */\n required?: boolean;\n\n /** When true, the select is disabled and non-interactive. */\n disabled?: boolean;\n\n /** Accessible label for screen readers. */\n ariaLabel?: string;\n};\n\nexport type PiSelectChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** The value of the newly selected option. */\n value: string;\n};\n\nexport type PiSelectOpenedEvent = Record<string, never>;\n\nexport type PiSelectClosedEvent = Record<string, never>;\n\nexport type PiSelectEvents = {\n onChanged: PiSelectChangedEvent;\n onOpened: PiSelectOpenedEvent;\n onClosed: PiSelectClosedEvent;\n};\n"],"mappings":";;AAOA,IAAa,IAAiB,aAEjB,IAAS,EACpB,CACF,GAEa,IAAmB,EAAgB,GAAgB;CAC9D;CACA;CACA;AACF,CAAC,GAEY,IAAkB,EAC7B,EAAiB,OACnB,GAGa,IAAoB,GAEpB,IAAiB,EAC5B,EAAiB,MACnB,GAGa,IAAmB,GAEnB,IAAiB,EAC5B,EAAiB,MACnB,GAGa,IAAmB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FormFieldProps } from '../form/form.types';
|
|
1
2
|
export declare const PI_SWITCH_CARD = "pi/switch";
|
|
2
3
|
export declare const Switch: <S extends import('@pihanga2/core').ReduxState>(p: import('@pihanga2/core').PiMapProps<PiSwitchProps, S, PiSwitchEvents>) => import('@pihanga2/core').PiCardDef;
|
|
3
4
|
export declare const PI_SWITCH_ACTION: {
|
|
@@ -10,13 +11,7 @@ export declare const onSwitchChanged: <S extends import('@pihanga2/core').ReduxS
|
|
|
10
11
|
export declare const onPiSwitchChanged: <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
|
} & PiSwitchChangedEvent>) => void;
|
|
13
|
-
export type PiSwitchProps = {
|
|
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 PiSwitchProps = 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":"switch.types.js","names":[],"sources":["../../../src/cards/switch/switch.types.ts"],"sourcesContent":["import {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PI_SWITCH_CARD = \"pi/switch\";\n\nexport const Switch = createCardDeclaration<PiSwitchProps, PiSwitchEvents>(\n PI_SWITCH_CARD,\n);\n\nexport const PI_SWITCH_ACTION = registerActions(PI_SWITCH_CARD, [\"changed\"]);\n\nexport const onSwitchChanged = createOnAction<PiSwitchChangedEvent>(\n PI_SWITCH_ACTION.CHANGED,\n);\n\n/** @deprecated Use `onSwitchChanged` instead. */\nexport const onPiSwitchChanged = onSwitchChanged;\n\n// ---------------------------------------------------------------------------\n// Props & Events\n// ---------------------------------------------------------------------------\n\nexport type PiSwitchProps =
|
|
1
|
+
{"version":3,"file":"switch.types.js","names":[],"sources":["../../../src/cards/switch/switch.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_SWITCH_CARD = \"pi/switch\";\n\nexport const Switch = createCardDeclaration<PiSwitchProps, PiSwitchEvents>(\n PI_SWITCH_CARD,\n);\n\nexport const PI_SWITCH_ACTION = registerActions(PI_SWITCH_CARD, [\"changed\"]);\n\nexport const onSwitchChanged = createOnAction<PiSwitchChangedEvent>(\n PI_SWITCH_ACTION.CHANGED,\n);\n\n/** @deprecated Use `onSwitchChanged` instead. */\nexport const onPiSwitchChanged = onSwitchChanged;\n\n// ---------------------------------------------------------------------------\n// Props & Events\n// ---------------------------------------------------------------------------\n\nexport type PiSwitchProps = 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 /**\n * When true the component manages its own checked state internally via\n * React.useState, seeded from the `checked` prop.\n * The onChanged event still fires on every toggle so external listeners can\n * react. Useful in playground previews and self-contained demos where no\n * Redux state is wired up.\n */\n selfManaged?: boolean;\n\n /** When true, the switch is disabled and non-interactive. */\n disabled?: boolean;\n\n /**\n * Optional label text rendered beside the switch.\n * The label is automatically associated with the switch via `htmlFor`.\n */\n label?: string;\n\n /** Extra Tailwind / CSS classes forwarded to the root wrapper element. */\n className?: string;\n};\n\nexport type PiSwitchChangedEvent = {\n /** Field name, mirrors the `name` prop if provided. */\n name?: string;\n /** New checked state after the toggle. */\n checked: boolean;\n};\n\nexport type PiSwitchEvents = {\n onChanged: PiSwitchChangedEvent;\n};\n"],"mappings":";;AAOA,IAAa,IAAiB,aAEjB,IAAS,EACpB,CACF,GAEa,IAAmB,EAAgB,GAAgB,CAAC,SAAS,CAAC,GAE9D,IAAkB,EAC7B,EAAiB,OACnB,GAGa,IAAoB"}
|
|
@@ -43,7 +43,7 @@ var s = (s) => {
|
|
|
43
43
|
if (w && e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
44
44
|
e.preventDefault();
|
|
45
45
|
let t = e.currentTarget.value;
|
|
46
|
-
z && H.current != null && (clearTimeout(H.current), H.current = null, G(t)), M({
|
|
46
|
+
z && H.current != null && (clearTimeout(H.current), H.current = null, G(t)), L ? I.requestAction("submit") : M({
|
|
47
47
|
name: c,
|
|
48
48
|
value: t
|
|
49
49
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textArea.component.js","names":[],"sources":["../../../src/cards/textArea/textArea.component.tsx"],"sourcesContent":["import React, {useEffect, useRef, useState} from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {Textarea} from \"@/components/ui/textarea\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport {cn} from \"@/lib/utils\";\nimport type {\n CssLength,\n PiTextAreaEvents,\n PiTextAreaProps,\n} from \"./textArea.types\";\n\nfunction toCssLength(v: CssLength | undefined): string | undefined {\n if (v === undefined) return undefined;\n return typeof v === \"number\" ? `${v}px` : v;\n}\n\nexport const TextAreaComponent = (\n props: PiCardProps<PiTextAreaProps, PiTextAreaEvents>,\n): React.ReactNode => {\n const {\n name,\n value: propValue = \"\",\n placeholder,\n disabled,\n readOnly,\n rows = 4,\n cols,\n fieldSizing = \"fixed\",\n resize,\n minHeight,\n maxHeight,\n width,\n height,\n maxWidth,\n dir,\n writingMode,\n submitOnEnter,\n selfManaged = false,\n debounceMs = 0,\n className,\n style,\n autoFocus,\n cardName,\n onChanged,\n onSubmitted,\n } = props;\n\n // Focus the textarea once, the first time it is mounted/shown (e.g. when a\n // dialog or drawer opens). Intentionally an empty dependency array — this\n // must not re-fire on subsequent re-renders.\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n useEffect(() => {\n if (autoFocus) textareaRef.current?.focus();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // `id` may be injected by a parent pi/field card via Pihanga's extra-prop\n // forwarding. Not part of the official type so read through an escape hatch.\n const injectedId = (props as {id?: string}).id;\n const invalid = Boolean((props as {invalid?: boolean}).invalid);\n\n // Detect if we are inside a pi/form card via React context. When inside a\n // form, the form itself is the source of truth and buffering mechanism\n // (see pi/input, pi/text-field, etc.) — every keystroke writes straight to\n // form.formData via handleChange, and `selfManaged`/`debounceMs` are\n // ignored: there's no separate \"commit\" step to defer since the form's own\n // submit action is that commit point.\n const form = useFormContext();\n const useFormData = form.isInForm && Boolean(name);\n\n const canonicalValue = useFormData\n ? ((form.formData[name!] as string | undefined) ?? \"\")\n : propValue;\n\n // Self-managed buffering only applies in standalone mode.\n const applySelfManaged = selfManaged && !useFormData;\n\n // ---------------------------------------------------------------------\n // Self-managed mode (standalone only): keep an internal, always-up-to-date\n // text buffer so typing never lags, but only dispatch the outgoing\n // onChanged event on blur (and, optionally, after a trailing debounce\n // window) — not on every keystroke.\n // ---------------------------------------------------------------------\n const [localValue, setLocalValue] = useState(canonicalValue);\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const pendingValue = useRef<string>(canonicalValue);\n\n // Keep the local buffer in sync if the canonical value changes from\n // outside (e.g. a reset) while not actively editing.\n useEffect(() => {\n if (applySelfManaged) setLocalValue(canonicalValue);\n }, [canonicalValue, applySelfManaged]);\n\n useEffect(() => {\n return () => {\n if (debounceTimer.current != null) clearTimeout(debounceTimer.current);\n };\n }, []);\n\n const value = applySelfManaged ? localValue : canonicalValue;\n\n function commitChange(newValue: string) {\n if (useFormData) {\n form.handleChange(name!, newValue);\n } else {\n onChanged({name, value: newValue});\n }\n }\n\n function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {\n const newValue = e.target.value;\n\n if (!applySelfManaged) {\n // Inside a form, or selfManaged not requested: commit on every\n // keystroke, matching pi/input's default (non-onCommitted) behaviour.\n commitChange(newValue);\n return;\n }\n\n // Self-managed (standalone): update the visible text immediately, defer\n // the outgoing event.\n setLocalValue(newValue);\n pendingValue.current = newValue;\n\n if (debounceMs > 0) {\n if (debounceTimer.current != null) clearTimeout(debounceTimer.current);\n debounceTimer.current = setTimeout(() => {\n debounceTimer.current = null;\n commitChange(pendingValue.current);\n }, debounceMs);\n }\n }\n\n function handleBlur() {\n if (!applySelfManaged) return;\n if (debounceTimer.current != null) {\n clearTimeout(debounceTimer.current);\n debounceTimer.current = null;\n }\n commitChange(pendingValue.current);\n }\n\n function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {\n if (\n submitOnEnter &&\n e.key === \"Enter\" &&\n !e.shiftKey &&\n !e.ctrlKey &&\n !e.metaKey &&\n !e.altKey\n ) {\n e.preventDefault();\n const currentValue = e.currentTarget.value;\n if (applySelfManaged && debounceTimer.current != null) {\n clearTimeout(debounceTimer.current);\n debounceTimer.current = null;\n commitChange(currentValue);\n }\n onSubmitted({name, value: currentValue});\n }\n }\n\n // Default resize behaviour: content-sizing boxes shouldn't be manually\n // resizable (dragging writes an inline width/height that permanently\n // defeats field-sizing: content).\n const resolvedResize =\n resize ?? (fieldSizing === \"content\" ? \"none\" : \"vertical\");\n\n const resizeClass =\n resolvedResize === \"none\"\n ? \"resize-none\"\n : resolvedResize === \"vertical\"\n ? \"resize-y\"\n : resolvedResize === \"horizontal\"\n ? \"resize-x\"\n : \"resize\";\n\n // `field-sizing` is expressed as a Tailwind utility class (shipped by\n // Tailwind v4) rather than an inline style — this is both more idiomatic\n // for this codebase (the base Textarea already defaults to\n // `field-sizing-content`) and avoids relying on very new CSS properties\n // being present in every CSSStyleDeclaration IDL implementation.\n const fieldSizingClass =\n fieldSizing === \"content\" ? \"field-sizing-content\" : \"field-sizing-fixed\";\n\n const inlineStyle: React.CSSProperties = {\n minHeight: toCssLength(minHeight),\n maxHeight: toCssLength(maxHeight),\n width: toCssLength(width),\n height: toCssLength(height),\n maxWidth: toCssLength(maxWidth),\n writingMode,\n ...style,\n };\n\n // Use the id injected by pi/field (for label↔control linking); fall back\n // to a locally-generated id when used standalone.\n const selfId = `${cardName}-${name ?? \"field\"}`;\n const fieldId = injectedId ?? selfId;\n\n return (\n <Textarea\n ref={textareaRef}\n id={fieldId}\n value={value}\n onChange={handleChange}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled}\n readOnly={readOnly}\n rows={fieldSizing === \"fixed\" ? rows : undefined}\n cols={fieldSizing === \"fixed\" ? cols : undefined}\n dir={dir}\n aria-invalid={invalid || undefined}\n data-pihanga={cardName}\n className={cn(fieldSizingClass, resizeClass, className)}\n style={inlineStyle}\n />\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"textArea.component.js","names":[],"sources":["../../../src/cards/textArea/textArea.component.tsx"],"sourcesContent":["import React, {useEffect, useRef, useState} from \"react\";\nimport {type PiCardProps} from \"@pihanga2/core\";\nimport {Textarea} from \"@/components/ui/textarea\";\nimport {useFormContext} from \"@/cards/form/form.context\";\nimport {cn} from \"@/lib/utils\";\nimport type {\n CssLength,\n PiTextAreaEvents,\n PiTextAreaProps,\n} from \"./textArea.types\";\n\nfunction toCssLength(v: CssLength | undefined): string | undefined {\n if (v === undefined) return undefined;\n return typeof v === \"number\" ? `${v}px` : v;\n}\n\nexport const TextAreaComponent = (\n props: PiCardProps<PiTextAreaProps, PiTextAreaEvents>,\n): React.ReactNode => {\n const {\n name,\n value: propValue = \"\",\n placeholder,\n disabled,\n readOnly,\n rows = 4,\n cols,\n fieldSizing = \"fixed\",\n resize,\n minHeight,\n maxHeight,\n width,\n height,\n maxWidth,\n dir,\n writingMode,\n submitOnEnter,\n selfManaged = false,\n debounceMs = 0,\n className,\n style,\n autoFocus,\n cardName,\n onChanged,\n onSubmitted,\n } = props;\n\n // Focus the textarea once, the first time it is mounted/shown (e.g. when a\n // dialog or drawer opens). Intentionally an empty dependency array — this\n // must not re-fire on subsequent re-renders.\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n useEffect(() => {\n if (autoFocus) textareaRef.current?.focus();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // `id` may be injected by a parent pi/field card via Pihanga's extra-prop\n // forwarding. Not part of the official type so read through an escape hatch.\n const injectedId = (props as {id?: string}).id;\n const invalid = Boolean((props as {invalid?: boolean}).invalid);\n\n // Detect if we are inside a pi/form card via React context. When inside a\n // form, the form itself is the source of truth and buffering mechanism\n // (see pi/input, pi/text-field, etc.) — every keystroke writes straight to\n // form.formData via handleChange, and `selfManaged`/`debounceMs` are\n // ignored: there's no separate \"commit\" step to defer since the form's own\n // submit action is that commit point.\n const form = useFormContext();\n const useFormData = form.isInForm && Boolean(name);\n\n const canonicalValue = useFormData\n ? ((form.formData[name!] as string | undefined) ?? \"\")\n : propValue;\n\n // Self-managed buffering only applies in standalone mode.\n const applySelfManaged = selfManaged && !useFormData;\n\n // ---------------------------------------------------------------------\n // Self-managed mode (standalone only): keep an internal, always-up-to-date\n // text buffer so typing never lags, but only dispatch the outgoing\n // onChanged event on blur (and, optionally, after a trailing debounce\n // window) — not on every keystroke.\n // ---------------------------------------------------------------------\n const [localValue, setLocalValue] = useState(canonicalValue);\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const pendingValue = useRef<string>(canonicalValue);\n\n // Keep the local buffer in sync if the canonical value changes from\n // outside (e.g. a reset) while not actively editing.\n useEffect(() => {\n if (applySelfManaged) setLocalValue(canonicalValue);\n }, [canonicalValue, applySelfManaged]);\n\n useEffect(() => {\n return () => {\n if (debounceTimer.current != null) clearTimeout(debounceTimer.current);\n };\n }, []);\n\n const value = applySelfManaged ? localValue : canonicalValue;\n\n function commitChange(newValue: string) {\n if (useFormData) {\n form.handleChange(name!, newValue);\n } else {\n onChanged({name, value: newValue});\n }\n }\n\n function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {\n const newValue = e.target.value;\n\n if (!applySelfManaged) {\n // Inside a form, or selfManaged not requested: commit on every\n // keystroke, matching pi/input's default (non-onCommitted) behaviour.\n commitChange(newValue);\n return;\n }\n\n // Self-managed (standalone): update the visible text immediately, defer\n // the outgoing event.\n setLocalValue(newValue);\n pendingValue.current = newValue;\n\n if (debounceMs > 0) {\n if (debounceTimer.current != null) clearTimeout(debounceTimer.current);\n debounceTimer.current = setTimeout(() => {\n debounceTimer.current = null;\n commitChange(pendingValue.current);\n }, debounceMs);\n }\n }\n\n function handleBlur() {\n if (!applySelfManaged) return;\n if (debounceTimer.current != null) {\n clearTimeout(debounceTimer.current);\n debounceTimer.current = null;\n }\n commitChange(pendingValue.current);\n }\n\n function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {\n if (\n submitOnEnter &&\n e.key === \"Enter\" &&\n !e.shiftKey &&\n !e.ctrlKey &&\n !e.metaKey &&\n !e.altKey\n ) {\n e.preventDefault();\n const currentValue = e.currentTarget.value;\n if (applySelfManaged && debounceTimer.current != null) {\n clearTimeout(debounceTimer.current);\n debounceTimer.current = null;\n commitChange(currentValue);\n }\n if (useFormData) {\n // Inside a form, form.formData is already current (every keystroke\n // wrote through handleChange), so request the form-level submit —\n // consistent with pi/input's and pi/text-field's submitOnEnter.\n form.requestAction(\"submit\");\n } else {\n onSubmitted({name, value: currentValue});\n }\n }\n }\n\n // Default resize behaviour: content-sizing boxes shouldn't be manually\n // resizable (dragging writes an inline width/height that permanently\n // defeats field-sizing: content).\n const resolvedResize =\n resize ?? (fieldSizing === \"content\" ? \"none\" : \"vertical\");\n\n const resizeClass =\n resolvedResize === \"none\"\n ? \"resize-none\"\n : resolvedResize === \"vertical\"\n ? \"resize-y\"\n : resolvedResize === \"horizontal\"\n ? \"resize-x\"\n : \"resize\";\n\n // `field-sizing` is expressed as a Tailwind utility class (shipped by\n // Tailwind v4) rather than an inline style — this is both more idiomatic\n // for this codebase (the base Textarea already defaults to\n // `field-sizing-content`) and avoids relying on very new CSS properties\n // being present in every CSSStyleDeclaration IDL implementation.\n const fieldSizingClass =\n fieldSizing === \"content\" ? \"field-sizing-content\" : \"field-sizing-fixed\";\n\n const inlineStyle: React.CSSProperties = {\n minHeight: toCssLength(minHeight),\n maxHeight: toCssLength(maxHeight),\n width: toCssLength(width),\n height: toCssLength(height),\n maxWidth: toCssLength(maxWidth),\n writingMode,\n ...style,\n };\n\n // Use the id injected by pi/field (for label↔control linking); fall back\n // to a locally-generated id when used standalone.\n const selfId = `${cardName}-${name ?? \"field\"}`;\n const fieldId = injectedId ?? selfId;\n\n return (\n <Textarea\n ref={textareaRef}\n id={fieldId}\n value={value}\n onChange={handleChange}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled}\n readOnly={readOnly}\n rows={fieldSizing === \"fixed\" ? rows : undefined}\n cols={fieldSizing === \"fixed\" ? cols : undefined}\n dir={dir}\n aria-invalid={invalid || undefined}\n data-pihanga={cardName}\n className={cn(fieldSizingClass, resizeClass, className)}\n style={inlineStyle}\n />\n );\n};\n"],"mappings":";;;;;;;AAWA,SAAS,EAAY,GAA8C;CAC7D,UAAM,KAAA,GACV,OAAO,OAAO,KAAM,WAAW,GAAG,EAAE,MAAM;AAC5C;AAEA,IAAa,KACX,MACoB;CACpB,IAAM,EACJ,SACA,OAAO,IAAY,IACnB,gBACA,aACA,aACA,UAAO,GACP,SACA,iBAAc,SACd,WACA,cACA,cACA,UACA,WACA,aACA,QACA,gBACA,kBACA,iBAAc,IACd,gBAAa,GACb,cACA,UACA,cACA,aACA,cACA,mBACE,GAKE,IAAc,EAA4B,IAAI;CACpD,QAAgB;EACd,AAAI,KAAW,EAAY,SAAS,MAAM;CAE5C,GAAG,CAAC,CAAC;CAIL,IAAM,IAAc,EAAwB,IACtC,IAAU,EAAS,EAA8B,SAQjD,IAAO,EAAe,GACtB,IAAc,EAAK,YAAY,EAAQ,GAEvC,IAAiB,IACjB,EAAK,SAAS,MAAiC,KACjD,GAGE,IAAmB,KAAe,CAAC,GAQnC,CAAC,GAAY,KAAiB,GAAS,CAAc,GACrD,IAAgB,EAA6C,IAAI,GACjE,IAAe,EAAe,CAAc;CAQlD,AAJA,QAAgB;EACd,AAAI,KAAkB,EAAc,CAAc;CACpD,GAAG,CAAC,GAAgB,CAAgB,CAAC,GAErC,cACe;EACX,AAAI,EAAc,WAAW,QAAM,aAAa,EAAc,OAAO;CACvE,GACC,CAAC,CAAC;CAEL,IAAM,IAAQ,IAAmB,IAAa;CAE9C,SAAS,EAAa,GAAkB;EACtC,AAAI,IACF,EAAK,aAAa,GAAO,CAAQ,IAEjC,EAAU;GAAC;GAAM,OAAO;EAAQ,CAAC;CAErC;CAEA,SAAS,EAAa,GAA2C;EAC/D,IAAM,IAAW,EAAE,OAAO;EAE1B,IAAI,CAAC,GAAkB;GAGrB,EAAa,CAAQ;GACrB;EACF;EAOA,AAHA,EAAc,CAAQ,GACtB,EAAa,UAAU,GAEnB,IAAa,MACX,EAAc,WAAW,QAAM,aAAa,EAAc,OAAO,GACrE,EAAc,UAAU,iBAAiB;GAEvC,AADA,EAAc,UAAU,MACxB,EAAa,EAAa,OAAO;EACnC,GAAG,CAAU;CAEjB;CAEA,SAAS,IAAa;EACf,MACD,EAAc,WAAW,SAC3B,aAAa,EAAc,OAAO,GAClC,EAAc,UAAU,OAE1B,EAAa,EAAa,OAAO;CACnC;CAEA,SAAS,EAAc,GAA6C;EAClE,IACE,KACA,EAAE,QAAQ,WACV,CAAC,EAAE,YACH,CAAC,EAAE,WACH,CAAC,EAAE,WACH,CAAC,EAAE,QACH;GACA,EAAE,eAAe;GACjB,IAAM,IAAe,EAAE,cAAc;GAMrC,AALI,KAAoB,EAAc,WAAW,SAC/C,aAAa,EAAc,OAAO,GAClC,EAAc,UAAU,MACxB,EAAa,CAAY,IAEvB,IAIF,EAAK,cAAc,QAAQ,IAE3B,EAAY;IAAC;IAAM,OAAO;GAAY,CAAC;EAE3C;CACF;CAKA,IAAM,IACJ,MAAW,MAAgB,YAAY,SAAS,aAE5C,IACJ,MAAmB,SACf,gBACA,MAAmB,aACjB,aACA,MAAmB,eACjB,aACA,UAOJ,IACJ,MAAgB,YAAY,yBAAyB,sBAEjD,IAAmC;EACvC,WAAW,EAAY,CAAS;EAChC,WAAW,EAAY,CAAS;EAChC,OAAO,EAAY,CAAK;EACxB,QAAQ,EAAY,CAAM;EAC1B,UAAU,EAAY,CAAQ;EAC9B;EACA,GAAG;CACL,GAIM,IAAS,GAAG,EAAS,GAAG,KAAQ;CAGtC,OACE,kBAAC,GAAD;EACE,KAAK;EACL,IALY,KAAc;EAMnB;EACP,UAAU;EACV,QAAQ;EACR,WAAW;EACE;EACH;EACA;EACV,MAAM,MAAgB,UAAU,IAAO,KAAA;EACvC,MAAM,MAAgB,UAAU,IAAO,KAAA;EAClC;EACL,gBAAc,KAAW,KAAA;EACzB,gBAAc;EACd,WAAW,EAAG,GAAkB,GAAa,CAAS;EACtD,OAAO;CACR,CAAA;AAEL"}
|