@asteby/metacore-runtime-react 13.4.1 → 13.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 13.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0e427f1: feat(forms): modern date picker, roomy line-items modal, 2-column layout
8
+
9
+ Three declarative-form polish items, all driven by field shape — zero per-app code:
10
+ - **DynamicDateField**: `type: "date"` fields now render a shadcn Calendar inside
11
+ a Popover instead of the native `<input type="date">`. The Popover portals to
12
+ the body so the calendar is never clipped by the modal (fixes the cut-off), and
13
+ it looks modern. The field value stays an ISO `YYYY-MM-DD` string, so payloads
14
+ are unchanged. No future-date restriction (entries can be post-dated).
15
+ - **Roomy modal for line-items**: GenericActionModal auto-widens to ~820px when
16
+ the action has a line-items (`type:"array"`) field so the debit/credit grid has
17
+ room; plain forms stay compact. An optional `action.modalWidth` overrides.
18
+ Applied as an inline style so it always takes effect.
19
+ - **2-column field layout**: scalar fields (journal, date, reference) flow
20
+ side-by-side instead of one tall vertical stack; line-items grids and textareas
21
+ span full width. Mirrors DynamicForm so the action modal and standalone form
22
+ render identically.
23
+
3
24
  ## 13.4.1
4
25
 
5
26
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA6CA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAEhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,kDAiDlB"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA8CA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAEhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,kDAiDlB"}
@@ -16,6 +16,7 @@ import { useApi } from './api-context';
16
16
  import { DynamicIcon } from './dynamic-icon';
17
17
  import { DynamicLineItems } from './dynamic-line-items';
18
18
  import { DynamicSelectField } from './dynamic-select-field';
19
+ import { DynamicDateField } from './dynamic-date-field';
19
20
  import { isLineItemsField, resolveWidget } from './dynamic-form-schema';
20
21
  // Canonical registry lives in @asteby/metacore-sdk
21
22
  import { getActionComponent, } from '@asteby/metacore-sdk';
@@ -120,7 +121,26 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
120
121
  setExecuting(false);
121
122
  }
122
123
  };
123
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), action.label] }), action.confirmMessage && _jsx(DialogDescription, { children: action.confirmMessage })] }), _jsx("div", { className: "grid gap-4 py-4", children: action.fields?.map((field) => (_jsxs("div", { className: "grid gap-2", children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), renderField(field, formData[field.key], (v) => updateField(field.key, v))] }, field.key))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] })] })] }) }));
124
+ // Size the modal to the form. A line-items form (the debit/credit grid of a
125
+ // journal entry, a "receive goods" modal) needs room for its columns, so it
126
+ // gets a roomy width; a plain field form stays compact. An explicit
127
+ // `action.modalWidth` (number px or CSS length) overrides — the declarative
128
+ // escape hatch. Width is applied as an inline style (guaranteed to apply,
129
+ // unlike an arbitrary Tailwind class that the host's scan may drop), capped
130
+ // to the viewport so it stays responsive on phones.
131
+ const hasLineItems = useMemo(() => (action.fields ?? []).some(isLineItemsField), [action.fields]);
132
+ const explicitWidth = action.modalWidth;
133
+ const widthPx = explicitWidth != null
134
+ ? (typeof explicitWidth === 'number' ? `${explicitWidth}px` : explicitWidth)
135
+ : hasLineItems
136
+ ? '820px'
137
+ : undefined;
138
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: widthPx ? '' : 'sm:max-w-lg', style: widthPx ? { maxWidth: widthPx, width: '95vw' } : undefined, children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), action.label] }), action.confirmMessage && _jsx(DialogDescription, { children: action.confirmMessage })] }), _jsx("div", { className: "grid gap-4 py-4 sm:grid-cols-2", children: action.fields?.map((field) => {
139
+ const fullWidth = isLineItemsField(field) ||
140
+ resolveWidget(field) === 'textarea' ||
141
+ resolveWidget(field) === 'richtext';
142
+ return (_jsxs("div", { className: 'grid gap-2 ' + (fullWidth ? 'sm:col-span-2' : ''), children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), renderField(field, formData[field.key], (v) => updateField(field.key, v))] }, field.key));
143
+ }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] })] })] }) }));
124
144
  }
