@hyddenlabs/hydn-ui 0.3.7 → 0.3.9
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/dist/components/forms/form-grid/form-grid-row.d.ts.map +1 -1
- package/dist/components/forms/form-grid/form-grid-row.js +53 -65
- package/dist/components/forms/form-grid/form-grid-row.js.map +1 -1
- package/dist/components/forms/form-grid/form-grid.d.ts +10 -2
- package/dist/components/forms/form-grid/form-grid.d.ts.map +1 -1
- package/dist/components/forms/form-grid/form-grid.js +46 -22
- package/dist/components/forms/form-grid/form-grid.js.map +1 -1
- package/dist/components/navigation/tabs/tabs.d.ts +14 -5
- package/dist/components/navigation/tabs/tabs.d.ts.map +1 -1
- package/dist/components/navigation/tabs/tabs.js +22 -17
- package/dist/components/navigation/tabs/tabs.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-grid-row.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/form-grid/form-grid-row.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAInF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,eAAe;IACf,GAAG,EAAE,eAAe,CAAC;IACrB,yBAAyB;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,QAAQ,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,IAAI,EAAE,eAAe,CAAC;IACtB,uDAAuD;IACvD,YAAY,EAAE,OAAO,CAAC;IACtB,yFAAyF;IACzF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC;IAC/C,kCAAkC;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,mCAAmC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACpE,gCAAgC;IAChC,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,kCAAkC;IAClC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,sCAAsC;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACpD,CAAC;
|
|
1
|
+
{"version":3,"file":"form-grid-row.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/form-grid/form-grid-row.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAInF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,eAAe;IACf,GAAG,EAAE,eAAe,CAAC;IACrB,yBAAyB;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,QAAQ,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,IAAI,EAAE,eAAe,CAAC;IACtB,uDAAuD;IACvD,YAAY,EAAE,OAAO,CAAC;IACtB,yFAAyF;IACzF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC;IAC/C,kCAAkC;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,mCAAmC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACpE,gCAAgC;IAChC,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,kCAAkC;IAClC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,sCAAsC;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACpD,CAAC;AAwDF;;;GAGG;AACH,iBAAS,WAAW,CAAC,EACnB,GAAG,EACH,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,cAAc,EACd,aAAa,EACb,WAAW,EACX,QAAQ,EACR,SAAS,EACT,gBAAqB,EACtB,EAAE,QAAQ,CAAC,gBAAgB,CAAC,2CAmM5B;kBAnNQ,WAAW;;;AAuNpB,eAAe,WAAW,CAAC"}
|
|
@@ -16,6 +16,9 @@ const depthIndentClasses = {
|
|
|
16
16
|
4: "pl-32",
|
|
17
17
|
5: "pl-40"
|
|
18
18
|
};
|
|
19
|
+
function rowHasContent(row) {
|
|
20
|
+
return Object.values(row.values).some((v) => v !== "" && v !== false && v !== null && v !== void 0);
|
|
21
|
+
}
|
|
19
22
|
function getCheckboxColumnWidth(label) {
|
|
20
23
|
const minCh = 8;
|
|
21
24
|
return `${Math.max(minCh, label.length + 2)}ch`;
|
|
@@ -63,9 +66,8 @@ function FormGridRow({
|
|
|
63
66
|
}) {
|
|
64
67
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
65
68
|
const desktopGridTemplate = getDesktopGridTemplate(columns);
|
|
66
|
-
const showAddSubRowButton = allowSubRows && depth < maxDepth;
|
|
67
69
|
const hasSubRows = row.subRows && row.subRows.length > 0;
|
|
68
|
-
const renderField = (column) => {
|
|
70
|
+
const renderField = (column, fieldId) => {
|
|
69
71
|
const value = row.values[column.key];
|
|
70
72
|
const validationState = validationStates[column.key] || "neutral";
|
|
71
73
|
const handleChange = (newValue) => {
|
|
@@ -76,6 +78,8 @@ function FormGridRow({
|
|
|
76
78
|
return /* @__PURE__ */ jsx(
|
|
77
79
|
Input,
|
|
78
80
|
{
|
|
81
|
+
id: fieldId,
|
|
82
|
+
ariaLabel: column.label,
|
|
79
83
|
value: value ?? "",
|
|
80
84
|
onChange: (e) => handleChange(e.target.value),
|
|
81
85
|
placeholder: column.placeholder,
|
|
@@ -90,6 +94,8 @@ function FormGridRow({
|
|
|
90
94
|
return /* @__PURE__ */ jsx(
|
|
91
95
|
Select,
|
|
92
96
|
{
|
|
97
|
+
id: fieldId,
|
|
98
|
+
ariaLabel: column.label,
|
|
93
99
|
value: value ?? "",
|
|
94
100
|
onChange: (e) => handleChange(e.target.value),
|
|
95
101
|
size,
|
|
@@ -104,6 +110,8 @@ function FormGridRow({
|
|
|
104
110
|
return /* @__PURE__ */ jsx("div", { className: `flex items-center h-full ${!isMobile ? "justify-center" : ""}`, children: /* @__PURE__ */ jsx(
|
|
105
111
|
Checkbox,
|
|
106
112
|
{
|
|
113
|
+
id: fieldId,
|
|
114
|
+
ariaLabel: column.label,
|
|
107
115
|
checked: Boolean(value),
|
|
108
116
|
onChange: (e) => handleChange(e.target.checked),
|
|
109
117
|
disabled,
|
|
@@ -114,6 +122,8 @@ function FormGridRow({
|
|
|
114
122
|
return /* @__PURE__ */ jsx(
|
|
115
123
|
Textarea,
|
|
116
124
|
{
|
|
125
|
+
id: fieldId,
|
|
126
|
+
ariaLabel: column.label,
|
|
117
127
|
value: value ?? "",
|
|
118
128
|
onChange: (e) => handleChange(e.target.value),
|
|
119
129
|
placeholder: column.placeholder,
|
|
@@ -140,32 +150,24 @@ function FormGridRow({
|
|
|
140
150
|
`,
|
|
141
151
|
style: !isMobile ? { gridTemplateColumns: desktopGridTemplate } : void 0,
|
|
142
152
|
children: [
|
|
143
|
-
columns.map((column) =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
children: [
|
|
148
|
-
isMobile && /* @__PURE__ */ jsxs("label", { className: "block text-sm font-medium text-muted-foreground mb-1", children: [
|
|
149
|
-
column.label,
|
|
150
|
-
column.required && /* @__PURE__ */ jsx("span", { className: "text-error ml-0.5", children: "*" })
|
|
151
|
-
] }),
|
|
152
|
-
renderField(column)
|
|
153
|
-
]
|
|
154
|
-
},
|
|
155
|
-
column.key
|
|
156
|
-
)),
|
|
157
|
-
/* @__PURE__ */ jsxs("div", { className: `flex items-center gap-1 ${isMobile ? "justify-end" : "w-16 shrink-0 justify-end"}`, children: [
|
|
158
|
-
showAddSubRowButton && /* @__PURE__ */ jsx(
|
|
159
|
-
IconButton,
|
|
153
|
+
columns.map((column) => {
|
|
154
|
+
const fieldId = `${row.id}-${column.key}`;
|
|
155
|
+
return /* @__PURE__ */ jsxs(
|
|
156
|
+
"div",
|
|
160
157
|
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
className: `${isMobile ? "w-full" : "min-w-0"} ${column.type === "checkbox" && !isMobile ? "flex justify-center" : ""}`,
|
|
159
|
+
children: [
|
|
160
|
+
isMobile && /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: "block text-sm font-medium text-muted-foreground mb-1", children: [
|
|
161
|
+
column.label,
|
|
162
|
+
column.required && /* @__PURE__ */ jsx("span", { className: "text-error ml-0.5", children: "*" })
|
|
163
|
+
] }),
|
|
164
|
+
renderField(column, fieldId)
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
column.key
|
|
168
|
+
);
|
|
169
|
+
}),
|
|
170
|
+
/* @__PURE__ */ jsxs("div", { className: `flex items-center gap-1 ${isMobile ? "justify-end" : "w-16 shrink-0 justify-end"}`, children: [
|
|
169
171
|
canRemove && !isMobile && /* @__PURE__ */ jsx(
|
|
170
172
|
IconButton,
|
|
171
173
|
{
|
|
@@ -173,9 +175,10 @@ function FormGridRow({
|
|
|
173
175
|
ariaLabel: "Remove row",
|
|
174
176
|
onClick: () => onRemove(row.id),
|
|
175
177
|
disabled,
|
|
176
|
-
|
|
178
|
+
iconSize: "sm",
|
|
179
|
+
hoverIcon: "IconTrashFilled",
|
|
180
|
+
noPadding: true,
|
|
177
181
|
buttonStyle: "link",
|
|
178
|
-
className: "p-0!",
|
|
179
182
|
variant: "error"
|
|
180
183
|
}
|
|
181
184
|
),
|
|
@@ -211,42 +214,27 @@ function FormGridRow({
|
|
|
211
214
|
]
|
|
212
215
|
}
|
|
213
216
|
),
|
|
214
|
-
!isCollapsed && /* @__PURE__ */
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
)),
|
|
236
|
-
depth + 1 < maxDepth && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(
|
|
237
|
-
ButtonWithIcon,
|
|
238
|
-
{
|
|
239
|
-
icon: "plus",
|
|
240
|
-
buttonStyle: "link",
|
|
241
|
-
ariaLabel: "Add Property",
|
|
242
|
-
onClick: () => onAddSubRow(row.id),
|
|
243
|
-
disabled,
|
|
244
|
-
size,
|
|
245
|
-
className: "pl-0!",
|
|
246
|
-
children: "Add Property"
|
|
247
|
-
}
|
|
248
|
-
) })
|
|
249
|
-
] })
|
|
217
|
+
!isCollapsed && /* @__PURE__ */ jsx("div", { className: "space-y-1", children: row.subRows.map((subRow) => /* @__PURE__ */ jsx(
|
|
218
|
+
FormGridRow,
|
|
219
|
+
{
|
|
220
|
+
row: subRow,
|
|
221
|
+
columns,
|
|
222
|
+
depth: depth + 1,
|
|
223
|
+
maxDepth,
|
|
224
|
+
isMobile,
|
|
225
|
+
size,
|
|
226
|
+
allowSubRows,
|
|
227
|
+
autoSubRow,
|
|
228
|
+
disabled,
|
|
229
|
+
addSubRowLabel,
|
|
230
|
+
onValueChange,
|
|
231
|
+
onAddSubRow,
|
|
232
|
+
onRemove,
|
|
233
|
+
canRemove: rowHasContent(subRow),
|
|
234
|
+
validationStates
|
|
235
|
+
},
|
|
236
|
+
subRow.id
|
|
237
|
+
)) })
|
|
250
238
|
] })
|
|
251
239
|
] });
|
|
252
240
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-grid-row.js","sources":["../../../../src/components/forms/form-grid/form-grid-row.tsx"],"sourcesContent":["import { useState } from 'react';\nimport Input from '../input/input';\nimport Select from '../select/select';\nimport SelectItem from '../select/select-item';\nimport Checkbox from '../checkbox/checkbox';\nimport Textarea from '../textarea/textarea';\nimport IconButton from '../button/icon-button';\nimport type { FormGridColumn, FormGridRowData } from './form-grid';\nimport type { InteractiveSize, ValidationState } from '../../../theme/size-tokens';\nimport { ButtonWithIcon } from '../button';\nimport { IconChevronDown, IconChevronRight } from '@tabler/icons-react';\n\nexport type FormGridRowProps = {\n /** Row data */\n row: FormGridRowData;\n /** Column definitions */\n columns: FormGridColumn[];\n /** Current nesting depth (0 = root) */\n depth: number;\n /** Maximum allowed nesting depth */\n maxDepth: number;\n /** Whether in mobile layout mode */\n isMobile: boolean;\n /** Size variant for form controls */\n size: InteractiveSize;\n /** Whether sub-rows are allowed (manual add button) */\n allowSubRows: boolean;\n /** Auto sub-row callback (when provided, sub-rows are auto-managed, no manual button) */\n autoSubRow?: (row: FormGridRowData) => boolean;\n /** Whether the row is disabled */\n disabled: boolean;\n /** Label for add sub-row button */\n addSubRowLabel: string;\n /** Callback when a field value changes */\n onValueChange: (rowId: string, key: string, value: unknown) => void;\n /** Callback to add a sub-row */\n onAddSubRow: (parentId: string) => void;\n /** Callback to remove this row */\n onRemove: (id: string) => void;\n /** Whether this row can be removed */\n canRemove: boolean;\n /** Validation states for fields */\n validationStates?: Record<string, ValidationState>;\n};\n\n// Depth indent classes (using pl-* for left padding)\nconst depthIndentClasses: Record<number, string> = {\n 0: '',\n 1: 'pl-8',\n 2: 'pl-16',\n 3: 'pl-24',\n 4: 'pl-32',\n 5: 'pl-40'\n};\n\nfunction getCheckboxColumnWidth(label: string): string {\n const minCh = 8;\n return `${Math.max(minCh, label.length + 2)}ch`;\n}\n\n/**\n * Convert a Tailwind width utility class to a valid CSS grid track value.\n * e.g. 'w-24' → '6rem', 'flex-1' → 'minmax(0, 1fr)', 'w-1/2' → '50%'\n */\nfunction tailwindWidthToCss(width: string): string {\n if (width === 'flex-1' || width === 'w-full') return 'minmax(0, 1fr)';\n if (width === 'w-auto') return 'auto';\n // w-{number} → n * 0.25rem\n const numMatch = width.match(/^w-(\\d+)$/);\n if (numMatch) return `${parseInt(numMatch[1]) * 0.25}rem`;\n // w-{num}/{den} fraction\n const fracMatch = width.match(/^w-(\\d+)\\/(\\d+)$/);\n if (fracMatch) return `${((parseInt(fracMatch[1]) / parseInt(fracMatch[2])) * 100).toFixed(2)}%`;\n // Already a valid CSS value (contains a unit or keyword)\n if (/\\d+(px|rem|em|%|fr|ch|vw|vh)/.test(width) || /^(auto|min-content|max-content|fit-content)$/.test(width)) {\n return width;\n }\n return 'auto';\n}\n\nfunction getDesktopGridTemplate(columns: FormGridColumn[]): string {\n const columnTracks = columns.map((column) => {\n if (column.width) {\n return tailwindWidthToCss(column.width);\n }\n if (column.type === 'checkbox') {\n return getCheckboxColumnWidth(column.label);\n }\n return 'minmax(0, 1fr)';\n });\n\n return `${columnTracks.join(' ')} 4rem`;\n}\n\n/**\n * FormGridRow - Recursive row component for FormGrid\n * Handles rendering of field controls and nested sub-rows\n */\nfunction FormGridRow({\n row,\n columns,\n depth,\n maxDepth,\n isMobile,\n size,\n allowSubRows,\n autoSubRow,\n disabled,\n addSubRowLabel,\n onValueChange,\n onAddSubRow,\n onRemove,\n canRemove,\n validationStates = {}\n}: Readonly<FormGridRowProps>) {\n const [isCollapsed, setIsCollapsed] = useState(false);\n const desktopGridTemplate = getDesktopGridTemplate(columns);\n // Show manual add button only if allowSubRows is true (which is false when autoSubRow is provided)\n const showAddSubRowButton = allowSubRows && depth < maxDepth;\n const hasSubRows = row.subRows && row.subRows.length > 0;\n\n // Render a single field based on column type\n const renderField = (column: FormGridColumn) => {\n const value = row.values[column.key];\n const validationState = validationStates[column.key] || 'neutral';\n\n const handleChange = (newValue: unknown) => {\n onValueChange(row.id, column.key, newValue);\n };\n\n switch (column.type) {\n case 'input':\n return (\n <Input\n value={(value as string) ?? ''}\n onChange={(e) => handleChange(e.target.value)}\n placeholder={column.placeholder}\n type={column.inputType || 'text'}\n size={size}\n disabled={disabled}\n required={column.required}\n validationState={validationState}\n />\n );\n\n case 'select':\n return (\n <Select\n value={(value as string) ?? ''}\n onChange={(e) => handleChange(e.target.value)}\n size={size}\n disabled={disabled}\n required={column.required}\n validationState={validationState}\n label={column.placeholder}\n >\n {column.options?.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {opt.label}\n </SelectItem>\n ))}\n </Select>\n );\n\n case 'checkbox':\n return (\n <div className={`flex items-center h-full ${!isMobile ? 'justify-center' : ''}`}>\n <Checkbox\n checked={Boolean(value)}\n onChange={(e) => handleChange(e.target.checked)}\n disabled={disabled}\n validationState={validationState}\n />\n </div>\n );\n\n case 'textarea':\n return (\n <Textarea\n value={(value as string) ?? ''}\n onChange={(e) => handleChange(e.target.value)}\n placeholder={column.placeholder}\n disabled={disabled}\n required={column.required}\n validationState={validationState}\n />\n );\n\n case 'custom':\n if (column.render) {\n return column.render(value, row, handleChange, 0);\n }\n return null;\n\n default:\n return null;\n }\n };\n\n return (\n <div>\n {/* Row content */}\n <div\n className={`\n ${isMobile ? 'flex flex-col gap-3 border border-border rounded-lg bg-muted/20 px-3 py-3' : 'grid items-center gap-3 py-1'}\n `}\n style={!isMobile ? { gridTemplateColumns: desktopGridTemplate } : undefined}\n >\n {/* Fields */}\n {columns.map((column) => (\n <div\n key={column.key}\n className={`${isMobile ? 'w-full' : 'min-w-0'} ${column.type === 'checkbox' && !isMobile ? 'flex justify-center' : ''}`}\n >\n {/* Mobile: show label above field */}\n {isMobile && (\n <label className=\"block text-sm font-medium text-muted-foreground mb-1\">\n {column.label}\n {column.required && <span className=\"text-error ml-0.5\">*</span>}\n </label>\n )}\n {renderField(column)}\n </div>\n ))}\n\n {/* Action buttons */}\n <div className={`flex items-center gap-1 ${isMobile ? 'justify-end' : 'w-16 shrink-0 justify-end'}`}>\n {showAddSubRowButton && (\n <IconButton\n icon=\"hierarchy-2\"\n ariaLabel={addSubRowLabel}\n onClick={() => onAddSubRow(row.id)}\n disabled={disabled}\n size=\"sm\"\n buttonStyle=\"ghost\"\n />\n )}\n {canRemove && !isMobile && (\n <IconButton\n icon=\"trash\"\n ariaLabel=\"Remove row\"\n onClick={() => onRemove(row.id)}\n disabled={disabled}\n size=\"sm\"\n buttonStyle=\"link\"\n className=\"p-0!\"\n variant=\"error\"\n />\n )}\n {canRemove && isMobile && (\n <ButtonWithIcon\n icon=\"trash\"\n ariaLabel=\"Remove row\"\n onClick={() => onRemove(row.id)}\n disabled={disabled}\n size={size}\n buttonStyle=\"link\"\n variant=\"error\"\n iconPosition=\"right\"\n >\n Delete Field\n </ButtonWithIcon>\n )}\n </div>\n </div>\n\n {/* Sub-rows section with collapse toggle */}\n {hasSubRows && (\n <div className={`mt-1 ${depthIndentClasses[Math.min(depth + 1, 5)] || ''}`}>\n {/* Collapse toggle at the start of sub-rows */}\n <button\n type=\"button\"\n onClick={() => setIsCollapsed(!isCollapsed)}\n className=\"cursor-pointer flex items-center gap-1 mb-1 text-sm text-muted-foreground hover:text-foreground transition-colors\"\n aria-label={isCollapsed ? 'Expand sub-rows' : 'Collapse sub-rows'}\n >\n {isCollapsed ? <IconChevronRight size={14} /> : <IconChevronDown size={14} />}\n <span>{isCollapsed ? 'Show properties' : 'Hide properties'}</span>\n </button>\n\n {/* Sub-rows (recursive) - collapsible */}\n {!isCollapsed && (\n <div className=\"space-y-1\">\n {row.subRows!.map((subRow, index) => (\n <FormGridRow\n key={subRow.id}\n row={subRow}\n columns={columns}\n depth={depth + 1}\n maxDepth={maxDepth}\n isMobile={isMobile}\n size={size}\n allowSubRows={allowSubRows}\n autoSubRow={autoSubRow}\n disabled={disabled}\n addSubRowLabel={addSubRowLabel}\n onValueChange={onValueChange}\n onAddSubRow={onAddSubRow}\n onRemove={onRemove}\n canRemove={autoSubRow ? index > 0 : true}\n validationStates={validationStates}\n />\n ))}\n {/* Add field button within sub-row section */}\n {depth + 1 < maxDepth && (\n <div className=\"flex items-center gap-2\">\n <ButtonWithIcon\n icon=\"plus\"\n buttonStyle=\"link\"\n ariaLabel=\"Add Property\"\n onClick={() => onAddSubRow(row.id)}\n disabled={disabled}\n size={size}\n className=\"pl-0!\"\n >\n Add Property\n </ButtonWithIcon>\n </div>\n )}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n\nFormGridRow.displayName = 'FormGridRow';\n\nexport default FormGridRow;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"form-grid-row.js","sources":["../../../../src/components/forms/form-grid/form-grid-row.tsx"],"sourcesContent":["import { useState } from 'react';\nimport Input from '../input/input';\nimport Select from '../select/select';\nimport SelectItem from '../select/select-item';\nimport Checkbox from '../checkbox/checkbox';\nimport Textarea from '../textarea/textarea';\nimport IconButton from '../button/icon-button';\nimport type { FormGridColumn, FormGridRowData } from './form-grid';\nimport type { InteractiveSize, ValidationState } from '../../../theme/size-tokens';\nimport { ButtonWithIcon } from '../button';\nimport { IconChevronDown, IconChevronRight } from '@tabler/icons-react';\n\nexport type FormGridRowProps = {\n /** Row data */\n row: FormGridRowData;\n /** Column definitions */\n columns: FormGridColumn[];\n /** Current nesting depth (0 = root) */\n depth: number;\n /** Maximum allowed nesting depth */\n maxDepth: number;\n /** Whether in mobile layout mode */\n isMobile: boolean;\n /** Size variant for form controls */\n size: InteractiveSize;\n /** Whether sub-rows are allowed (manual add button) */\n allowSubRows: boolean;\n /** Auto sub-row callback (when provided, sub-rows are auto-managed, no manual button) */\n autoSubRow?: (row: FormGridRowData) => boolean;\n /** Whether the row is disabled */\n disabled: boolean;\n /** Label for add sub-row button */\n addSubRowLabel: string;\n /** Callback when a field value changes */\n onValueChange: (rowId: string, key: string, value: unknown) => void;\n /** Callback to add a sub-row */\n onAddSubRow: (parentId: string) => void;\n /** Callback to remove this row */\n onRemove: (id: string) => void;\n /** Whether this row can be removed */\n canRemove: boolean;\n /** Validation states for fields */\n validationStates?: Record<string, ValidationState>;\n};\n\n// Depth indent classes (using pl-* for left padding)\nconst depthIndentClasses: Record<number, string> = {\n 0: '',\n 1: 'pl-8',\n 2: 'pl-16',\n 3: 'pl-24',\n 4: 'pl-32',\n 5: 'pl-40'\n};\n\n/** Check whether a row has any user-entered content (non-empty, non-default values) */\nfunction rowHasContent(row: FormGridRowData): boolean {\n return Object.values(row.values).some((v) => v !== '' && v !== false && v !== null && v !== undefined);\n}\n\nfunction getCheckboxColumnWidth(label: string): string {\n const minCh = 8;\n return `${Math.max(minCh, label.length + 2)}ch`;\n}\n\n/**\n * Convert a Tailwind width utility class to a valid CSS grid track value.\n * e.g. 'w-24' → '6rem', 'flex-1' → 'minmax(0, 1fr)', 'w-1/2' → '50%'\n */\nfunction tailwindWidthToCss(width: string): string {\n if (width === 'flex-1' || width === 'w-full') return 'minmax(0, 1fr)';\n if (width === 'w-auto') return 'auto';\n // w-{number} → n * 0.25rem\n const numMatch = width.match(/^w-(\\d+)$/);\n if (numMatch) return `${parseInt(numMatch[1]) * 0.25}rem`;\n // w-{num}/{den} fraction\n const fracMatch = width.match(/^w-(\\d+)\\/(\\d+)$/);\n if (fracMatch) return `${((parseInt(fracMatch[1]) / parseInt(fracMatch[2])) * 100).toFixed(2)}%`;\n // Already a valid CSS value (contains a unit or keyword)\n if (/\\d+(px|rem|em|%|fr|ch|vw|vh)/.test(width) || /^(auto|min-content|max-content|fit-content)$/.test(width)) {\n return width;\n }\n return 'auto';\n}\n\nfunction getDesktopGridTemplate(columns: FormGridColumn[]): string {\n const columnTracks = columns.map((column) => {\n if (column.width) {\n return tailwindWidthToCss(column.width);\n }\n if (column.type === 'checkbox') {\n return getCheckboxColumnWidth(column.label);\n }\n return 'minmax(0, 1fr)';\n });\n\n return `${columnTracks.join(' ')} 4rem`;\n}\n\n/**\n * FormGridRow - Recursive row component for FormGrid\n * Handles rendering of field controls and nested sub-rows\n */\nfunction FormGridRow({\n row,\n columns,\n depth,\n maxDepth,\n isMobile,\n size,\n allowSubRows,\n autoSubRow,\n disabled,\n addSubRowLabel,\n onValueChange,\n onAddSubRow,\n onRemove,\n canRemove,\n validationStates = {}\n}: Readonly<FormGridRowProps>) {\n const [isCollapsed, setIsCollapsed] = useState(false);\n const desktopGridTemplate = getDesktopGridTemplate(columns);\n const hasSubRows = row.subRows && row.subRows.length > 0;\n\n // Render a single field based on column type\n const renderField = (column: FormGridColumn, fieldId: string) => {\n const value = row.values[column.key];\n const validationState = validationStates[column.key] || 'neutral';\n\n const handleChange = (newValue: unknown) => {\n onValueChange(row.id, column.key, newValue);\n };\n\n switch (column.type) {\n case 'input':\n return (\n <Input\n id={fieldId}\n ariaLabel={column.label}\n value={(value as string) ?? ''}\n onChange={(e) => handleChange(e.target.value)}\n placeholder={column.placeholder}\n type={column.inputType || 'text'}\n size={size}\n disabled={disabled}\n required={column.required}\n validationState={validationState}\n />\n );\n\n case 'select':\n return (\n <Select\n id={fieldId}\n ariaLabel={column.label}\n value={(value as string) ?? ''}\n onChange={(e) => handleChange(e.target.value)}\n size={size}\n disabled={disabled}\n required={column.required}\n validationState={validationState}\n label={column.placeholder}\n >\n {column.options?.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {opt.label}\n </SelectItem>\n ))}\n </Select>\n );\n\n case 'checkbox':\n return (\n <div className={`flex items-center h-full ${!isMobile ? 'justify-center' : ''}`}>\n <Checkbox\n id={fieldId}\n ariaLabel={column.label}\n checked={Boolean(value)}\n onChange={(e) => handleChange(e.target.checked)}\n disabled={disabled}\n validationState={validationState}\n />\n </div>\n );\n\n case 'textarea':\n return (\n <Textarea\n id={fieldId}\n ariaLabel={column.label}\n value={(value as string) ?? ''}\n onChange={(e) => handleChange(e.target.value)}\n placeholder={column.placeholder}\n disabled={disabled}\n required={column.required}\n validationState={validationState}\n />\n );\n\n case 'custom':\n if (column.render) {\n return column.render(value, row, handleChange, 0);\n }\n return null;\n\n default:\n return null;\n }\n };\n\n return (\n <div>\n {/* Row content */}\n <div\n className={`\n ${isMobile ? 'flex flex-col gap-3 border border-border rounded-lg bg-muted/20 px-3 py-3' : 'grid items-center gap-3 py-1'}\n `}\n style={!isMobile ? { gridTemplateColumns: desktopGridTemplate } : undefined}\n >\n {/* Fields */}\n {columns.map((column) => {\n const fieldId = `${row.id}-${column.key}`;\n return (\n <div\n key={column.key}\n className={`${isMobile ? 'w-full' : 'min-w-0'} ${column.type === 'checkbox' && !isMobile ? 'flex justify-center' : ''}`}\n >\n {/* Mobile: show label above field */}\n {isMobile && (\n <label htmlFor={fieldId} className=\"block text-sm font-medium text-muted-foreground mb-1\">\n {column.label}\n {column.required && <span className=\"text-error ml-0.5\">*</span>}\n </label>\n )}\n {renderField(column, fieldId)}\n </div>\n );\n })}\n\n {/* Action buttons */}\n <div className={`flex items-center gap-1 ${isMobile ? 'justify-end' : 'w-16 shrink-0 justify-end'}`}>\n {canRemove && !isMobile && (\n <IconButton\n icon=\"trash\"\n ariaLabel=\"Remove row\"\n onClick={() => onRemove(row.id)}\n disabled={disabled}\n iconSize=\"sm\"\n hoverIcon=\"IconTrashFilled\"\n noPadding\n buttonStyle=\"link\"\n variant=\"error\"\n />\n )}\n {canRemove && isMobile && (\n <ButtonWithIcon\n icon=\"trash\"\n ariaLabel=\"Remove row\"\n onClick={() => onRemove(row.id)}\n disabled={disabled}\n size={size}\n buttonStyle=\"link\"\n variant=\"error\"\n iconPosition=\"right\"\n >\n Delete Field\n </ButtonWithIcon>\n )}\n </div>\n </div>\n\n {/* Sub-rows section with collapse toggle */}\n {hasSubRows && (\n <div className={`mt-1 ${depthIndentClasses[Math.min(depth + 1, 5)] || ''}`}>\n {/* Collapse toggle at the start of sub-rows */}\n <button\n type=\"button\"\n onClick={() => setIsCollapsed(!isCollapsed)}\n className=\"cursor-pointer flex items-center gap-1 mb-1 text-sm text-muted-foreground hover:text-foreground transition-colors\"\n aria-label={isCollapsed ? 'Expand sub-rows' : 'Collapse sub-rows'}\n >\n {isCollapsed ? <IconChevronRight size={14} /> : <IconChevronDown size={14} />}\n <span>{isCollapsed ? 'Show properties' : 'Hide properties'}</span>\n </button>\n\n {/* Sub-rows (recursive) - collapsible */}\n {!isCollapsed && (\n <div className=\"space-y-1\">\n {row.subRows!.map((subRow) => (\n <FormGridRow\n key={subRow.id}\n row={subRow}\n columns={columns}\n depth={depth + 1}\n maxDepth={maxDepth}\n isMobile={isMobile}\n size={size}\n allowSubRows={allowSubRows}\n autoSubRow={autoSubRow}\n disabled={disabled}\n addSubRowLabel={addSubRowLabel}\n onValueChange={onValueChange}\n onAddSubRow={onAddSubRow}\n onRemove={onRemove}\n canRemove={rowHasContent(subRow)}\n validationStates={validationStates}\n />\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n\nFormGridRow.displayName = 'FormGridRow';\n\nexport default FormGridRow;\n"],"names":[],"mappings":"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"}
|
|
@@ -68,8 +68,6 @@ export type FormGridProps = {
|
|
|
68
68
|
size?: InteractiveSize;
|
|
69
69
|
/** Maximum nesting depth for sub-rows (default: 3) */
|
|
70
70
|
maxDepth?: number;
|
|
71
|
-
/** Label for add row button */
|
|
72
|
-
addRowLabel?: string;
|
|
73
71
|
/** Label for add sub-row button */
|
|
74
72
|
addSubRowLabel?: string;
|
|
75
73
|
/** Whether to allow sub-rows (manual add button) */
|
|
@@ -84,7 +82,17 @@ export type FormGridProps = {
|
|
|
84
82
|
className?: string;
|
|
85
83
|
/** Validation state map by row id and column key */
|
|
86
84
|
validationStates?: Record<string, Record<string, ValidationState>>;
|
|
85
|
+
/** Label displayed above the grid */
|
|
86
|
+
label?: string;
|
|
87
|
+
/** Helper text displayed below the grid */
|
|
88
|
+
helperText?: string;
|
|
89
|
+
/** Error message displayed below the grid (overrides helperText) */
|
|
90
|
+
error?: string;
|
|
91
|
+
/** Shows required asterisk next to label */
|
|
92
|
+
required?: boolean;
|
|
87
93
|
};
|
|
94
|
+
/** Check whether a row has any user-entered content (non-empty, non-default values) */
|
|
95
|
+
export declare function rowHasContent(row: FormGridRowData): boolean;
|
|
88
96
|
/**
|
|
89
97
|
* FormGrid - Dynamic form builder with support for adding/removing rows and nested sub-rows.
|
|
90
98
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-grid.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/form-grid/form-grid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAiE,SAAS,EAAE,MAAM,OAAO,CAAC;AAExG,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"form-grid.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/form-grid/form-grid.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import React, { useState, useRef, useEffect, useCallback, useImperativeHandle } from "react";
|
|
3
3
|
import FormGridRow from "./form-grid-row.js";
|
|
4
|
-
import
|
|
4
|
+
import { validationTextClasses } from "../../../theme/size-tokens.js";
|
|
5
|
+
import Text from "../../typography/text/text.js";
|
|
5
6
|
const MOBILE_BREAKPOINT = 640;
|
|
6
7
|
function getCheckboxColumnWidth(label) {
|
|
7
8
|
const minCh = 8;
|
|
@@ -91,6 +92,30 @@ function validateRows(rows, columns) {
|
|
|
91
92
|
};
|
|
92
93
|
return rows.every(checkRow);
|
|
93
94
|
}
|
|
95
|
+
function rowHasContent(row) {
|
|
96
|
+
return Object.values(row.values).some((v) => v !== "" && v !== false && v !== null && v !== void 0);
|
|
97
|
+
}
|
|
98
|
+
function ensureTrailingEmptyRows(rows, changedRowId, columns) {
|
|
99
|
+
const rootIdx = rows.findIndex((r) => r.id === changedRowId);
|
|
100
|
+
if (rootIdx === rows.length - 1) {
|
|
101
|
+
if (rowHasContent(rows[rootIdx])) {
|
|
102
|
+
return [...rows, createEmptyRow(columns)];
|
|
103
|
+
}
|
|
104
|
+
return rows;
|
|
105
|
+
}
|
|
106
|
+
return rows.map((row) => {
|
|
107
|
+
if (!row.subRows || row.subRows.length === 0) return row;
|
|
108
|
+
const subIdx = row.subRows.findIndex((r) => r.id === changedRowId);
|
|
109
|
+
if (subIdx === row.subRows.length - 1) {
|
|
110
|
+
if (rowHasContent(row.subRows[subIdx])) {
|
|
111
|
+
return { ...row, subRows: [...row.subRows, createEmptyRow(columns)] };
|
|
112
|
+
}
|
|
113
|
+
return row;
|
|
114
|
+
}
|
|
115
|
+
const newSubRows = ensureTrailingEmptyRows(row.subRows, changedRowId, columns);
|
|
116
|
+
return newSubRows !== row.subRows ? { ...row, subRows: newSubRows } : row;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
94
119
|
function getRowDepth(rows, targetId, currentDepth = 0) {
|
|
95
120
|
for (const row of rows) {
|
|
96
121
|
if (row.id === targetId) {
|
|
@@ -112,14 +137,17 @@ const FormGrid = React.forwardRef(
|
|
|
112
137
|
onChange,
|
|
113
138
|
size = "md",
|
|
114
139
|
maxDepth = 3,
|
|
115
|
-
addRowLabel = "Add Row",
|
|
116
140
|
addSubRowLabel = "Add Sub-Row",
|
|
117
141
|
allowSubRows = true,
|
|
118
142
|
autoSubRow,
|
|
119
143
|
disabled = false,
|
|
120
144
|
showHeaders = true,
|
|
121
145
|
className = "",
|
|
122
|
-
validationStates = {}
|
|
146
|
+
validationStates = {},
|
|
147
|
+
label,
|
|
148
|
+
helperText,
|
|
149
|
+
error,
|
|
150
|
+
required = false
|
|
123
151
|
}, ref) => {
|
|
124
152
|
const [rows, setRows] = useState(() => {
|
|
125
153
|
if (defaultRows && defaultRows.length > 0) {
|
|
@@ -184,6 +212,7 @@ const FormGrid = React.forwardRef(
|
|
|
184
212
|
return row;
|
|
185
213
|
});
|
|
186
214
|
}
|
|
215
|
+
updated = ensureTrailingEmptyRows(updated, rowId, columns);
|
|
187
216
|
return updated;
|
|
188
217
|
});
|
|
189
218
|
},
|
|
@@ -207,11 +236,17 @@ const FormGrid = React.forwardRef(
|
|
|
207
236
|
[rows, columns, handleAddRow, handleAddSubRow, handleRemoveRow]
|
|
208
237
|
);
|
|
209
238
|
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `w-full ${className}`, children: [
|
|
210
|
-
|
|
239
|
+
label && /* @__PURE__ */ jsxs("label", { className: "block text-sm font-semibold text-foreground mb-1.5", children: [
|
|
240
|
+
label,
|
|
241
|
+
required && /* @__PURE__ */ jsx("span", { className: "text-error ml-1", children: "*" })
|
|
242
|
+
] }),
|
|
243
|
+
showHeaders && !isMobile && /* @__PURE__ */ jsxs("div", { className: "grid items-center gap-3 mt-3 mb-1", style: { gridTemplateColumns: desktopGridTemplate }, children: [
|
|
211
244
|
columns.map((col) => /* @__PURE__ */ jsxs(
|
|
212
|
-
|
|
245
|
+
Text,
|
|
213
246
|
{
|
|
214
|
-
|
|
247
|
+
size: "sm",
|
|
248
|
+
align: col.type === "checkbox" ? "center" : "left",
|
|
249
|
+
className: "text-muted-foreground min-w-0",
|
|
215
250
|
children: [
|
|
216
251
|
col.label,
|
|
217
252
|
col.required && /* @__PURE__ */ jsx("span", { className: "text-error ml-0.5", children: "*" })
|
|
@@ -221,7 +256,7 @@ const FormGrid = React.forwardRef(
|
|
|
221
256
|
)),
|
|
222
257
|
/* @__PURE__ */ jsx("div", { className: "w-16 shrink-0" })
|
|
223
258
|
] }),
|
|
224
|
-
/* @__PURE__ */ jsx("div", { className: "space-y-
|
|
259
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1", children: rows.map((row) => /* @__PURE__ */ jsx(
|
|
225
260
|
FormGridRow,
|
|
226
261
|
{
|
|
227
262
|
row,
|
|
@@ -237,29 +272,18 @@ const FormGrid = React.forwardRef(
|
|
|
237
272
|
onValueChange: handleValueChange,
|
|
238
273
|
onAddSubRow: handleAddSubRow,
|
|
239
274
|
onRemove: handleRemoveRow,
|
|
240
|
-
canRemove:
|
|
275
|
+
canRemove: rowHasContent(row),
|
|
241
276
|
validationStates: validationStates[row.id]
|
|
242
277
|
},
|
|
243
278
|
row.id
|
|
244
279
|
)) }),
|
|
245
|
-
/* @__PURE__ */ jsx(
|
|
246
|
-
ButtonWithIcon,
|
|
247
|
-
{
|
|
248
|
-
icon: "plus",
|
|
249
|
-
ariaLabel: addRowLabel,
|
|
250
|
-
onClick: handleAddRow,
|
|
251
|
-
disabled,
|
|
252
|
-
size,
|
|
253
|
-
buttonStyle: "link",
|
|
254
|
-
className: "pl-0!",
|
|
255
|
-
children: addRowLabel
|
|
256
|
-
}
|
|
257
|
-
) })
|
|
280
|
+
(helperText || error) && /* @__PURE__ */ jsx(Text, { size: "xs", className: `mt-1.5 ${error ? validationTextClasses.error : "text-muted-foreground"}`, children: error || helperText })
|
|
258
281
|
] });
|
|
259
282
|
}
|
|
260
283
|
);
|
|
261
284
|
FormGrid.displayName = "FormGrid";
|
|
262
285
|
export {
|
|
263
|
-
FormGrid as default
|
|
286
|
+
FormGrid as default,
|
|
287
|
+
rowHasContent
|
|
264
288
|
};
|
|
265
289
|
//# sourceMappingURL=form-grid.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-grid.js","sources":["../../../../src/components/forms/form-grid/form-grid.tsx"],"sourcesContent":["import React, { useState, useImperativeHandle, useEffect, useRef, useCallback, ReactNode } from 'react';\nimport FormGridRow from './form-grid-row';\nimport type { InteractiveSize, ValidationState } from '../../../theme/size-tokens';\nimport { ButtonWithIcon } from '../..';\n\n// =============================================================================\n// TYPES\n// =============================================================================\n\n/**\n * Column definition for FormGrid fields\n */\nexport type FormGridColumn = {\n /** Unique key identifying this column in row data */\n key: string;\n /** Display label for the column header */\n label: string;\n /** Type of form control to render */\n type: 'input' | 'select' | 'checkbox' | 'textarea' | 'custom';\n /** HTML input type (only for type='input') */\n inputType?: 'text' | 'email' | 'number' | 'tel' | 'url' | 'password';\n /** Placeholder text for input/textarea/select */\n placeholder?: string;\n /** Options for select type */\n options?: { value: string; label: string }[];\n /** Custom render function for type='custom' */\n render?: (value: unknown, row: FormGridRowData, onChange: (value: unknown) => void, rowIndex: number) => ReactNode;\n /** Width of the field (Tailwind width class or 'flex-1') */\n width?: string;\n /** Whether this field is required */\n required?: boolean;\n /** Default value for new rows */\n defaultValue?: unknown;\n};\n\n/**\n * Row data structure with recursive sub-rows\n */\nexport type FormGridRowData = {\n /** Unique identifier for the row */\n id: string;\n /** Key-value map of field values */\n values: Record<string, unknown>;\n /** Nested sub-rows */\n subRows?: FormGridRowData[];\n};\n\n/**\n * Methods exposed via ref for uncontrolled usage\n */\nexport type FormGridRef = {\n /** Get all row data including sub-rows */\n getData: () => FormGridRowData[];\n /** Add a new row at root level or as sub-row of parentId */\n addRow: (parentId?: string) => void;\n /** Remove a row by id (searches recursively) */\n removeRow: (id: string) => void;\n /** Validate all rows, returns true if valid */\n validate: () => boolean;\n /** Reset to initial state with one empty row */\n reset: () => void;\n};\n\n/**\n * Props for the FormGrid component\n */\nexport type FormGridProps = {\n /** Column definitions describing each field */\n columns: FormGridColumn[];\n /** Initial rows data (defaults to one empty row) */\n defaultRows?: FormGridRowData[];\n /** Callback when data changes */\n onChange?: (rows: FormGridRowData[]) => void;\n /** Size variant for form controls */\n size?: InteractiveSize;\n /** Maximum nesting depth for sub-rows (default: 3) */\n maxDepth?: number;\n /** Label for add row button */\n addRowLabel?: string;\n /** Label for add sub-row button */\n addSubRowLabel?: string;\n /** Whether to allow sub-rows (manual add button) */\n allowSubRows?: boolean;\n /** Callback to determine if a row should automatically have sub-rows. When provided, sub-rows are auto-managed based on row data (e.g., type === 'object'). No manual add button is shown. */\n autoSubRow?: (row: FormGridRowData) => boolean;\n /** Whether the entire grid is disabled */\n disabled?: boolean;\n /** Whether to show column headers */\n showHeaders?: boolean;\n /** Custom class name */\n className?: string;\n /** Validation state map by row id and column key */\n validationStates?: Record<string, Record<string, ValidationState>>;\n};\n\n// =============================================================================\n// HELPERS\n// =============================================================================\n\nconst MOBILE_BREAKPOINT = 640;\n\nfunction getCheckboxColumnWidth(label: string): string {\n const minCh = 8;\n return `${Math.max(minCh, label.length + 2)}ch`;\n}\n\n/**\n * Convert a Tailwind width utility class to a valid CSS grid track value.\n * e.g. 'w-24' → '6rem', 'flex-1' → 'minmax(0, 1fr)', 'w-1/2' → '50%'\n */\nfunction tailwindWidthToCss(width: string): string {\n if (width === 'flex-1' || width === 'w-full') return 'minmax(0, 1fr)';\n if (width === 'w-auto') return 'auto';\n // w-{number} → n * 0.25rem\n const numMatch = width.match(/^w-(\\d+)$/);\n if (numMatch) return `${parseInt(numMatch[1]) * 0.25}rem`;\n // w-{num}/{den} fraction\n const fracMatch = width.match(/^w-(\\d+)\\/(\\d+)$/);\n if (fracMatch) return `${((parseInt(fracMatch[1]) / parseInt(fracMatch[2])) * 100).toFixed(2)}%`;\n // Already a valid CSS value (contains a unit or keyword)\n if (/\\d+(px|rem|em|%|fr|ch|vw|vh)/.test(width) || /^(auto|min-content|max-content|fit-content)$/.test(width)) {\n return width;\n }\n return 'auto';\n}\n\nfunction getDesktopGridTemplate(columns: FormGridColumn[]): string {\n const columnTracks = columns.map((col) => {\n if (col.width) {\n return tailwindWidthToCss(col.width);\n }\n if (col.type === 'checkbox') {\n return getCheckboxColumnWidth(col.label);\n }\n return 'minmax(0, 1fr)';\n });\n\n return `${columnTracks.join(' ')} 4rem`;\n}\n\n/** Generate a unique ID for rows */\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\n/** Create an empty row with default values from columns */\nfunction createEmptyRow(columns: FormGridColumn[]): FormGridRowData {\n const values: Record<string, unknown> = {};\n columns.forEach((col) => {\n if (col.defaultValue !== undefined) {\n values[col.key] = col.defaultValue;\n } else if (col.type === 'checkbox') {\n values[col.key] = false;\n } else {\n values[col.key] = '';\n }\n });\n return { id: generateId(), values };\n}\n\n/** Recursively find and update a row */\nfunction updateRowInTree(\n rows: FormGridRowData[],\n targetId: string,\n updater: (row: FormGridRowData) => FormGridRowData\n): FormGridRowData[] {\n return rows.map((row) => {\n if (row.id === targetId) {\n return updater(row);\n }\n if (row.subRows && row.subRows.length > 0) {\n return { ...row, subRows: updateRowInTree(row.subRows, targetId, updater) };\n }\n return row;\n });\n}\n\n/** Recursively remove a row */\nfunction removeRowFromTree(rows: FormGridRowData[], targetId: string): FormGridRowData[] {\n return rows\n .filter((row) => row.id !== targetId)\n .map((row) => ({\n ...row,\n subRows: row.subRows ? removeRowFromTree(row.subRows, targetId) : undefined\n }));\n}\n\n/** Recursively add a sub-row to a parent */\nfunction addSubRowToParent(rows: FormGridRowData[], parentId: string, newRow: FormGridRowData): FormGridRowData[] {\n return rows.map((row) => {\n if (row.id === parentId) {\n return { ...row, subRows: [...(row.subRows || []), newRow] };\n }\n if (row.subRows && row.subRows.length > 0) {\n return { ...row, subRows: addSubRowToParent(row.subRows, parentId, newRow) };\n }\n return row;\n });\n}\n\n/** Check if any required fields are empty */\nfunction validateRows(rows: FormGridRowData[], columns: FormGridColumn[]): boolean {\n const requiredKeys = columns.filter((c) => c.required).map((c) => c.key);\n\n const checkRow = (row: FormGridRowData): boolean => {\n for (const key of requiredKeys) {\n const value = row.values[key];\n if (value === undefined || value === null || value === '') {\n return false;\n }\n }\n if (row.subRows) {\n return row.subRows.every(checkRow);\n }\n return true;\n };\n\n return rows.every(checkRow);\n}\n\n/** Get the depth of a row in the tree (0-indexed) */\nfunction getRowDepth(rows: FormGridRowData[], targetId: string, currentDepth: number = 0): number | null {\n for (const row of rows) {\n if (row.id === targetId) {\n return currentDepth;\n }\n if (row.subRows) {\n const foundDepth = getRowDepth(row.subRows, targetId, currentDepth + 1);\n if (foundDepth !== null) {\n return foundDepth;\n }\n }\n }\n return null;\n}\n\n// =============================================================================\n// COMPONENT\n// =============================================================================\n\n/**\n * FormGrid - Dynamic form builder with support for adding/removing rows and nested sub-rows.\n *\n * The component manages its own state internally and exposes methods via ref for\n * uncontrolled usage. Use `onChange` callback to react to data changes.\n *\n * @example\n * ```tsx\n * const formGridRef = useRef<FormGridRef>(null);\n *\n * const columns: FormGridColumn[] = [\n * { key: 'name', label: 'Field Name', type: 'input', placeholder: 'Enter name' },\n * { key: 'type', label: 'Type', type: 'select', options: [\n * { value: 'string', label: 'String' },\n * { value: 'number', label: 'Number' },\n * ]},\n * { key: 'required', label: 'Required', type: 'checkbox' },\n * ];\n *\n * <FormGrid ref={formGridRef} columns={columns} onChange={console.log} />\n *\n * // Get data via ref\n * const data = formGridRef.current?.getData();\n * ```\n */\nconst FormGrid = React.forwardRef<FormGridRef, FormGridProps>(\n (\n {\n columns,\n defaultRows,\n onChange,\n size = 'md',\n maxDepth = 3,\n addRowLabel = 'Add Row',\n addSubRowLabel = 'Add Sub-Row',\n allowSubRows = true,\n autoSubRow,\n disabled = false,\n showHeaders = true,\n className = '',\n validationStates = {}\n },\n ref\n ) => {\n // Internal state\n const [rows, setRows] = useState<FormGridRowData[]>(() => {\n if (defaultRows && defaultRows.length > 0) {\n return defaultRows;\n }\n return [createEmptyRow(columns)];\n });\n\n const [isMobile, setIsMobile] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const desktopGridTemplate = getDesktopGridTemplate(columns);\n\n // Responsive detection via ResizeObserver\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const updateLayout = () => {\n const containerWidth = container.offsetWidth;\n setIsMobile(containerWidth < MOBILE_BREAKPOINT);\n };\n\n updateLayout();\n\n const resizeObserver = new ResizeObserver(updateLayout);\n resizeObserver.observe(container);\n\n return () => resizeObserver.disconnect();\n }, []);\n\n // Notify parent of changes\n useEffect(() => {\n onChange?.(rows);\n }, [rows, onChange]);\n\n // Row management handlers\n const handleAddRow = useCallback(() => {\n setRows((prev) => [...prev, createEmptyRow(columns)]);\n }, [columns]);\n\n const handleAddSubRow = useCallback(\n (parentId: string) => {\n setRows((prev) => addSubRowToParent(prev, parentId, createEmptyRow(columns)));\n },\n [columns]\n );\n\n const handleRemoveRow = useCallback((id: string) => {\n setRows((prev) => {\n const result = removeRowFromTree(prev, id);\n // Ensure at least one row remains\n if (result.length === 0) {\n return prev;\n }\n return result;\n });\n }, []);\n\n const handleValueChange = useCallback(\n (rowId: string, key: string, value: unknown) => {\n setRows((prev) => {\n // First update the value\n let updated = updateRowInTree(prev, rowId, (row) => ({\n ...row,\n values: { ...row.values, [key]: value }\n }));\n\n // If autoSubRow is provided, check if we need to add/remove sub-rows\n if (autoSubRow) {\n // Get the row's current depth to respect maxDepth\n const rowDepth = getRowDepth(updated, rowId);\n\n updated = updateRowInTree(updated, rowId, (row) => {\n const shouldHaveSubRows = autoSubRow(row);\n const hasSubRows = row.subRows && row.subRows.length > 0;\n const canAddMore = rowDepth !== null && rowDepth < maxDepth;\n\n if (shouldHaveSubRows && !hasSubRows && canAddMore) {\n // Auto-add a sub-row\n return { ...row, subRows: [createEmptyRow(columns)] };\n } else if (!shouldHaveSubRows && hasSubRows) {\n // Remove sub-rows when condition no longer applies\n return { ...row, subRows: undefined };\n }\n return row;\n });\n }\n\n return updated;\n });\n },\n [autoSubRow, columns, maxDepth]\n );\n\n // Expose methods via ref\n useImperativeHandle(\n ref,\n () => ({\n getData: () => rows,\n addRow: (parentId?: string) => {\n if (parentId) {\n handleAddSubRow(parentId);\n } else {\n handleAddRow();\n }\n },\n removeRow: (id: string) => handleRemoveRow(id),\n validate: () => validateRows(rows, columns),\n reset: () => setRows([createEmptyRow(columns)])\n }),\n [rows, columns, handleAddRow, handleAddSubRow, handleRemoveRow]\n );\n\n return (\n <div ref={containerRef} className={`w-full ${className}`}>\n {/* Column Headers (desktop only) */}\n {showHeaders && !isMobile && (\n <div className=\"grid items-center gap-3 mb-2\" style={{ gridTemplateColumns: desktopGridTemplate }}>\n {columns.map((col) => (\n <div\n key={col.key}\n className={`text-sm font-medium text-muted-foreground min-w-0 ${col.type === 'checkbox' ? 'text-center' : ''}`}\n >\n {col.label}\n {col.required && <span className=\"text-error ml-0.5\">*</span>}\n </div>\n ))}\n {/* Spacer for action buttons */}\n <div className=\"w-16 shrink-0\" />\n </div>\n )}\n\n {/* Rows */}\n <div className=\"space-y-2\">\n {rows.map((row, index) => (\n <FormGridRow\n key={row.id}\n row={row}\n columns={columns}\n depth={0}\n maxDepth={maxDepth}\n isMobile={isMobile}\n size={size}\n allowSubRows={allowSubRows && !autoSubRow}\n autoSubRow={autoSubRow}\n disabled={disabled}\n addSubRowLabel={addSubRowLabel}\n onValueChange={handleValueChange}\n onAddSubRow={handleAddSubRow}\n onRemove={handleRemoveRow}\n canRemove={rows.length > 1 || index > 0}\n validationStates={validationStates[row.id]}\n />\n ))}\n </div>\n\n {/* Add Row Button */}\n <div className=\"flex items-center gap-2\">\n <ButtonWithIcon\n icon=\"plus\"\n ariaLabel={addRowLabel}\n onClick={handleAddRow}\n disabled={disabled}\n size={size}\n buttonStyle=\"link\"\n className=\"pl-0!\"\n >\n {addRowLabel}\n </ButtonWithIcon>\n </div>\n </div>\n );\n }\n);\n\nFormGrid.displayName = 'FormGrid';\n\nexport default FormGrid;\n"],"names":[],"mappings":";;;;AAmGA,MAAM,oBAAoB;AAE1B,SAAS,uBAAuB,OAAuB;AACrD,QAAM,QAAQ;AACd,SAAO,GAAG,KAAK,IAAI,OAAO,MAAM,SAAS,CAAC,CAAC;AAC7C;AAMA,SAAS,mBAAmB,OAAuB;AACjD,MAAI,UAAU,YAAY,UAAU,SAAU,QAAO;AACrD,MAAI,UAAU,SAAU,QAAO;AAE/B,QAAM,WAAW,MAAM,MAAM,WAAW;AACxC,MAAI,iBAAiB,GAAG,SAAS,SAAS,CAAC,CAAC,IAAI,IAAI;AAEpD,QAAM,YAAY,MAAM,MAAM,kBAAkB;AAChD,MAAI,UAAW,QAAO,IAAK,SAAS,UAAU,CAAC,CAAC,IAAI,SAAS,UAAU,CAAC,CAAC,IAAK,KAAK,QAAQ,CAAC,CAAC;AAE7F,MAAI,+BAA+B,KAAK,KAAK,KAAK,+CAA+C,KAAK,KAAK,GAAG;AAC5G,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,uBAAuB,SAAmC;AACjE,QAAM,eAAe,QAAQ,IAAI,CAAC,QAAQ;AACxC,QAAI,IAAI,OAAO;AACb,aAAO,mBAAmB,IAAI,KAAK;AAAA,IACrC;AACA,QAAI,IAAI,SAAS,YAAY;AAC3B,aAAO,uBAAuB,IAAI,KAAK;AAAA,IACzC;AACA,WAAO;AAAA,EACT,CAAC;AAED,SAAO,GAAG,aAAa,KAAK,GAAG,CAAC;AAClC;AAGA,SAAS,aAAqB;AAC5B,SAAO,OAAO,WAAA;AAChB;AAGA,SAAS,eAAe,SAA4C;AAClE,QAAM,SAAkC,CAAA;AACxC,UAAQ,QAAQ,CAAC,QAAQ;AACvB,QAAI,IAAI,iBAAiB,QAAW;AAClC,aAAO,IAAI,GAAG,IAAI,IAAI;AAAA,IACxB,WAAW,IAAI,SAAS,YAAY;AAClC,aAAO,IAAI,GAAG,IAAI;AAAA,IACpB,OAAO;AACL,aAAO,IAAI,GAAG,IAAI;AAAA,IACpB;AAAA,EACF,CAAC;AACD,SAAO,EAAE,IAAI,WAAA,GAAc,OAAA;AAC7B;AAGA,SAAS,gBACP,MACA,UACA,SACmB;AACnB,SAAO,KAAK,IAAI,CAAC,QAAQ;AACvB,QAAI,IAAI,OAAO,UAAU;AACvB,aAAO,QAAQ,GAAG;AAAA,IACpB;AACA,QAAI,IAAI,WAAW,IAAI,QAAQ,SAAS,GAAG;AACzC,aAAO,EAAE,GAAG,KAAK,SAAS,gBAAgB,IAAI,SAAS,UAAU,OAAO,EAAA;AAAA,IAC1E;AACA,WAAO;AAAA,EACT,CAAC;AACH;AAGA,SAAS,kBAAkB,MAAyB,UAAqC;AACvF,SAAO,KACJ,OAAO,CAAC,QAAQ,IAAI,OAAO,QAAQ,EACnC,IAAI,CAAC,SAAS;AAAA,IACb,GAAG;AAAA,IACH,SAAS,IAAI,UAAU,kBAAkB,IAAI,SAAS,QAAQ,IAAI;AAAA,EAAA,EAClE;AACN;AAGA,SAAS,kBAAkB,MAAyB,UAAkB,QAA4C;AAChH,SAAO,KAAK,IAAI,CAAC,QAAQ;AACvB,QAAI,IAAI,OAAO,UAAU;AACvB,aAAO,EAAE,GAAG,KAAK,SAAS,CAAC,GAAI,IAAI,WAAW,IAAK,MAAM,EAAA;AAAA,IAC3D;AACA,QAAI,IAAI,WAAW,IAAI,QAAQ,SAAS,GAAG;AACzC,aAAO,EAAE,GAAG,KAAK,SAAS,kBAAkB,IAAI,SAAS,UAAU,MAAM,EAAA;AAAA,IAC3E;AACA,WAAO;AAAA,EACT,CAAC;AACH;AAGA,SAAS,aAAa,MAAyB,SAAoC;AACjF,QAAM,eAAe,QAAQ,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AAEvE,QAAM,WAAW,CAAC,QAAkC;AAClD,eAAW,OAAO,cAAc;AAC9B,YAAM,QAAQ,IAAI,OAAO,GAAG;AAC5B,UAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,IAAI;AACzD,eAAO;AAAA,MACT;AAAA,IACF;AACA,QAAI,IAAI,SAAS;AACf,aAAO,IAAI,QAAQ,MAAM,QAAQ;AAAA,IACnC;AACA,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,MAAM,QAAQ;AAC5B;AAGA,SAAS,YAAY,MAAyB,UAAkB,eAAuB,GAAkB;AACvG,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,OAAO,UAAU;AACvB,aAAO;AAAA,IACT;AACA,QAAI,IAAI,SAAS;AACf,YAAM,aAAa,YAAY,IAAI,SAAS,UAAU,eAAe,CAAC;AACtE,UAAI,eAAe,MAAM;AACvB,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AA+BA,MAAM,WAAW,MAAM;AAAA,EACrB,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,WAAW;AAAA,IACX,cAAc;AAAA,IACd,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf;AAAA,IACA,WAAW;AAAA,IACX,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,mBAAmB,CAAA;AAAA,EAAC,GAEtB,QACG;AAEH,UAAM,CAAC,MAAM,OAAO,IAAI,SAA4B,MAAM;AACxD,UAAI,eAAe,YAAY,SAAS,GAAG;AACzC,eAAO;AAAA,MACT;AACA,aAAO,CAAC,eAAe,OAAO,CAAC;AAAA,IACjC,CAAC;AAED,UAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,UAAM,eAAe,OAAuB,IAAI;AAChD,UAAM,sBAAsB,uBAAuB,OAAO;AAG1D,cAAU,MAAM;AACd,YAAM,YAAY,aAAa;AAC/B,UAAI,CAAC,UAAW;AAEhB,YAAM,eAAe,MAAM;AACzB,cAAM,iBAAiB,UAAU;AACjC,oBAAY,iBAAiB,iBAAiB;AAAA,MAChD;AAEA,mBAAA;AAEA,YAAM,iBAAiB,IAAI,eAAe,YAAY;AACtD,qBAAe,QAAQ,SAAS;AAEhC,aAAO,MAAM,eAAe,WAAA;AAAA,IAC9B,GAAG,CAAA,CAAE;AAGL,cAAU,MAAM;AACd,iBAAW,IAAI;AAAA,IACjB,GAAG,CAAC,MAAM,QAAQ,CAAC;AAGnB,UAAM,eAAe,YAAY,MAAM;AACrC,cAAQ,CAAC,SAAS,CAAC,GAAG,MAAM,eAAe,OAAO,CAAC,CAAC;AAAA,IACtD,GAAG,CAAC,OAAO,CAAC;AAEZ,UAAM,kBAAkB;AAAA,MACtB,CAAC,aAAqB;AACpB,gBAAQ,CAAC,SAAS,kBAAkB,MAAM,UAAU,eAAe,OAAO,CAAC,CAAC;AAAA,MAC9E;AAAA,MACA,CAAC,OAAO;AAAA,IAAA;AAGV,UAAM,kBAAkB,YAAY,CAAC,OAAe;AAClD,cAAQ,CAAC,SAAS;AAChB,cAAM,SAAS,kBAAkB,MAAM,EAAE;AAEzC,YAAI,OAAO,WAAW,GAAG;AACvB,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH,GAAG,CAAA,CAAE;AAEL,UAAM,oBAAoB;AAAA,MACxB,CAAC,OAAe,KAAa,UAAmB;AAC9C,gBAAQ,CAAC,SAAS;AAEhB,cAAI,UAAU,gBAAgB,MAAM,OAAO,CAAC,SAAS;AAAA,YACnD,GAAG;AAAA,YACH,QAAQ,EAAE,GAAG,IAAI,QAAQ,CAAC,GAAG,GAAG,MAAA;AAAA,UAAM,EACtC;AAGF,cAAI,YAAY;AAEd,kBAAM,WAAW,YAAY,SAAS,KAAK;AAE3C,sBAAU,gBAAgB,SAAS,OAAO,CAAC,QAAQ;AACjD,oBAAM,oBAAoB,WAAW,GAAG;AACxC,oBAAM,aAAa,IAAI,WAAW,IAAI,QAAQ,SAAS;AACvD,oBAAM,aAAa,aAAa,QAAQ,WAAW;AAEnD,kBAAI,qBAAqB,CAAC,cAAc,YAAY;AAElD,uBAAO,EAAE,GAAG,KAAK,SAAS,CAAC,eAAe,OAAO,CAAC,EAAA;AAAA,cACpD,WAAW,CAAC,qBAAqB,YAAY;AAE3C,uBAAO,EAAE,GAAG,KAAK,SAAS,OAAA;AAAA,cAC5B;AACA,qBAAO;AAAA,YACT,CAAC;AAAA,UACH;AAEA,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,MACA,CAAC,YAAY,SAAS,QAAQ;AAAA,IAAA;AAIhC;AAAA,MACE;AAAA,MACA,OAAO;AAAA,QACL,SAAS,MAAM;AAAA,QACf,QAAQ,CAAC,aAAsB;AAC7B,cAAI,UAAU;AACZ,4BAAgB,QAAQ;AAAA,UAC1B,OAAO;AACL,yBAAA;AAAA,UACF;AAAA,QACF;AAAA,QACA,WAAW,CAAC,OAAe,gBAAgB,EAAE;AAAA,QAC7C,UAAU,MAAM,aAAa,MAAM,OAAO;AAAA,QAC1C,OAAO,MAAM,QAAQ,CAAC,eAAe,OAAO,CAAC,CAAC;AAAA,MAAA;AAAA,MAEhD,CAAC,MAAM,SAAS,cAAc,iBAAiB,eAAe;AAAA,IAAA;AAGhE,gCACG,OAAA,EAAI,KAAK,cAAc,WAAW,UAAU,SAAS,IAEnD,UAAA;AAAA,MAAA,eAAe,CAAC,YACf,qBAAC,OAAA,EAAI,WAAU,gCAA+B,OAAO,EAAE,qBAAqB,oBAAA,GACzE,UAAA;AAAA,QAAA,QAAQ,IAAI,CAAC,QACZ;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,WAAW,qDAAqD,IAAI,SAAS,aAAa,gBAAgB,EAAE;AAAA,YAE3G,UAAA;AAAA,cAAA,IAAI;AAAA,cACJ,IAAI,YAAY,oBAAC,QAAA,EAAK,WAAU,qBAAoB,UAAA,IAAA,CAAC;AAAA,YAAA;AAAA,UAAA;AAAA,UAJjD,IAAI;AAAA,QAAA,CAMZ;AAAA,QAED,oBAAC,OAAA,EAAI,WAAU,gBAAA,CAAgB;AAAA,MAAA,GACjC;AAAA,MAIF,oBAAC,SAAI,WAAU,aACZ,eAAK,IAAI,CAAC,KAAK,UACd;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP;AAAA,UACA;AAAA,UACA;AAAA,UACA,cAAc,gBAAgB,CAAC;AAAA,UAC/B;AAAA,UACA;AAAA,UACA;AAAA,UACA,eAAe;AAAA,UACf,aAAa;AAAA,UACb,UAAU;AAAA,UACV,WAAW,KAAK,SAAS,KAAK,QAAQ;AAAA,UACtC,kBAAkB,iBAAiB,IAAI,EAAE;AAAA,QAAA;AAAA,QAfpC,IAAI;AAAA,MAAA,CAiBZ,GACH;AAAA,MAGA,oBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW;AAAA,UACX,SAAS;AAAA,UACT;AAAA,UACA;AAAA,UACA,aAAY;AAAA,UACZ,WAAU;AAAA,UAET,UAAA;AAAA,QAAA;AAAA,MAAA,EACH,CACF;AAAA,IAAA,GACF;AAAA,EAEJ;AACF;AAEA,SAAS,cAAc;"}
|
|
1
|
+
{"version":3,"file":"form-grid.js","sources":["../../../../src/components/forms/form-grid/form-grid.tsx"],"sourcesContent":["import React, { useState, useImperativeHandle, useEffect, useRef, useCallback, ReactNode } from 'react';\nimport FormGridRow from './form-grid-row';\nimport type { InteractiveSize, ValidationState } from '../../../theme/size-tokens';\nimport { validationTextClasses } from '../../../theme/size-tokens';\nimport Text from '../../typography/text/text';\n\n// =============================================================================\n// TYPES\n// =============================================================================\n\n/**\n * Column definition for FormGrid fields\n */\nexport type FormGridColumn = {\n /** Unique key identifying this column in row data */\n key: string;\n /** Display label for the column header */\n label: string;\n /** Type of form control to render */\n type: 'input' | 'select' | 'checkbox' | 'textarea' | 'custom';\n /** HTML input type (only for type='input') */\n inputType?: 'text' | 'email' | 'number' | 'tel' | 'url' | 'password';\n /** Placeholder text for input/textarea/select */\n placeholder?: string;\n /** Options for select type */\n options?: { value: string; label: string }[];\n /** Custom render function for type='custom' */\n render?: (value: unknown, row: FormGridRowData, onChange: (value: unknown) => void, rowIndex: number) => ReactNode;\n /** Width of the field (Tailwind width class or 'flex-1') */\n width?: string;\n /** Whether this field is required */\n required?: boolean;\n /** Default value for new rows */\n defaultValue?: unknown;\n};\n\n/**\n * Row data structure with recursive sub-rows\n */\nexport type FormGridRowData = {\n /** Unique identifier for the row */\n id: string;\n /** Key-value map of field values */\n values: Record<string, unknown>;\n /** Nested sub-rows */\n subRows?: FormGridRowData[];\n};\n\n/**\n * Methods exposed via ref for uncontrolled usage\n */\nexport type FormGridRef = {\n /** Get all row data including sub-rows */\n getData: () => FormGridRowData[];\n /** Add a new row at root level or as sub-row of parentId */\n addRow: (parentId?: string) => void;\n /** Remove a row by id (searches recursively) */\n removeRow: (id: string) => void;\n /** Validate all rows, returns true if valid */\n validate: () => boolean;\n /** Reset to initial state with one empty row */\n reset: () => void;\n};\n\n/**\n * Props for the FormGrid component\n */\nexport type FormGridProps = {\n /** Column definitions describing each field */\n columns: FormGridColumn[];\n /** Initial rows data (defaults to one empty row) */\n defaultRows?: FormGridRowData[];\n /** Callback when data changes */\n onChange?: (rows: FormGridRowData[]) => void;\n /** Size variant for form controls */\n size?: InteractiveSize;\n /** Maximum nesting depth for sub-rows (default: 3) */\n maxDepth?: number;\n /** Label for add sub-row button */\n addSubRowLabel?: string;\n /** Whether to allow sub-rows (manual add button) */\n allowSubRows?: boolean;\n /** Callback to determine if a row should automatically have sub-rows. When provided, sub-rows are auto-managed based on row data (e.g., type === 'object'). No manual add button is shown. */\n autoSubRow?: (row: FormGridRowData) => boolean;\n /** Whether the entire grid is disabled */\n disabled?: boolean;\n /** Whether to show column headers */\n showHeaders?: boolean;\n /** Custom class name */\n className?: string;\n /** Validation state map by row id and column key */\n validationStates?: Record<string, Record<string, ValidationState>>;\n /** Label displayed above the grid */\n label?: string;\n /** Helper text displayed below the grid */\n helperText?: string;\n /** Error message displayed below the grid (overrides helperText) */\n error?: string;\n /** Shows required asterisk next to label */\n required?: boolean;\n};\n\n// =============================================================================\n// HELPERS\n// =============================================================================\n\nconst MOBILE_BREAKPOINT = 640;\n\nfunction getCheckboxColumnWidth(label: string): string {\n const minCh = 8;\n return `${Math.max(minCh, label.length + 2)}ch`;\n}\n\n/**\n * Convert a Tailwind width utility class to a valid CSS grid track value.\n * e.g. 'w-24' → '6rem', 'flex-1' → 'minmax(0, 1fr)', 'w-1/2' → '50%'\n */\nfunction tailwindWidthToCss(width: string): string {\n if (width === 'flex-1' || width === 'w-full') return 'minmax(0, 1fr)';\n if (width === 'w-auto') return 'auto';\n // w-{number} → n * 0.25rem\n const numMatch = width.match(/^w-(\\d+)$/);\n if (numMatch) return `${parseInt(numMatch[1]) * 0.25}rem`;\n // w-{num}/{den} fraction\n const fracMatch = width.match(/^w-(\\d+)\\/(\\d+)$/);\n if (fracMatch) return `${((parseInt(fracMatch[1]) / parseInt(fracMatch[2])) * 100).toFixed(2)}%`;\n // Already a valid CSS value (contains a unit or keyword)\n if (/\\d+(px|rem|em|%|fr|ch|vw|vh)/.test(width) || /^(auto|min-content|max-content|fit-content)$/.test(width)) {\n return width;\n }\n return 'auto';\n}\n\nfunction getDesktopGridTemplate(columns: FormGridColumn[]): string {\n const columnTracks = columns.map((col) => {\n if (col.width) {\n return tailwindWidthToCss(col.width);\n }\n if (col.type === 'checkbox') {\n return getCheckboxColumnWidth(col.label);\n }\n return 'minmax(0, 1fr)';\n });\n\n return `${columnTracks.join(' ')} 4rem`;\n}\n\n/** Generate a unique ID for rows */\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\n/** Create an empty row with default values from columns */\nfunction createEmptyRow(columns: FormGridColumn[]): FormGridRowData {\n const values: Record<string, unknown> = {};\n columns.forEach((col) => {\n if (col.defaultValue !== undefined) {\n values[col.key] = col.defaultValue;\n } else if (col.type === 'checkbox') {\n values[col.key] = false;\n } else {\n values[col.key] = '';\n }\n });\n return { id: generateId(), values };\n}\n\n/** Recursively find and update a row */\nfunction updateRowInTree(\n rows: FormGridRowData[],\n targetId: string,\n updater: (row: FormGridRowData) => FormGridRowData\n): FormGridRowData[] {\n return rows.map((row) => {\n if (row.id === targetId) {\n return updater(row);\n }\n if (row.subRows && row.subRows.length > 0) {\n return { ...row, subRows: updateRowInTree(row.subRows, targetId, updater) };\n }\n return row;\n });\n}\n\n/** Recursively remove a row */\nfunction removeRowFromTree(rows: FormGridRowData[], targetId: string): FormGridRowData[] {\n return rows\n .filter((row) => row.id !== targetId)\n .map((row) => ({\n ...row,\n subRows: row.subRows ? removeRowFromTree(row.subRows, targetId) : undefined\n }));\n}\n\n/** Recursively add a sub-row to a parent */\nfunction addSubRowToParent(rows: FormGridRowData[], parentId: string, newRow: FormGridRowData): FormGridRowData[] {\n return rows.map((row) => {\n if (row.id === parentId) {\n return { ...row, subRows: [...(row.subRows || []), newRow] };\n }\n if (row.subRows && row.subRows.length > 0) {\n return { ...row, subRows: addSubRowToParent(row.subRows, parentId, newRow) };\n }\n return row;\n });\n}\n\n/** Check if any required fields are empty */\nfunction validateRows(rows: FormGridRowData[], columns: FormGridColumn[]): boolean {\n const requiredKeys = columns.filter((c) => c.required).map((c) => c.key);\n\n const checkRow = (row: FormGridRowData): boolean => {\n for (const key of requiredKeys) {\n const value = row.values[key];\n if (value === undefined || value === null || value === '') {\n return false;\n }\n }\n if (row.subRows) {\n return row.subRows.every(checkRow);\n }\n return true;\n };\n\n return rows.every(checkRow);\n}\n\n/** Check whether a row has any user-entered content (non-empty, non-default values) */\nexport function rowHasContent(row: FormGridRowData): boolean {\n return Object.values(row.values).some((v) => v !== '' && v !== false && v !== null && v !== undefined);\n}\n\n/**\n * After a value change, ensure there is always a trailing empty row at every level\n * that contains the changed row.\n */\nfunction ensureTrailingEmptyRows(\n rows: FormGridRowData[],\n changedRowId: string,\n columns: FormGridColumn[]\n): FormGridRowData[] {\n // Check root level\n const rootIdx = rows.findIndex((r) => r.id === changedRowId);\n if (rootIdx === rows.length - 1) {\n if (rowHasContent(rows[rootIdx])) {\n return [...rows, createEmptyRow(columns)];\n }\n return rows;\n }\n\n // Recurse into sub-rows\n return rows.map((row) => {\n if (!row.subRows || row.subRows.length === 0) return row;\n const subIdx = row.subRows.findIndex((r) => r.id === changedRowId);\n if (subIdx === row.subRows.length - 1) {\n if (rowHasContent(row.subRows[subIdx])) {\n return { ...row, subRows: [...row.subRows, createEmptyRow(columns)] };\n }\n return row;\n }\n // Recurse deeper\n const newSubRows = ensureTrailingEmptyRows(row.subRows, changedRowId, columns);\n return newSubRows !== row.subRows ? { ...row, subRows: newSubRows } : row;\n });\n}\n\n/** Get the depth of a row in the tree (0-indexed) */\nfunction getRowDepth(rows: FormGridRowData[], targetId: string, currentDepth: number = 0): number | null {\n for (const row of rows) {\n if (row.id === targetId) {\n return currentDepth;\n }\n if (row.subRows) {\n const foundDepth = getRowDepth(row.subRows, targetId, currentDepth + 1);\n if (foundDepth !== null) {\n return foundDepth;\n }\n }\n }\n return null;\n}\n\n// =============================================================================\n// COMPONENT\n// =============================================================================\n\n/**\n * FormGrid - Dynamic form builder with support for adding/removing rows and nested sub-rows.\n *\n * The component manages its own state internally and exposes methods via ref for\n * uncontrolled usage. Use `onChange` callback to react to data changes.\n *\n * @example\n * ```tsx\n * const formGridRef = useRef<FormGridRef>(null);\n *\n * const columns: FormGridColumn[] = [\n * { key: 'name', label: 'Field Name', type: 'input', placeholder: 'Enter name' },\n * { key: 'type', label: 'Type', type: 'select', options: [\n * { value: 'string', label: 'String' },\n * { value: 'number', label: 'Number' },\n * ]},\n * { key: 'required', label: 'Required', type: 'checkbox' },\n * ];\n *\n * <FormGrid ref={formGridRef} columns={columns} onChange={console.log} />\n *\n * // Get data via ref\n * const data = formGridRef.current?.getData();\n * ```\n */\nconst FormGrid = React.forwardRef<FormGridRef, FormGridProps>(\n (\n {\n columns,\n defaultRows,\n onChange,\n size = 'md',\n maxDepth = 3,\n addSubRowLabel = 'Add Sub-Row',\n allowSubRows = true,\n autoSubRow,\n disabled = false,\n showHeaders = true,\n className = '',\n validationStates = {},\n label,\n helperText,\n error,\n required = false\n },\n ref\n ) => {\n // Internal state\n const [rows, setRows] = useState<FormGridRowData[]>(() => {\n if (defaultRows && defaultRows.length > 0) {\n return defaultRows;\n }\n return [createEmptyRow(columns)];\n });\n\n const [isMobile, setIsMobile] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const desktopGridTemplate = getDesktopGridTemplate(columns);\n\n // Responsive detection via ResizeObserver\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const updateLayout = () => {\n const containerWidth = container.offsetWidth;\n setIsMobile(containerWidth < MOBILE_BREAKPOINT);\n };\n\n updateLayout();\n\n const resizeObserver = new ResizeObserver(updateLayout);\n resizeObserver.observe(container);\n\n return () => resizeObserver.disconnect();\n }, []);\n\n // Notify parent of changes\n useEffect(() => {\n onChange?.(rows);\n }, [rows, onChange]);\n\n // Row management handlers\n const handleAddRow = useCallback(() => {\n setRows((prev) => [...prev, createEmptyRow(columns)]);\n }, [columns]);\n\n const handleAddSubRow = useCallback(\n (parentId: string) => {\n setRows((prev) => addSubRowToParent(prev, parentId, createEmptyRow(columns)));\n },\n [columns]\n );\n\n const handleRemoveRow = useCallback((id: string) => {\n setRows((prev) => {\n const result = removeRowFromTree(prev, id);\n // Ensure at least one row remains\n if (result.length === 0) {\n return prev;\n }\n return result;\n });\n }, []);\n\n const handleValueChange = useCallback(\n (rowId: string, key: string, value: unknown) => {\n setRows((prev) => {\n // First update the value\n let updated = updateRowInTree(prev, rowId, (row) => ({\n ...row,\n values: { ...row.values, [key]: value }\n }));\n\n // If autoSubRow is provided, check if we need to add/remove sub-rows\n if (autoSubRow) {\n // Get the row's current depth to respect maxDepth\n const rowDepth = getRowDepth(updated, rowId);\n\n updated = updateRowInTree(updated, rowId, (row) => {\n const shouldHaveSubRows = autoSubRow(row);\n const hasSubRows = row.subRows && row.subRows.length > 0;\n const canAddMore = rowDepth !== null && rowDepth < maxDepth;\n\n if (shouldHaveSubRows && !hasSubRows && canAddMore) {\n // Auto-add a sub-row\n return { ...row, subRows: [createEmptyRow(columns)] };\n } else if (!shouldHaveSubRows && hasSubRows) {\n // Remove sub-rows when condition no longer applies\n return { ...row, subRows: undefined };\n }\n return row;\n });\n }\n\n // Always ensure there is a trailing empty row at every level\n updated = ensureTrailingEmptyRows(updated, rowId, columns);\n\n return updated;\n });\n },\n [autoSubRow, columns, maxDepth]\n );\n\n // Expose methods via ref\n useImperativeHandle(\n ref,\n () => ({\n getData: () => rows,\n addRow: (parentId?: string) => {\n if (parentId) {\n handleAddSubRow(parentId);\n } else {\n handleAddRow();\n }\n },\n removeRow: (id: string) => handleRemoveRow(id),\n validate: () => validateRows(rows, columns),\n reset: () => setRows([createEmptyRow(columns)])\n }),\n [rows, columns, handleAddRow, handleAddSubRow, handleRemoveRow]\n );\n\n return (\n <div ref={containerRef} className={`w-full ${className}`}>\n {/* Label */}\n {label && (\n <label className=\"block text-sm font-semibold text-foreground mb-1.5\">\n {label}\n {required && <span className=\"text-error ml-1\">*</span>}\n </label>\n )}\n\n {/* Column Headers (desktop only) */}\n {showHeaders && !isMobile && (\n <div className=\"grid items-center gap-3 mt-3 mb-1\" style={{ gridTemplateColumns: desktopGridTemplate }}>\n {columns.map((col) => (\n <Text\n key={col.key}\n size=\"sm\"\n align={col.type === 'checkbox' ? 'center' : 'left'}\n className=\"text-muted-foreground min-w-0\"\n >\n {col.label}\n {col.required && <span className=\"text-error ml-0.5\">*</span>}\n </Text>\n ))}\n {/* Spacer for action buttons */}\n <div className=\"w-16 shrink-0\" />\n </div>\n )}\n\n {/* Rows */}\n <div className=\"space-y-1\">\n {rows.map((row) => (\n <FormGridRow\n key={row.id}\n row={row}\n columns={columns}\n depth={0}\n maxDepth={maxDepth}\n isMobile={isMobile}\n size={size}\n allowSubRows={allowSubRows && !autoSubRow}\n autoSubRow={autoSubRow}\n disabled={disabled}\n addSubRowLabel={addSubRowLabel}\n onValueChange={handleValueChange}\n onAddSubRow={handleAddSubRow}\n onRemove={handleRemoveRow}\n canRemove={rowHasContent(row)}\n validationStates={validationStates[row.id]}\n />\n ))}\n </div>\n\n {/* Helper / error text */}\n {(helperText || error) && (\n <Text size=\"xs\" className={`mt-1.5 ${error ? validationTextClasses.error : 'text-muted-foreground'}`}>\n {error || helperText}\n </Text>\n )}\n </div>\n );\n }\n);\n\nFormGrid.displayName = 'FormGrid';\n\nexport default FormGrid;\n"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,7 @@ export type Tab = {
|
|
|
10
10
|
export type TabsProps = {
|
|
11
11
|
/** Array of tab configurations with id, label, and content */
|
|
12
12
|
tabs: Tab[];
|
|
13
|
-
/** ID of the tab to display initially (defaults to first tab) */
|
|
13
|
+
/** ID of the tab to display initially when no URL param is present (defaults to first tab) */
|
|
14
14
|
defaultTab?: string;
|
|
15
15
|
/** Additional CSS classes for the tabs container */
|
|
16
16
|
className?: string;
|
|
@@ -18,13 +18,22 @@ export type TabsProps = {
|
|
|
18
18
|
ariaLabel?: string;
|
|
19
19
|
/** Remove the bottom border from the tab list */
|
|
20
20
|
noBorder?: boolean;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* URL search-param key used to persist the active tab (defaults to `"tab"`).
|
|
23
|
+
* e.g. `urlParam="section"` → `?section=details`
|
|
24
|
+
*/
|
|
25
|
+
urlParam?: string;
|
|
26
|
+
/** Called every time the active tab changes. */
|
|
27
|
+
onTabChange?: (tabId: string) => void;
|
|
23
28
|
};
|
|
24
29
|
/**
|
|
25
|
-
* Accessible Tabs component with animated tab switching using PageTransition
|
|
30
|
+
* Accessible Tabs component with animated tab switching using PageTransition.
|
|
31
|
+
*
|
|
32
|
+
* Active tab state is automatically persisted in the URL as a search param
|
|
33
|
+
* (default key: `"tab"`). Browser back/forward navigation is supported.
|
|
34
|
+
* Falls back to `defaultTab` or the first tab when the param is absent or invalid.
|
|
26
35
|
*/
|
|
27
|
-
declare function Tabs({ tabs, defaultTab, className, ariaLabel, noBorder,
|
|
36
|
+
declare function Tabs({ tabs, defaultTab, className, ariaLabel, noBorder, urlParam, onTabChange }: Readonly<TabsProps>): import("react/jsx-runtime").JSX.Element;
|
|
28
37
|
declare namespace Tabs {
|
|
29
38
|
var displayName: string;
|
|
30
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../../src/components/navigation/tabs/tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../../src/components/navigation/tabs/tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,MAAM,MAAM,GAAG,GAAG;IAChB,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,8DAA8D;IAC9D,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,CAAC;AAEF;;;;;;GAMG;AACH,iBAAS,IAAI,CAAC,EACZ,IAAI,EACJ,UAAU,EACV,SAAc,EACd,SAAkB,EAClB,QAAgB,EAChB,QAAgB,EAChB,WAAW,EACZ,EAAE,QAAQ,CAAC,SAAS,CAAC,2CAoErB;kBA5EQ,IAAI;;;AAgFb,eAAe,IAAI,CAAC"}
|