125
145
  function renderField(field, value, onChange) {
126
146
  // Repeatable line-items group → row grid (value is an array of row objects).
@@ -145,7 +165,9 @@ function renderField(field, value, onChange) {
145
165
  case 'number':
146
166
  return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => onChange(e.target.valueAsNumber || ''), placeholder: field.placeholder });
147
167
  case 'date':
148
- return _jsx(Input, { id: field.key, type: "date", value: value || '', onChange: (e) => onChange(e.target.value) });
168
+ // Modern shadcn Calendar in a Popover (portaled, never clipped by the
169
+ // modal) instead of the native, dated, easily-cut <input type=date>.
170
+ return _jsx(DynamicDateField, { field: field, value: value, onChange: onChange });
149
171
  default:
150
172
  return _jsx(Input, { id: field.key, type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
151
173
  }
@@ -0,0 +1,9 @@
1
+ import type { ActionFieldDef } from './types';
2
+ export interface DynamicDateFieldProps {
3
+ field: ActionFieldDef;
4
+ value: any;
5
+ onChange: (v: any) => void;
6
+ }
7
+ export declare function DynamicDateField({ field, value, onChange }: DynamicDateFieldProps): import("react/jsx-runtime").JSX.Element;
8
+ export default DynamicDateField;
9
+ //# sourceMappingURL=dynamic-date-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-date-field.d.ts","sourceRoot":"","sources":["../src/dynamic-date-field.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AA+BD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,qBAAqB,2CAkCjF;AAED,eAAe,gBAAgB,CAAA"}
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // DynamicDateField — modern date picker for declarative forms: a shadcn
3
+ // Calendar inside a Popover, instead of the native `<input type="date">` whose
4
+ // browser calendar looks dated and gets clipped inside a modal.
5
+ //
6
+ // Contract: the field VALUE stays an ISO date string ("YYYY-MM-DD") so the form
7
+ // payload is unchanged (the kernel/handlers already expect that). We parse the
8
+ // string to a Date for the Calendar and format back to ISO on select. The
9
+ // Popover portals to the document body, so the calendar is never clipped by the
10
+ // modal's overflow — fixing the "se corta" cut-off.
11
+ //
12
+ // Deliberately NOT reusing metacore-ui's `DatePicker` shared helper: that one
13
+ // hard-disables future dates and pins a fixed 240px width, neither of which is
14
+ // right for a generic declarative field (a journal entry can be post-dated).
15
+ import { useState } from 'react';
16
+ import { Button, Calendar, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
17
+ import { CalendarIcon } from 'lucide-react';
18
+ // Parse "YYYY-MM-DD" (or any Date-parseable string) into a local Date, or
19
+ // undefined when empty/invalid. Uses noon to dodge timezone day-shift.
20
+ function toDate(v) {
21
+ if (!v)
22
+ return undefined;
23
+ const s = String(v);
24
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
25
+ if (m)
26
+ return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 12);
27
+ const d = new Date(s);
28
+ return isNaN(d.getTime()) ? undefined : d;
29
+ }
30
+ // Format a Date back to "YYYY-MM-DD" (local, no timezone shift).
31
+ function toISO(d) {
32
+ const y = d.getFullYear();
33
+ const m = String(d.getMonth() + 1).padStart(2, '0');
34
+ const day = String(d.getDate()).padStart(2, '0');
35
+ return `${y}-${m}-${day}`;
36
+ }
37
+ // Human label for the trigger. Locale-aware, falls back to the raw ISO.
38
+ function label(d) {
39
+ if (!d)
40
+ return '';
41
+ try {
42
+ return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
43
+ }
44
+ catch {
45
+ return toISO(d);
46
+ }
47
+ }
48
+ export function DynamicDateField({ field, value, onChange }) {
49
+ const [open, setOpen] = useState(false);
50
+ const selected = toDate(value);
51
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", id: field.key, "data-empty": !selected, className: "w-full justify-start text-start font-normal data-[empty=true]:text-muted-foreground", children: [_jsx(CalendarIcon, { className: "mr-2 size-4 shrink-0 opacity-50" }), _jsx("span", { className: "truncate", children: selected ? label(selected) : (field.placeholder || 'Elegí una fecha') })] }) }), _jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: _jsx(Calendar, { mode: "single", captionLayout: "dropdown", selected: selected, defaultMonth: selected, onSelect: (d) => {
52
+ onChange(d ? toISO(d) : '');
53
+ setOpen(false);
54
+ } }) })] }));
55
+ }
56
+ export default DynamicDateField;
@@ -3,6 +3,7 @@ import { buildZodSchema, resolveWidget } from './dynamic-form-schema';
3
3
  export { buildZodSchema, resolveWidget };
4
4
  export { DynamicLineItems } from './dynamic-line-items';
5
5
  export { DynamicSelectField } from './dynamic-select-field';
6
+ export { DynamicDateField } from './dynamic-date-field';
6
7
  export interface DynamicFormProps {
7
8
  fields: ActionFieldDef[];
8
9
  initialValues?: Record<string, any>;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAGhB,MAAM,uBAAuB,CAAA;AAK9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAE3D,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,2CAkGlB"}
1
+ {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAGhB,MAAM,uBAAuB,CAAA;AAM9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAEvD,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,2CAkGlB"}
@@ -8,9 +8,11 @@ import { buildZodSchema, resolveWidget, isLineItemsField, evaluateBalance, } fro
8
8
  import { useOptionsResolver } from './use-options-resolver';
9
9
  import { DynamicLineItems } from './dynamic-line-items';
10
10
  import { DynamicSelectField } from './dynamic-select-field';
11
+ import { DynamicDateField } from './dynamic-date-field';
11
12
  export { buildZodSchema, resolveWidget };
12
13
  export { DynamicLineItems } from './dynamic-line-items';
13
14
  export { DynamicSelectField } from './dynamic-select-field';
15
+ export { DynamicDateField } from './dynamic-date-field';
14
16
  export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, }) {
15
17
  const [values, setValues] = useState({});
16
18
  const [errors, setErrors] = useState({});
@@ -113,7 +115,7 @@ function FieldRenderer({ field, value, onChange }) {
113
115
  case 'number':
114
116
  return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => onChange(e.target.valueAsNumber || ''), placeholder: field.placeholder });
115
117
  case 'date':
116
- return _jsx(Input, { id: field.key, type: "date", value: value || '', onChange: (e) => onChange(e.target.value) });
118
+ return _jsx(DynamicDateField, { field: field, value: value, onChange: onChange });
117
119
  default:
118
120
  return _jsx(Input, { id: field.key, type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
119
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "13.4.1",
3
+ "version": "13.5.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,6 +40,7 @@ import { useApi } from './api-context'
40
40
  import { DynamicIcon } from './dynamic-icon'
41
41
  import { DynamicLineItems } from './dynamic-line-items'
42
42
  import { DynamicSelectField } from './dynamic-select-field'
43
+ import { DynamicDateField } from './dynamic-date-field'
43
44
  import { isLineItemsField, resolveWidget } from './dynamic-form-schema'
44
45
  import type { ActionFieldDef } from './types'
45
46
  // Canonical registry lives in @asteby/metacore-sdk
@@ -224,9 +225,31 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
224
225
  }
225
226
  }
226
227
 
228
+ // Size the modal to the form. A line-items form (the debit/credit grid of a
229
+ // journal entry, a "receive goods" modal) needs room for its columns, so it
230
+ // gets a roomy width; a plain field form stays compact. An explicit
231
+ // `action.modalWidth` (number px or CSS length) overrides — the declarative
232
+ // escape hatch. Width is applied as an inline style (guaranteed to apply,
233
+ // unlike an arbitrary Tailwind class that the host's scan may drop), capped
234
+ // to the viewport so it stays responsive on phones.
235
+ const hasLineItems = useMemo(
236
+ () => (action.fields ?? []).some(isLineItemsField),
237
+ [action.fields],
238
+ )
239
+ const explicitWidth = (action as unknown as { modalWidth?: number | string }).modalWidth
240
+ const widthPx =
241
+ explicitWidth != null
242
+ ? (typeof explicitWidth === 'number' ? `${explicitWidth}px` : explicitWidth)
243
+ : hasLineItems
244
+ ? '820px'
245
+ : undefined
246
+
227
247
  return (
228
248
  <Dialog open={open} onOpenChange={onOpenChange}>
229
- <DialogContent className="sm:max-w-lg">
249
+ <DialogContent
250
+ className={widthPx ? '' : 'sm:max-w-lg'}
251
+ style={widthPx ? { maxWidth: widthPx, width: '95vw' } : undefined}
252
+ >
230
253
  <DialogHeader>
231
254
  <DialogTitle className="flex items-center gap-2">
232
255
  <DynamicIcon name={action.icon} className="h-5 w-5" />
@@ -234,16 +257,30 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
234
257
  </DialogTitle>
235
258
  {action.confirmMessage && <DialogDescription>{action.confirmMessage}</DialogDescription>}
236
259
  </DialogHeader>
237
- <div className="grid gap-4 py-4">
238
- {action.fields?.map((field) => (
239
- <div key={field.key} className="grid gap-2">
240
- <Label htmlFor={field.key}>
241
- {field.label}
242
- {field.required && <span className="text-red-500 ml-1">*</span>}
243
- </Label>
244
- {renderField(field, formData[field.key], (v: any) => updateField(field.key, v))}
245
- </div>
246
- ))}
260
+ {/* Responsive 2-column grid: scalar fields (journal, date,
261
+ reference) flow side-by-side instead of one tall vertical
262
+ stack; line-items grids and textareas span the full width so
263
+ they get room. Mirrors DynamicForm's pro layout — driven only
264
+ by field shape, fully declarative. */}
265
+ <div className="grid gap-4 py-4 sm:grid-cols-2">
266
+ {action.fields?.map((field) => {
267
+ const fullWidth =
268
+ isLineItemsField(field) ||
269
+ resolveWidget(field) === 'textarea' ||
270
+ resolveWidget(field) === 'richtext'
271
+ return (
272
+ <div
273
+ key={field.key}
274
+ className={'grid gap-2 ' + (fullWidth ? 'sm:col-span-2' : '')}
275
+ >
276
+ <Label htmlFor={field.key}>
277
+ {field.label}
278
+ {field.required && <span className="text-red-500 ml-1">*</span>}
279
+ </Label>
280
+ {renderField(field, formData[field.key], (v: any) => updateField(field.key, v))}
281
+ </div>
282
+ )
283
+ })}
247
284
  </div>
248
285
  <DialogFooter>
249
286
  <Button variant="outline" onClick={() => onOpenChange(false)} disabled={executing}>
@@ -297,7 +334,9 @@ function renderField(
297
334
  case 'number':
298
335
  return <Input id={field.key} type="number" value={value ?? ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.valueAsNumber || '')} placeholder={field.placeholder} />
299
336
  case 'date':
300
- return <Input id={field.key} type="date" value={value || ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} />
337
+ // Modern shadcn Calendar in a Popover (portaled, never clipped by the
338
+ // modal) instead of the native, dated, easily-cut <input type=date>.
339
+ return <DynamicDateField field={field} value={value} onChange={onChange} />
301
340
  default:
302
341
  return <Input id={field.key} type={field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text'} value={value || ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} placeholder={field.placeholder} />
303
342
  }
@@ -0,0 +1,96 @@
1
+ // DynamicDateField — modern date picker for declarative forms: a shadcn
2
+ // Calendar inside a Popover, instead of the native `<input type="date">` whose
3
+ // browser calendar looks dated and gets clipped inside a modal.
4
+ //
5
+ // Contract: the field VALUE stays an ISO date string ("YYYY-MM-DD") so the form
6
+ // payload is unchanged (the kernel/handlers already expect that). We parse the
7
+ // string to a Date for the Calendar and format back to ISO on select. The
8
+ // Popover portals to the document body, so the calendar is never clipped by the
9
+ // modal's overflow — fixing the "se corta" cut-off.
10
+ //
11
+ // Deliberately NOT reusing metacore-ui's `DatePicker` shared helper: that one
12
+ // hard-disables future dates and pins a fixed 240px width, neither of which is
13
+ // right for a generic declarative field (a journal entry can be post-dated).
14
+ import { useState } from 'react'
15
+ import {
16
+ Button,
17
+ Calendar,
18
+ Popover,
19
+ PopoverContent,
20
+ PopoverTrigger,
21
+ } from '@asteby/metacore-ui/primitives'
22
+ import { CalendarIcon } from 'lucide-react'
23
+ import type { ActionFieldDef } from './types'
24
+
25
+ export interface DynamicDateFieldProps {
26
+ field: ActionFieldDef
27
+ value: any
28
+ onChange: (v: any) => void
29
+ }
30
+
31
+ // Parse "YYYY-MM-DD" (or any Date-parseable string) into a local Date, or
32
+ // undefined when empty/invalid. Uses noon to dodge timezone day-shift.
33
+ function toDate(v: any): Date | undefined {
34
+ if (!v) return undefined
35
+ const s = String(v)
36
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s)
37
+ if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 12)
38
+ const d = new Date(s)
39
+ return isNaN(d.getTime()) ? undefined : d
40
+ }
41
+
42
+ // Format a Date back to "YYYY-MM-DD" (local, no timezone shift).
43
+ function toISO(d: Date): string {
44
+ const y = d.getFullYear()
45
+ const m = String(d.getMonth() + 1).padStart(2, '0')
46
+ const day = String(d.getDate()).padStart(2, '0')
47
+ return `${y}-${m}-${day}`
48
+ }
49
+
50
+ // Human label for the trigger. Locale-aware, falls back to the raw ISO.
51
+ function label(d: Date | undefined): string {
52
+ if (!d) return ''
53
+ try {
54
+ return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
55
+ } catch {
56
+ return toISO(d)
57
+ }
58
+ }
59
+
60
+ export function DynamicDateField({ field, value, onChange }: DynamicDateFieldProps) {
61
+ const [open, setOpen] = useState(false)
62
+ const selected = toDate(value)
63
+
64
+ return (
65
+ <Popover open={open} onOpenChange={setOpen}>
66
+ <PopoverTrigger asChild>
67
+ <Button
68
+ type="button"
69
+ variant="outline"
70
+ id={field.key}
71
+ data-empty={!selected}
72
+ className="w-full justify-start text-start font-normal data-[empty=true]:text-muted-foreground"
73
+ >
74
+ <CalendarIcon className="mr-2 size-4 shrink-0 opacity-50" />
75
+ <span className="truncate">
76
+ {selected ? label(selected) : (field.placeholder || 'Elegí una fecha')}
77
+ </span>
78
+ </Button>
79
+ </PopoverTrigger>
80
+ <PopoverContent className="w-auto p-0" align="start">
81
+ <Calendar
82
+ mode="single"
83
+ captionLayout="dropdown"
84
+ selected={selected}
85
+ defaultMonth={selected}
86
+ onSelect={(d: Date | undefined) => {
87
+ onChange(d ? toISO(d) : '')
88
+ setOpen(false)
89
+ }}
90
+ />
91
+ </PopoverContent>
92
+ </Popover>
93
+ )
94
+ }
95
+
96
+ export default DynamicDateField
@@ -24,10 +24,12 @@ import {
24
24
  import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
25
25
  import { DynamicLineItems } from './dynamic-line-items'
26
26
  import { DynamicSelectField } from './dynamic-select-field'
27
+ import { DynamicDateField } from './dynamic-date-field'
27
28
 
28
29
  export { buildZodSchema, resolveWidget }
29
30
  export { DynamicLineItems } from './dynamic-line-items'
30
31
  export { DynamicSelectField } from './dynamic-select-field'
32
+ export { DynamicDateField } from './dynamic-date-field'
31
33
 
32
34
  export interface DynamicFormProps {
33
35
  fields: ActionFieldDef[]
@@ -197,7 +199,7 @@ function FieldRenderer({ field, value, onChange }: FieldRendererProps) {
197
199
  case 'number':
198
200
  return <Input id={field.key} type="number" value={value ?? ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.valueAsNumber || '')} placeholder={field.placeholder} />
199
201
  case 'date':
200
- return <Input id={field.key} type="date" value={value || ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} />
202
+ return <DynamicDateField field={field} value={value} onChange={onChange} />
201
203
  default:
202
204
  return <Input id={field.key} type={field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text'} value={value || ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} placeholder={field.placeholder} />
203
205
  }