betterstart-cli 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +1 -0
  2. package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +132 -99
  3. package/dist/assets/adapters/next/plugins/portfolio/plugin.ts +17 -0
  4. package/dist/assets/adapters/next/plugins/portfolio/schemas/portfolio.json +128 -0
  5. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/dev-mode-types.ts +2 -2
  6. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx +1 -0
  7. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
  8. package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-dialog.tsx +1 -1
  9. package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-drawer.tsx +1 -1
  10. package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-item.tsx +17 -7
  11. package/dist/assets/adapters/next/templates/init/data/navigation.ts +2 -1
  12. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-source-mode.tsx +3 -3
  13. package/dist/assets/adapters/next/templates/init/hooks/use-media.ts +2 -1
  14. package/dist/assets/adapters/next/templates/init/pages/profile/profile-form.tsx +3 -3
  15. package/dist/assets/adapters/next/templates/init/types/index.ts +7 -2
  16. package/dist/assets/shared-assets/react-admin/custom/content-editor/media-gallery-block.tsx +2 -2
  17. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-add-controls.tsx +2 -2
  18. package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +219 -40
  19. package/dist/assets/shared-assets/react-admin/custom/media-field-empty-state.tsx +99 -0
  20. package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +181 -0
  21. package/dist/assets/shared-assets/react-admin/custom/sortable-gallery-item.tsx +38 -38
  22. package/dist/assets/shared-assets/react-admin/dependencies.ts +1 -0
  23. package/dist/assets/shared-assets/react-admin/schema.json +64 -2
  24. package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
  25. package/dist/assets/shared-assets/react-admin/ui/pagination.tsx +1 -1
  26. package/dist/cli.js +562 -151
  27. package/dist/cli.js.map +1 -1
  28. package/package.json +1 -1
  29. package/dist/assets/shared-assets/react-admin/custom/gallery-thumbnail.tsx +0 -39
  30. package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +0 -182
package/dist/cli.js CHANGED
@@ -125,7 +125,7 @@ function createUninstallCommand(runtime) {
125
125
  return new Command("uninstall").description("Remove all Admin files and undo modifications made by betterstart init").option("-f, --force", "Skip all confirmation prompts", false).option("--cwd <path>", "Project root path").action((options) => runtime.runUninstall(options));
126
126
  }
127
127
  function createUpdateCommand(runtime) {
128
- return new Command("update").alias("update-component").description("Update Admin components from the latest CLI templates").argument("[components...]", "Component names to update (e.g., media-gallery-field button)").option("--list", "List all available components").option("--all", "Update all components").option("--shadcn-preset <preset>", "Apply a shadcn preset to Admin UI components").option("--only <parts>", "Apply only supported shadcn preset parts: theme").option("--cwd <path>", "Project root path").action(
128
+ return new Command("update").alias("update-component").description("Update Admin components from the latest CLI templates").argument("[components...]", "Component names to update (e.g., media-field button)").option("--list", "List all available components").option("--all", "Update all components").option("--shadcn-preset <preset>", "Apply a shadcn preset to Admin UI components").option("--only <parts>", "Apply only supported shadcn preset parts: theme").option("--cwd <path>", "Project root path").action(
129
129
  (components, options) => runtime.runUpdate(components, options)
130
130
  );
131
131
  }
@@ -867,9 +867,27 @@ var blogPlugin = {
867
867
  configure: "none"
868
868
  };
869
869
 
870
+ // adapters/next/plugins/portfolio/plugin.ts
871
+ var portfolioPlugin = {
872
+ id: "portfolio",
873
+ title: "Portfolio",
874
+ description: "Portfolio content module",
875
+ kind: "content",
876
+ version: "1.0.0",
877
+ dependencies: [],
878
+ conflicts: [],
879
+ schemaFiles: ["portfolio.json"],
880
+ files: [],
881
+ packageDependencies: [],
882
+ devDependencies: [],
883
+ envSections: [],
884
+ configure: "none"
885
+ };
886
+
870
887
  // adapters/next/plugin-registry.ts
871
888
  var pluginRegistry = {
872
- blog: blogPlugin
889
+ blog: blogPlugin,
890
+ portfolio: portfolioPlugin
873
891
  };
874
892
  function listPluginDefinitions() {
875
893
  return Object.values(pluginRegistry);
@@ -895,6 +913,7 @@ var REACT_ADMIN_DEPS = [
895
913
  "@codemirror/lang-javascript",
896
914
  "@codemirror/lang-json",
897
915
  "@codemirror/lang-markdown",
916
+ "@codemirror/state",
898
917
  "@codemirror/view",
899
918
  "@dnd-kit/core",
900
919
  "@dnd-kit/sortable",
@@ -965,6 +984,18 @@ var REACT_ADMIN_DEV_DEPS = ["@tailwindcss/typography"];
965
984
  // adapters/next/init/scaffolders/dependencies.ts
966
985
  var CLI_PACKAGE_NAME = "betterstart-cli";
967
986
  var LOCAL_PACKAGE_PREFIXES = ["workspace:", "link:", "file:", "portal:"];
987
+ var TIPTAP_PACKAGE_PREFIX = "@tiptap/";
988
+ var TIPTAP_PACKAGE_VERSION_SPEC = "^3.26.0";
989
+ var CODEMIRROR_PACKAGE_SPECS = {
990
+ "@codemirror/commands": "^6.10.3",
991
+ "@codemirror/lang-javascript": "^6.2.5",
992
+ "@codemirror/lang-json": "^6.0.2",
993
+ "@codemirror/lang-markdown": "^6.5.0",
994
+ "@codemirror/state": "^6.6.0",
995
+ "@codemirror/view": "^6.43.0",
996
+ "@uiw/codemirror-theme-github": "^4.25.10",
997
+ "@uiw/react-codemirror": "^4.25.10"
998
+ };
968
999
  var requireFromCli = createRequire(import.meta.url);
969
1000
  var PNPM_PACKAGE_EXTENSION_DEFINITIONS = [
970
1001
  {
@@ -1059,6 +1090,70 @@ function hasDeclaredPackage(pkg, name) {
1059
1090
  if (!pkg) return false;
1060
1091
  return Object.hasOwn(pkg.dependencies ?? {}, name) || Object.hasOwn(pkg.devDependencies ?? {}, name) || Object.hasOwn(pkg.optionalDependencies ?? {}, name);
1061
1092
  }
1093
+ function getDeclaredRuntimePackageNames(pkg) {
1094
+ if (!pkg) return [];
1095
+ return Object.keys({
1096
+ ...pkg.dependencies ?? {},
1097
+ ...pkg.optionalDependencies ?? {}
1098
+ });
1099
+ }
1100
+ function getDeclaredDevPackageNames(pkg) {
1101
+ if (!pkg) return [];
1102
+ return Object.keys(pkg.devDependencies ?? {}).filter(
1103
+ (name) => !Object.hasOwn(pkg.dependencies ?? {}, name)
1104
+ );
1105
+ }
1106
+ function isTiptapPackageName(name) {
1107
+ return name.startsWith(TIPTAP_PACKAGE_PREFIX);
1108
+ }
1109
+ function getAlignedPackageSpec(name) {
1110
+ if (isTiptapPackageName(name)) {
1111
+ return `${name}@${TIPTAP_PACKAGE_VERSION_SPEC}`;
1112
+ }
1113
+ const versionSpec = CODEMIRROR_PACKAGE_SPECS[name];
1114
+ return versionSpec ? `${name}@${versionSpec}` : null;
1115
+ }
1116
+ function hasAlignedPackageSpec(spec) {
1117
+ return getAlignedPackageSpec(packageNameFromSpec(spec)) !== null;
1118
+ }
1119
+ function alignDependencySpecs(dependencies) {
1120
+ return dependencies.map((dependency) => {
1121
+ const name = packageNameFromSpec(dependency);
1122
+ return getAlignedPackageSpec(name) ?? dependency;
1123
+ });
1124
+ }
1125
+ function getAlignedDeclaredDependencies(packageNames) {
1126
+ return packageNames.flatMap((name) => {
1127
+ const alignedSpec = getAlignedPackageSpec(name);
1128
+ return alignedSpec ? [alignedSpec] : [];
1129
+ });
1130
+ }
1131
+ function resolveDependencyInstallPlan(options) {
1132
+ const pkg = readProjectPackageJson(options.cwd);
1133
+ const dependencies = alignDependencySpecs(options.dependencies);
1134
+ const devDependencies = alignDependencySpecs(options.devDependencies);
1135
+ const needsPackageAlignment = [...options.dependencies, ...options.devDependencies].some(
1136
+ hasAlignedPackageSpec
1137
+ );
1138
+ if (!needsPackageAlignment) {
1139
+ return {
1140
+ ...options,
1141
+ dependencies: unique(dependencies),
1142
+ devDependencies: unique(devDependencies)
1143
+ };
1144
+ }
1145
+ return {
1146
+ ...options,
1147
+ dependencies: unique([
1148
+ ...dependencies,
1149
+ ...getAlignedDeclaredDependencies(getDeclaredRuntimePackageNames(pkg))
1150
+ ]),
1151
+ devDependencies: unique([
1152
+ ...devDependencies,
1153
+ ...getAlignedDeclaredDependencies(getDeclaredDevPackageNames(pkg))
1154
+ ])
1155
+ };
1156
+ }
1062
1157
  function canResolveFromProject(cwd, name) {
1063
1158
  try {
1064
1159
  requireFromCli.resolve(name, { paths: [cwd] });
@@ -1198,21 +1293,27 @@ async function installDependenciesAsync({
1198
1293
  dependencies,
1199
1294
  devDependencies
1200
1295
  }) {
1296
+ const installPlan = resolveDependencyInstallPlan({ cwd, pm, dependencies, devDependencies });
1201
1297
  try {
1202
1298
  if (pm === "pnpm") {
1203
- ensurePnpmPackageExtensions(cwd, dependencies);
1299
+ ensurePnpmPackageExtensions(cwd, installPlan.dependencies);
1204
1300
  }
1205
- if (dependencies.length > 0) {
1206
- await spawnAsync(pm, buildAddArgs(pm, dependencies, false), cwd);
1301
+ if (installPlan.dependencies.length > 0) {
1302
+ await spawnAsync(pm, buildAddArgs(pm, installPlan.dependencies, false), cwd);
1207
1303
  }
1208
- if (devDependencies.length > 0) {
1209
- await spawnAsync(pm, buildAddArgs(pm, devDependencies, true), cwd);
1304
+ if (installPlan.devDependencies.length > 0) {
1305
+ await spawnAsync(pm, buildAddArgs(pm, installPlan.devDependencies, true), cwd);
1210
1306
  }
1211
- return { dependencies, devDeps: devDependencies, success: true, error: null };
1307
+ return {
1308
+ dependencies: installPlan.dependencies,
1309
+ devDeps: installPlan.devDependencies,
1310
+ success: true,
1311
+ error: null
1312
+ };
1212
1313
  } catch (err) {
1213
1314
  return {
1214
- dependencies,
1215
- devDeps: devDependencies,
1315
+ dependencies: installPlan.dependencies,
1316
+ devDeps: installPlan.devDependencies,
1216
1317
  success: false,
1217
1318
  error: err instanceof Error ? err.message : "Install failed"
1218
1319
  };
@@ -1641,6 +1742,7 @@ import path11 from "path";
1641
1742
  // core-engine/schema/constants.ts
1642
1743
  var FIELD_TYPES = {
1643
1744
  STRING: "string",
1745
+ EMAIL: "email",
1644
1746
  NUMBER: "number",
1645
1747
  BOOLEAN: "boolean",
1646
1748
  TEXT: "text",
@@ -1713,6 +1815,18 @@ var FORM_FIELD_TYPES = {
1713
1815
  LIST: "list",
1714
1816
  DYNAMIC_FIELDS: "dynamicFields"
1715
1817
  };
1818
+ var AUTO_FIELDS = {
1819
+ ID: "id",
1820
+ CREATED_AT: "createdAt",
1821
+ UPDATED_AT: "updatedAt",
1822
+ PUBLISHED: "published",
1823
+ SLUG: "slug"
1824
+ };
1825
+ var FORM_SUBMISSION_FIELDS = {
1826
+ SUBMITTED_AT: "submittedAt",
1827
+ IP_ADDRESS: "ipAddress",
1828
+ USER_AGENT: "userAgent"
1829
+ };
1716
1830
  function isFieldType(value) {
1717
1831
  return Object.values(FIELD_TYPES).includes(value);
1718
1832
  }
@@ -1873,6 +1987,11 @@ function loadSchema(schemasDir, name) {
1873
1987
  const parsed = parseJson(content, filePath);
1874
1988
  const obj = parsed;
1875
1989
  const type = obj.type;
1990
+ if (type === void 0) {
1991
+ throw new Error(
1992
+ `Schema "${name}" is missing required top-level "type". Expected "entity", "single", or "form".`
1993
+ );
1994
+ }
1876
1995
  if (type === "form") {
1877
1996
  return { type: "form", schema: parsed, filePath };
1878
1997
  }
@@ -1913,6 +2032,14 @@ var ENTITY_SLUG_FIELD_NAME = "slug";
1913
2032
  var ENTITY_SLUG_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "varchar", "text"]);
1914
2033
  var GENERATED_SCHEMA_TITLE_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "varchar", "text"]);
1915
2034
  var FORM_SCHEMA_TITLE_FIELD_TYPES = /* @__PURE__ */ new Set(["text"]);
2035
+ var FORM_SUBMISSION_COLUMN_NAMES = [
2036
+ AUTO_FIELDS.ID,
2037
+ FORM_SUBMISSION_FIELDS.SUBMITTED_AT,
2038
+ FORM_SUBMISSION_FIELDS.IP_ADDRESS,
2039
+ FORM_SUBMISSION_FIELDS.USER_AGENT,
2040
+ AUTO_FIELDS.CREATED_AT,
2041
+ AUTO_FIELDS.UPDATED_AT
2042
+ ];
1916
2043
  function isEntitySlugField(field) {
1917
2044
  return field.name === ENTITY_SLUG_FIELD_NAME;
1918
2045
  }
@@ -1956,52 +2083,84 @@ function createGeneratedSlugField(fields) {
1956
2083
  ...sourceField ? { defaultValueFrom: sourceField } : {}
1957
2084
  };
1958
2085
  }
1959
- function normalizeEntitySlugFields(fields, sourceField) {
1960
- let hasSlugField = false;
1961
- const normalizedFields = fields.map((field) => {
2086
+ function normalizeEntitySlugField(field, sourceField) {
2087
+ return {
2088
+ ...field,
2089
+ required: true,
2090
+ ...sourceField && !field.hidden ? { defaultValueFrom: sourceField } : {}
2091
+ };
2092
+ }
2093
+ function createGeneratedSlugFieldForTitle(fields, titleField) {
2094
+ const slugField = createGeneratedSlugField(fields);
2095
+ return titleField.slot ? { ...slugField, slot: titleField.slot } : slugField;
2096
+ }
2097
+ function orderEntityIdentityFields(fields) {
2098
+ const titleIndex = fields.findIndex(isGeneratedSchemaTitleField);
2099
+ const slugIndex = fields.findIndex(isEntitySlugField);
2100
+ if (titleIndex < 0 || slugIndex < 0 || titleIndex === 0 && slugIndex === 1) return fields;
2101
+ const titleField = fields[titleIndex];
2102
+ const slugField = fields[slugIndex];
2103
+ return [
2104
+ titleField,
2105
+ slugField,
2106
+ ...fields.filter((_, index) => index !== titleIndex && index !== slugIndex)
2107
+ ];
2108
+ }
2109
+ function normalizeEntityIdentityFields(fields, rootFields, state) {
2110
+ const normalizedFields = [];
2111
+ for (const field of fields) {
1962
2112
  if (isEntitySlugField(field)) {
1963
- hasSlugField = true;
1964
- return {
1965
- ...field,
1966
- required: true,
1967
- ...sourceField && !field.hidden ? { defaultValueFrom: sourceField } : {}
1968
- };
2113
+ if (state.keptAuthoredSlugField) continue;
2114
+ state.keptAuthoredSlugField = true;
2115
+ normalizedFields.push(normalizeEntitySlugField(field, state.sourceField));
2116
+ continue;
1969
2117
  }
1970
2118
  if ((field.type === "group" || field.type === "section") && field.fields) {
1971
- const nested = normalizeEntitySlugFields(field.fields, sourceField);
1972
- hasSlugField ||= nested.hasSlugField;
1973
- return { ...field, fields: nested.fields };
2119
+ normalizedFields.push({
2120
+ ...field,
2121
+ fields: normalizeEntityIdentityFields(field.fields, rootFields, state)
2122
+ });
2123
+ continue;
1974
2124
  }
1975
2125
  if (field.type === "tabs" && field.tabs) {
1976
- const tabs = field.tabs.map((tab) => {
1977
- const nested = normalizeEntitySlugFields(tab.fields ?? [], sourceField);
1978
- hasSlugField ||= nested.hasSlugField;
1979
- return { ...tab, fields: nested.fields };
2126
+ normalizedFields.push({
2127
+ ...field,
2128
+ tabs: field.tabs.map((tab) => ({
2129
+ ...tab,
2130
+ fields: normalizeEntityIdentityFields(tab.fields ?? [], rootFields, state)
2131
+ }))
1980
2132
  });
1981
- return { ...field, tabs };
2133
+ continue;
1982
2134
  }
1983
- return field;
1984
- });
1985
- return { fields: normalizedFields, hasSlugField };
2135
+ normalizedFields.push(field);
2136
+ if (!state.hasAuthoredSlugField && !state.generatedSlugInserted && isGeneratedSchemaTitleField(field)) {
2137
+ normalizedFields.push(createGeneratedSlugFieldForTitle(rootFields, field));
2138
+ state.generatedSlugInserted = true;
2139
+ }
2140
+ }
2141
+ return orderEntityIdentityFields(normalizedFields);
1986
2142
  }
1987
2143
  function ensureEntitySlugField(schema) {
1988
2144
  const sourceField = hasPersistedTitleField(schema.fields) ? "title" : void 0;
1989
- const normalized = normalizeEntitySlugFields(schema.fields, sourceField);
1990
- if (normalized.hasSlugField) {
2145
+ const hasAuthoredSlugField = flattenFields(schema.fields).some(isEntitySlugField);
2146
+ const state = {
2147
+ sourceField,
2148
+ hasAuthoredSlugField,
2149
+ keptAuthoredSlugField: false,
2150
+ generatedSlugInserted: false
2151
+ };
2152
+ const normalizedFields = normalizeEntityIdentityFields(schema.fields, schema.fields, state);
2153
+ if (hasAuthoredSlugField || state.generatedSlugInserted) {
1991
2154
  return {
1992
2155
  ...schema,
1993
- fields: normalized.fields
2156
+ fields: normalizedFields
1994
2157
  };
1995
2158
  }
1996
- const slugField = createGeneratedSlugField(schema.fields);
1997
- const titleIndex = schema.fields.findIndex(
1998
- (field) => field.name === GENERATED_SCHEMA_TITLE_FIELD_NAME && isStringLikeSlugField(field)
1999
- );
2000
- const insertIndex = titleIndex >= 0 ? titleIndex + 1 : Math.min(schema.fields.length, 1);
2159
+ const insertIndex = Math.min(normalizedFields.length, 1);
2001
2160
  const fields = [
2002
- ...schema.fields.slice(0, insertIndex),
2003
- slugField,
2004
- ...schema.fields.slice(insertIndex)
2161
+ ...normalizedFields.slice(0, insertIndex),
2162
+ createGeneratedSlugField(schema.fields),
2163
+ ...normalizedFields.slice(insertIndex)
2005
2164
  ];
2006
2165
  return {
2007
2166
  ...schema,
@@ -2115,6 +2274,23 @@ function collectInvalidFormColumnTypeErrors(schema, errors) {
2115
2274
  }
2116
2275
  }
2117
2276
  }
2277
+ function collectInvalidFormColumnReferenceErrors(schema, errors) {
2278
+ const fieldNames = new Set(
2279
+ getAllFormSchemaFields(schema).map((field) => field.name).filter((name) => typeof name === "string" && name.length > 0)
2280
+ );
2281
+ for (const columnName of FORM_SUBMISSION_COLUMN_NAMES) {
2282
+ fieldNames.add(columnName);
2283
+ }
2284
+ for (const column of schema.columns || []) {
2285
+ if (!column.accessorKey || !column.header) {
2286
+ errors.push(`Form column is missing required properties: ${JSON.stringify(column)}`);
2287
+ } else if (!fieldNames.has(column.accessorKey)) {
2288
+ errors.push(
2289
+ `Form column accessorKey "${column.accessorKey}" does not match any field or generated submission property. Available: ${Array.from(fieldNames).join(", ")}`
2290
+ );
2291
+ }
2292
+ }
2293
+ }
2118
2294
  function validateEntitySchema(schema) {
2119
2295
  const errors = [];
2120
2296
  collectSchemaSlotLayoutErrors(schema, errors);
@@ -2134,6 +2310,9 @@ function validateEntitySchema(schema) {
2134
2310
  if (!Array.isArray(normalizedSchema.fields) || normalizedSchema.fields.length === 0) {
2135
2311
  errors.push('Schema must have at least one field via "fields" or "slot"');
2136
2312
  }
2313
+ if (normalizedSchema.type !== "entity") {
2314
+ errors.push('Entity schema must have `type: "entity"`');
2315
+ }
2137
2316
  if (!Array.isArray(normalizedSchema.columns) || normalizedSchema.columns.length === 0) {
2138
2317
  errors.push("Schema must have at least one column");
2139
2318
  }
@@ -2196,6 +2375,9 @@ function validateSingleSchema(schema) {
2196
2375
  if (!Array.isArray(normalizedSchema.fields) || normalizedSchema.fields.length === 0) {
2197
2376
  errors.push('Schema must have at least one field via "fields" or "slot"');
2198
2377
  }
2378
+ if (normalizedSchema.type !== "single") {
2379
+ errors.push('Single schema must have `type: "single"`');
2380
+ }
2199
2381
  for (const field of normalizedSchema.fields || []) {
2200
2382
  if (field.type === "separator") continue;
2201
2383
  if (!field.name || !field.type) {
@@ -2255,6 +2437,7 @@ function validateFormSchema(schema) {
2255
2437
  }
2256
2438
  collectMissingFormSchemaTitleErrors(schema, errors);
2257
2439
  collectInvalidFormColumnTypeErrors(schema, errors);
2440
+ collectInvalidFormColumnReferenceErrors(schema, errors);
2258
2441
  return errors;
2259
2442
  }
2260
2443
  function validateLoadedSchema(loaded) {
@@ -3632,6 +3815,7 @@ function toDrizzleType(field, requiredImports) {
3632
3815
  requiredImports.add("uuid");
3633
3816
  return "uuid().defaultRandom()";
3634
3817
  case "string":
3818
+ case "email":
3635
3819
  case "varchar":
3636
3820
  requiredImports.add("varchar");
3637
3821
  return field.length ? `varchar({ length: ${field.length} })` : `varchar({ length: ${DRIZZLE_DEFAULTS.VARCHAR_LENGTH} })`;
@@ -3788,6 +3972,7 @@ function toTypeScriptType(field, mode = "output") {
3788
3972
  case "boolean":
3789
3973
  return "boolean";
3790
3974
  case "string":
3975
+ case "email":
3791
3976
  case "varchar":
3792
3977
  case "text":
3793
3978
  case "markdown":
@@ -3962,7 +4147,7 @@ function generateFieldMapping(field, source = "input") {
3962
4147
  if ((field.type === "date" || field.type === "timestamp" || field.type === "time") && !field.required) {
3963
4148
  return `${source}.${field.name} && ${source}.${field.name} !== '' ? ${source}.${field.name} : null`;
3964
4149
  }
3965
- if (!field.required && ["string", "varchar", "text", "select"].includes(field.type)) {
4150
+ if (!field.required && ["string", "email", "varchar", "text", "select"].includes(field.type)) {
3966
4151
  return `${source}.${field.name} && ${source}.${field.name} !== '' ? ${source}.${field.name} : null`;
3967
4152
  }
3968
4153
  return `${source}.${field.name}`;
@@ -4228,6 +4413,7 @@ ${blocks.join("\n\n")}
4228
4413
  // adapters/next/generators/entity-filters.ts
4229
4414
  var FILTERABLE_FIELD_TYPES = /* @__PURE__ */ new Set([
4230
4415
  "string",
4416
+ "email",
4231
4417
  "varchar",
4232
4418
  "text",
4233
4419
  "boolean",
@@ -4469,6 +4655,8 @@ function sampleSchemaFieldValue(field) {
4469
4655
  switch (field.type) {
4470
4656
  case "boolean":
4471
4657
  return true;
4658
+ case "email":
4659
+ return "developer@example.com";
4472
4660
  case "number":
4473
4661
  case "decimal":
4474
4662
  return 1;
@@ -5240,21 +5428,19 @@ function buildFormDevModeSnippets(schema, actionImportPath, adminImportAlias) {
5240
5428
  const fields = getAllFormSchemaFields(schema);
5241
5429
  const submissionInput = createFormInputObject(fields);
5242
5430
  const actionModulePath = resolveActionModulePath(actionImportPath, adminImportAlias);
5431
+ const formComponentModulePath = `${adminImportAlias ?? DEFAULT_ADMIN_IMPORT_ALIAS}/components/forms/${toKebabCase(schema.name)}-form`;
5243
5432
  const submissionData = createFormSubmissionObject(fields);
5244
5433
  const submitResponse = { success: true, submission: submissionData };
5245
5434
  const submissionsResponse = { submissions: [submissionData], total: 1 };
5246
5435
  const deleteResponse = { success: true };
5247
5436
  const formLabel2 = schema.label;
5248
- return [
5249
- {
5250
- id: "submit",
5251
- label: `Submit ${formLabel2}`,
5252
- kind: "mutation",
5253
- icon: "send",
5254
- language: "tsx",
5255
- payloadExample: createPayloadExample(submissionInput),
5256
- responseExample: createResponseExample(submitResponse),
5257
- code: `'use client'
5437
+ const formComponentCode = `import { ${pascal}Form } from '${formComponentModulePath}'
5438
+
5439
+ export default function ${pascal}Page() {
5440
+ return <${pascal}Form />
5441
+ }
5442
+ `;
5443
+ const submitActionCode = `'use client'
5258
5444
 
5259
5445
  import * as React from 'react'
5260
5446
  import {
@@ -5281,7 +5467,25 @@ export function ${pascal}SubmitButton() {
5281
5467
  </button>
5282
5468
  )
5283
5469
  }
5284
- `
5470
+ `;
5471
+ return [
5472
+ {
5473
+ id: "jsx",
5474
+ label: `${formLabel2} Component`,
5475
+ kind: "jsx",
5476
+ icon: "file-code",
5477
+ language: "tsx",
5478
+ code: formComponentCode
5479
+ },
5480
+ {
5481
+ id: "submit",
5482
+ label: `Submit ${formLabel2}`,
5483
+ kind: "mutation",
5484
+ icon: "send",
5485
+ language: "tsx",
5486
+ payloadExample: createPayloadExample(submissionInput),
5487
+ responseExample: createResponseExample(submitResponse),
5488
+ code: submitActionCode
5285
5489
  },
5286
5490
  {
5287
5491
  id: "submissions",
@@ -5375,7 +5579,7 @@ function generateColumnDef(col) {
5375
5579
  case "email":
5376
5580
  cellDef = `cell: ({ row }) => {
5377
5581
  const email = row.getValue('${col.accessorKey}') as string
5378
- return email ? <a href={\`mailto:\${email}\`} className="text-primary hover:underline">{email}</a> : '-'
5582
+ return <div className="text-sm font-medium">{email || '-'}</div>
5379
5583
  }`;
5380
5584
  break;
5381
5585
  case "date":
@@ -7184,7 +7388,7 @@ export { CACHE_TAG, CACHE_TAG_BY_ID } from './types'
7184
7388
  // adapters/next/generators/form-pipeline/form-component-shared.ts
7185
7389
  import fs16 from "fs";
7186
7390
  import path20 from "path";
7187
- var ADMIN_CUSTOM_COMPONENTS = /* @__PURE__ */ new Set(["media-gallery-field"]);
7391
+ var ADMIN_CUSTOM_COMPONENTS = /* @__PURE__ */ new Set(["media-field"]);
7188
7392
  function resolveUiImport(cwd, componentName) {
7189
7393
  const componentDirectory = ADMIN_CUSTOM_COMPONENTS.has(componentName) ? "components/custom" : "components/ui";
7190
7394
  const locations = [componentDirectory, `src/${componentDirectory}`];
@@ -7265,6 +7469,26 @@ ${decls}
7265
7469
  function getListFields(fields) {
7266
7470
  return fields.filter((f) => f.name && f.type === "list" && f.fields && f.fields.length > 0);
7267
7471
  }
7472
+ function hasFormDescriptionFields(fields) {
7473
+ return fields.some((field) => {
7474
+ if (field.hint) return true;
7475
+ if ((field.type === "group" || field.type === "list") && field.fields) {
7476
+ return hasFormDescriptionFields(field.fields);
7477
+ }
7478
+ return false;
7479
+ });
7480
+ }
7481
+ function hasSelectFields(fields) {
7482
+ return fields.some((field) => {
7483
+ if (field.type === "select" && field.options && field.options.length > 0) {
7484
+ return true;
7485
+ }
7486
+ if ((field.type === "group" || field.type === "list") && field.fields) {
7487
+ return hasSelectFields(field.fields);
7488
+ }
7489
+ return false;
7490
+ });
7491
+ }
7268
7492
  function renderFieldsJSX(fields) {
7269
7493
  return fields.filter((f) => !f.hidden).map((f) => {
7270
7494
  if (f.type === "dynamicFields") return "";
@@ -7516,7 +7740,7 @@ function generateFileUploadFieldJSX(field, name, label, hintJSX, requiredStar) {
7516
7740
  <FormItem${formItemProps(field.height)}>
7517
7741
  ${formLabelWithDescriptionLine(name, label, requiredStar, hintJSX, " ")}
7518
7742
  <FormControl>
7519
- <MediaGalleryField
7743
+ <MediaField
7520
7744
  value={field.value}
7521
7745
  onChange={field.onChange}
7522
7746
  onBlur={field.onBlur}
@@ -7684,13 +7908,15 @@ ${fieldsJSX}
7684
7908
  const successHandler = redirectUrl ? `window.location.href = ${JSON.stringify(redirectUrl)}` : "setSubmitted(true)";
7685
7909
  const hasRadio = allFields.some((f) => f.type === "radio");
7686
7910
  const hasFileUpload = allFields.some((f) => f.type === "file" || f.type === "upload");
7911
+ const hasFormDescriptions = hasFormDescriptionFields(allFields);
7912
+ const hasSelectControls = hasSelectFields(allFields);
7687
7913
  const buttonImport = resolveUiImport(cwd, "button");
7688
7914
  const formImport = resolveUiImport(cwd, "form");
7689
7915
  const inputImport = resolveUiImport(cwd, "input");
7690
7916
  const textareaImport = resolveUiImport(cwd, "textarea");
7691
- const selectImport = resolveUiImport(cwd, "select");
7917
+ const selectImport = hasSelectControls ? resolveUiImport(cwd, "select") : "";
7692
7918
  const radioGroupImport = resolveUiImport(cwd, "radio-group");
7693
- const mediaUploadImport = resolveUiImport(cwd, "media-gallery-field");
7919
+ const mediaUploadImport = resolveUiImport(cwd, "media-field");
7694
7920
  const content = buildComponentSource({
7695
7921
  pascal,
7696
7922
  kebab,
@@ -7698,6 +7924,8 @@ ${fieldsJSX}
7698
7924
  hasRadio,
7699
7925
  hasFileUpload,
7700
7926
  hasListFields,
7927
+ hasFormDescriptions,
7928
+ hasSelectControls,
7701
7929
  actionImportPath,
7702
7930
  buttonImport,
7703
7931
  formImport,
@@ -7767,17 +7995,16 @@ import { Button } from '${p15.buttonImport}'
7767
7995
  import {
7768
7996
  Form,
7769
7997
  FormControl,
7770
- FormDescription,
7771
- FormField,
7998
+ ${p15.hasFormDescriptions ? " FormDescription,\n" : ""} FormField,
7772
7999
  FormItem,
7773
8000
  FormLabel,
7774
8001
  FormMessage,
7775
8002
  } from '${p15.formImport}'
7776
8003
  import { Input } from '${p15.inputImport}'${p15.hasFileUpload ? `
7777
- import { MediaGalleryField } from '${p15.mediaUploadImport}'` : ""}
8004
+ import { MediaField } from '${p15.mediaUploadImport}'` : ""}
7778
8005
  ${p15.hasRadio ? `import { RadioGroup, RadioGroupItem } from '${p15.radioGroupImport}'
7779
8006
  ` : ""}
7780
- import { Textarea } from '${p15.textareaImport}'
8007
+ import { Textarea } from '${p15.textareaImport}'${p15.hasSelectControls ? `
7781
8008
  import {
7782
8009
  Select,
7783
8010
  SelectContent,
@@ -7785,6 +8012,7 @@ import {
7785
8012
  SelectTrigger,
7786
8013
  SelectValue,
7787
8014
  } from '${p15.selectImport}'
8015
+ ` : ""}
7788
8016
 
7789
8017
  const formSchema = z.object({
7790
8018
  ${p15.zodFields}
@@ -7932,13 +8160,15 @@ ${buildFieldArrayDecls(listFields)}
7932
8160
  ` : "";
7933
8161
  const hasRadio = fields.some((f) => f.type === "radio");
7934
8162
  const hasFileUpload = fields.some((f) => f.type === "file" || f.type === "upload");
8163
+ const hasFormDescriptions = hasFormDescriptionFields(rawFields);
8164
+ const hasSelectControls = hasSelectFields(rawFields);
7935
8165
  const buttonImport = resolveUiImport(cwd, "button");
7936
8166
  const formImport = resolveUiImport(cwd, "form");
7937
8167
  const inputImport = resolveUiImport(cwd, "input");
7938
8168
  const textareaImport = resolveUiImport(cwd, "textarea");
7939
- const selectImport = resolveUiImport(cwd, "select");
8169
+ const selectImport = hasSelectControls ? resolveUiImport(cwd, "select") : "";
7940
8170
  const radioGroupImport = resolveUiImport(cwd, "radio-group");
7941
- const mediaUploadImport = resolveUiImport(cwd, "media-gallery-field");
8171
+ const mediaUploadImport = resolveUiImport(cwd, "media-field");
7942
8172
  const queryClientImport = hasFileUpload ? `
7943
8173
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'` : "";
7944
8174
  const queryClientSetup = hasFileUpload ? `
@@ -7967,16 +8197,15 @@ import { Button } from '${buttonImport}'
7967
8197
  import {
7968
8198
  Form,
7969
8199
  FormControl,
7970
- FormDescription,
7971
- FormField,
8200
+ ${hasFormDescriptions ? " FormDescription,\n" : ""} FormField,
7972
8201
  FormItem,
7973
8202
  FormLabel,
7974
8203
  FormMessage,
7975
8204
  } from '${formImport}'
7976
8205
  import { Input } from '${inputImport}'${hasFileUpload ? `
7977
- import { MediaGalleryField } from '${mediaUploadImport}'` : ""}${hasRadio ? `
8206
+ import { MediaField } from '${mediaUploadImport}'` : ""}${hasRadio ? `
7978
8207
  import { RadioGroup, RadioGroupItem } from '${radioGroupImport}'` : ""}
7979
- import { Textarea } from '${textareaImport}'
8208
+ import { Textarea } from '${textareaImport}'${hasSelectControls ? `
7980
8209
  import {
7981
8210
  Select,
7982
8211
  SelectContent,
@@ -7984,6 +8213,7 @@ import {
7984
8213
  SelectTrigger,
7985
8214
  SelectValue,
7986
8215
  } from '${selectImport}'
8216
+ ` : ""}
7987
8217
 
7988
8218
  const formSchema = z.object({
7989
8219
  ${zodFields}
@@ -9352,7 +9582,7 @@ ${updateBeforePublishBlock}
9352
9582
  (typeof value === 'number' && Number.isNaN(value))
9353
9583
  ? null
9354
9584
  : value
9355
- } else if (!field.required && ['date', 'timestamp', 'time', 'string', 'varchar', 'text', 'select', 'relationship'].includes(field.type)) {
9585
+ } else if (!field.required && ['date', 'timestamp', 'time', 'string', 'email', 'varchar', 'text', 'select', 'relationship'].includes(field.type)) {
9356
9586
  processedData[key] = value && value !== '' ? value : null
9357
9587
  } else {
9358
9588
  processedData[key] = value
@@ -10593,7 +10823,7 @@ function buildEntityContext(schema, nextMajorVersion) {
10593
10823
  );
10594
10824
  const hasCreatableSelectFields = creatableSelectFields.length > 0;
10595
10825
  const selectFields = collectReadSelectFields(allDbFields);
10596
- const hasSelectFields = selectFields.length > 0;
10826
+ const hasSelectFields2 = selectFields.length > 0;
10597
10827
  const dateRangeFilterFields = new Set(
10598
10828
  resolvedFilters.filter((filter) => filter.type === "date-range").map((filter) => filter.field)
10599
10829
  );
@@ -10746,7 +10976,7 @@ ${searchFields.map((f) => ` ilike(${tableVar}.${f}, searchTerm)`).join(
10746
10976
  Singular,
10747
10977
  hasListRels
10748
10978
  );
10749
- const selectResultMapping = hasSelectFields ? wrapWithSelectResolution(baseResultMapping, camelPlural, Singular) : baseResultMapping;
10979
+ const selectResultMapping = hasSelectFields2 ? wrapWithSelectResolution(baseResultMapping, camelPlural, Singular) : baseResultMapping;
10750
10980
  const resultMapping = hasMediaFields ? wrapWithMediaResolution(selectResultMapping, camelPlural, Singular) : selectResultMapping;
10751
10981
  const baseListResultMapping = hasHtmlOutput ? buildResultMapping(
10752
10982
  listDbFields,
@@ -10756,7 +10986,7 @@ ${searchFields.map((f) => ` ilike(${tableVar}.${f}, searchTerm)`).join(
10756
10986
  Singular,
10757
10987
  hasListRels
10758
10988
  ) : baseResultMapping;
10759
- const selectListResultMapping = hasSelectFields ? wrapWithSelectResolution(baseListResultMapping, camelPlural, Singular) : baseListResultMapping;
10989
+ const selectListResultMapping = hasSelectFields2 ? wrapWithSelectResolution(baseListResultMapping, camelPlural, Singular) : baseListResultMapping;
10760
10990
  const listResultMapping = hasMediaFields ? wrapWithMediaResolution(selectListResultMapping, camelPlural, Singular) : selectListResultMapping;
10761
10991
  const fieldMeta = createFields.map((f) => `{ name: '${f.name}', type: '${f.type}', required: ${f.required ?? false} }`).join(",\n ");
10762
10992
  const createMappings = createFields.map((f) => ` ${f.name}: ${f.name === "slug" ? "resolvedSlug" : generateFieldMapping(f)}`).join(",\n");
@@ -10787,7 +11017,7 @@ ${searchFields.map((f) => ` ilike(${tableVar}.${f}, searchTerm)`).join(
10787
11017
  return ${singleRowMapping}`;
10788
11018
  })();
10789
11019
  const singleRowResolvers = [
10790
- ...hasSelectFields ? [`resolve${Singular}SelectFields`] : [],
11020
+ ...hasSelectFields2 ? [`resolve${Singular}SelectFields`] : [],
10791
11021
  ...hasMediaFields ? [`resolve${Singular}MediaFields`] : []
10792
11022
  ];
10793
11023
  const singleRowReturn = singleRowResolvers.length > 0 ? wrapSingleRowReturnWithResolvers(baseSingleRowReturn, singleRowResolvers) : baseSingleRowReturn;
@@ -10821,7 +11051,7 @@ ${searchFields.map((f) => ` ilike(${tableVar}.${f}, searchTerm)`).join(
10821
11051
  hasDraft,
10822
11052
  hasSearch,
10823
11053
  hasFilters,
10824
- hasSelectFields,
11054
+ hasSelectFields: hasSelectFields2,
10825
11055
  hasCreatableSelectFields,
10826
11056
  searchFields,
10827
11057
  htmlOutputFields,
@@ -11037,7 +11267,7 @@ function generateSingleActions(schema, actionsDir, options = {}) {
11037
11267
  );
11038
11268
  const hasCreatableSelectFields = creatableSelectFields.length > 0;
11039
11269
  const selectFields = collectReadSelectFields(dbFields);
11040
- const hasSelectFields = selectFields.length > 0;
11270
+ const hasSelectFields2 = selectFields.length > 0;
11041
11271
  const relationshipFields = dbFields.filter(
11042
11272
  (f) => f.type === "relationship" && f.relationship && !f.multiple
11043
11273
  );
@@ -11150,7 +11380,7 @@ ${allUpsertInterfaceFields}
11150
11380
  const m2mReadBlock = hasM2M ? `
11151
11381
  ${m2mReadQueries}
11152
11382
  ${m2mAssignments}` : "";
11153
- const singleRowReturn = hasSelectFields ? `const row = result[0]
11383
+ const singleRowReturn = hasSelectFields2 ? `const row = result[0]
11154
11384
  const singleRow = ${singleRowMapping}${m2mReadBlock}
11155
11385
  const [resolvedSelectRow] = await resolve${Singular}SelectFields([singleRow])
11156
11386
  return resolvedSelectRow` : `const row = result[0]
@@ -11169,7 +11399,7 @@ ${m2mAssignments}` : "";
11169
11399
  ...hasCreatableSelectFields ? [`${tableVar}SelectOptions`] : []
11170
11400
  ])
11171
11401
  ).sort();
11172
- const selectResolutionFunctions = hasSelectFields ? `
11402
+ const selectResolutionFunctions = hasSelectFields2 ? `
11173
11403
  ${generateSingleResolveSelectFieldsFunction(Singular, selectFields, `${tableVar}SelectOptions`, hasCreatableSelectFields)}
11174
11404
  ` : "";
11175
11405
  const getContent = `'use server'
@@ -11180,7 +11410,7 @@ import { eq } from 'drizzle-orm'
11180
11410
  import { alias } from 'drizzle-orm/pg-core'
11181
11411
  ${cacheReadImport}
11182
11412
  import { ${cacheTagsVar} } from './types'
11183
- import type { ${Singular}${hasSelectFields ? ", SelectOptionValue" : ""} } from './types'
11413
+ import type { ${Singular}${hasSelectFields2 ? ", SelectOptionValue" : ""} } from './types'
11184
11414
 
11185
11415
  ${buildAuthorshipAliasBlock()}
11186
11416
 
@@ -11279,7 +11509,7 @@ ${m2mInputDeclarations ? `${m2mInputDeclarations}
11279
11509
  (typeof value === 'number' && Number.isNaN(value))
11280
11510
  ? null
11281
11511
  : value
11282
- } else if (!field.required && ['date', 'timestamp', 'time', 'string', 'varchar', 'text', 'select', 'relationship'].includes(field.type)) {
11512
+ } else if (!field.required && ['date', 'timestamp', 'time', 'string', 'email', 'varchar', 'text', 'select', 'relationship'].includes(field.type)) {
11283
11513
  processedData[key] = value && value !== '' ? value : null
11284
11514
  } else {
11285
11515
  processedData[key] = value
@@ -12639,6 +12869,8 @@ function getFormFieldType(field) {
12639
12869
  switch (field.type) {
12640
12870
  case "boolean":
12641
12871
  return "checkbox";
12872
+ case "email":
12873
+ return "email";
12642
12874
  case "text":
12643
12875
  return "textarea";
12644
12876
  case "markdown":
@@ -12670,6 +12902,10 @@ function getZodType(field) {
12670
12902
  return field.required ? `z.number({ message: '${label} is required' })` : "z.number().nullable().optional()";
12671
12903
  case "boolean":
12672
12904
  return "z.boolean()";
12905
+ case "email": {
12906
+ const base = field.required ? `z.string().min(1, '${label} is required').email('Please enter a valid email address')` : `z.string().optional().refine((val) => !val || val.trim() === '' || z.string().email().safeParse(val).success, { message: 'Please enter a valid email address' })`;
12907
+ return field.length ? `${base}.max(${field.length})` : base;
12908
+ }
12673
12909
  case "string":
12674
12910
  case "varchar":
12675
12911
  case "text":
@@ -12808,6 +13044,40 @@ function collectRelationshipFields(fields, _tabFieldNames) {
12808
13044
  collect(fields);
12809
13045
  return result;
12810
13046
  }
13047
+ function getTabNestedFields(tab) {
13048
+ return [
13049
+ ...tab.fields ?? [],
13050
+ ...tab.slot?.main?.fields ?? [],
13051
+ ...tab.slot?.sidebar?.fields ?? []
13052
+ ];
13053
+ }
13054
+ function collectTabIconNames(fields) {
13055
+ const icons = /* @__PURE__ */ new Set();
13056
+ function collect(fieldsToCheck) {
13057
+ for (const field of fieldsToCheck) {
13058
+ if (field.fields) collect(field.fields);
13059
+ if (field.tabs) {
13060
+ for (const tab of field.tabs) {
13061
+ const icon = tab.icon?.trim();
13062
+ if (icon) icons.add(toPascalCase(icon));
13063
+ collect(getTabNestedFields(tab));
13064
+ }
13065
+ }
13066
+ }
13067
+ }
13068
+ collect(fields);
13069
+ return Array.from(icons);
13070
+ }
13071
+ function renderTabsTrigger(tab, indent, options = {}) {
13072
+ const iconName = tab.icon?.trim() ? toPascalCase(tab.icon.trim()) : null;
13073
+ const className = options.className ? ` className="${options.className}"` : "";
13074
+ if (!iconName)
13075
+ return `${indent}<TabsTrigger value="${tab.name}"${className}>${tab.label}</TabsTrigger>`;
13076
+ return `${indent}<TabsTrigger value="${tab.name}"${className}>
13077
+ ${indent} <${iconName} className="size-4 shrink-0" />
13078
+ ${indent} <span>${tab.label}</span>
13079
+ ${indent}</TabsTrigger>`;
13080
+ }
12811
13081
  function selectDefaultValues(f) {
12812
13082
  if (f.default === void 0 || f.default === null || f.default === "") return [];
12813
13083
  const values = Array.isArray(f.default) ? f.default : [f.default];
@@ -13082,9 +13352,7 @@ function buildUiImports(ctx) {
13082
13352
  if (ctx.hasBoolean) uiImports.push("import { Checkbox } from '@admin/components/ui/checkbox'");
13083
13353
  if (ctx.hasTextarea) uiImports.push("import { Textarea } from '@admin/components/ui/textarea'");
13084
13354
  if (ctx.hasImage || ctx.hasVideo || ctx.hasMedia)
13085
- uiImports.push(
13086
- "import { MediaGalleryField } from '@admin/components/custom/media-gallery-field'"
13087
- );
13355
+ uiImports.push("import { MediaField } from '@admin/components/custom/media-field'");
13088
13356
  if (ctx.hasGallery)
13089
13357
  uiImports.push("import { GalleryField } from '@admin/components/custom/gallery-field'");
13090
13358
  if (ctx.hasIcon || ctx.hasIconPostfix)
@@ -13299,7 +13567,7 @@ ${indent} render={({ field: formField }) => (
13299
13567
  ${indent} <FormItem${formItemProps2(field)}>
13300
13568
  ${indent} ${labelWithDescription(formLabel(field, nestedLabel), nestedHint, `${indent} `)}
13301
13569
  ${indent} <FormControl>
13302
- ${indent} <MediaGalleryField value={formField.value} onChange={formField.onChange} onBlur={formField.onBlur} disabled={isPending}${acceptProp} />
13570
+ ${indent} <MediaField value={formField.value} onChange={formField.onChange} onBlur={formField.onBlur} disabled={isPending}${acceptProp} />
13303
13571
  ${indent} </FormControl>
13304
13572
  ${indent} <FormMessage />
13305
13573
  ${indent} </FormItem>
@@ -13594,10 +13862,10 @@ function generateFieldJSXCore(field, indent = " ", context = {}) {
13594
13862
  if (field.type === "separator") return renderSeparatorField(field, indent);
13595
13863
  if (field.type === "boolean") return renderBooleanField(field, indent, label, hintJSX, context);
13596
13864
  if (field.type === "image")
13597
- return renderMediaGalleryField(field, indent, label, hintJSX, context, "image/*");
13865
+ return renderMediaField(field, indent, label, hintJSX, context, "image/*");
13598
13866
  if (field.type === "video")
13599
- return renderMediaGalleryField(field, indent, label, hintJSX, context, "video/*");
13600
- if (field.type === "media") return renderMediaGalleryField(field, indent, label, hintJSX, context);
13867
+ return renderMediaField(field, indent, label, hintJSX, context, "video/*");
13868
+ if (field.type === "media") return renderMediaField(field, indent, label, hintJSX, context);
13601
13869
  if (field.type === "gallery") return renderGalleryField(field, indent, label, hintJSX, context);
13602
13870
  if (field.type === "icon") return renderIconField(field, indent, label, hintJSX);
13603
13871
  if (field.type === "date") return renderDateField(field, indent, label, hintJSX, context);
@@ -13662,7 +13930,7 @@ ${indent} </FormItem>
13662
13930
  ${indent} )}
13663
13931
  ${indent}/>`;
13664
13932
  }
13665
- function renderMediaGalleryField(field, indent, label, hintJSX, context, accept) {
13933
+ function renderMediaField(field, indent, label, hintJSX, context, accept) {
13666
13934
  const acceptProp = accept ? `
13667
13935
  ${indent} accept="${accept}"` : "";
13668
13936
  const defaultValueFromHandler = defaultValueFromSourceHandler(field, context);
@@ -13677,7 +13945,7 @@ ${indent} render={({ field: formField }) => (
13677
13945
  ${indent} <FormItem${formItemProps2(field)}>
13678
13946
  ${indent} ${formLabelWithDescription(field, label, hintJSX, `${indent} `)}
13679
13947
  ${indent} <FormControl>
13680
- ${indent} <MediaGalleryField
13948
+ ${indent} <MediaField
13681
13949
  ${indent} value={formField.value}
13682
13950
  ${indent} onChange={${onChange}}
13683
13951
  ${indent} onBlur={formField.onBlur}
@@ -14398,6 +14666,23 @@ ${indent}/>`;
14398
14666
  }
14399
14667
 
14400
14668
  // adapters/next/generators/form/form-entity.ts
14669
+ function escapeRegExp(value) {
14670
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14671
+ }
14672
+ function usesIdentifier(source, identifier) {
14673
+ return new RegExp(`\\b${escapeRegExp(identifier)}\\b`).test(source);
14674
+ }
14675
+ function filterNamedImportsByUsage(importDeclarations, source) {
14676
+ return importDeclarations.flatMap((declaration) => {
14677
+ const match = declaration.match(/^import\s+\{\s*([\s\S]*?)\s*\}\s+from\s+(.+)$/);
14678
+ if (!match) return [declaration];
14679
+ const importedNames = match[1].split(",").map((name) => name.trim()).filter(Boolean);
14680
+ const usedNames = importedNames.filter((name) => usesIdentifier(source, name));
14681
+ if (usedNames.length === 0) return [];
14682
+ if (usedNames.length === importedNames.length) return [declaration];
14683
+ return [`import { ${usedNames.join(", ")} } from ${match[2]}`];
14684
+ });
14685
+ }
14401
14686
  function collectSidebarSectionHeadingFields(fields) {
14402
14687
  const headingFields = [];
14403
14688
  for (const field of fields) {
@@ -14558,10 +14843,25 @@ function ${slugValueHelperName}(value: unknown): string {
14558
14843
  slugValueHelperName: hasSlugTitleSource ? slugValueHelperName : void 0
14559
14844
  });
14560
14845
  const cardFieldIndent = " ";
14846
+ function hasRenderableTabsField(fields, excludedField) {
14847
+ for (const field of fields) {
14848
+ if (field.hidden) continue;
14849
+ if (field.type === "tabs" && field !== excludedField) return true;
14850
+ if ((field.type === "group" || field.type === "section") && field.fields) {
14851
+ if (hasRenderableTabsField(field.fields, excludedField)) return true;
14852
+ }
14853
+ if (field.type === "tabs" && field.tabs) {
14854
+ for (const tab of field.tabs) {
14855
+ if (hasRenderableTabsField(tab.fields ?? [], excludedField)) return true;
14856
+ }
14857
+ }
14858
+ }
14859
+ return false;
14860
+ }
14561
14861
  const renderNestedTabsField = (f, childContext = {}) => {
14562
14862
  const indent = cardFieldIndent;
14563
14863
  if (!f.tabs) return "";
14564
- const tabsList = f.tabs.map((t) => `${indent} <TabsTrigger value="${t.name}">${t.label}</TabsTrigger>`).join("\n");
14864
+ const tabsList = f.tabs.map((t) => renderTabsTrigger(t, `${indent} `)).join("\n");
14565
14865
  const tabsContent = f.tabs.map((t) => {
14566
14866
  const tabFields = (t.fields || []).map(
14567
14867
  (tf) => generateFieldJSX2(tf, `${indent} `, {
@@ -14649,7 +14949,7 @@ ${indent} </CardHeader>`;
14649
14949
  if (headingJSX) {
14650
14950
  return `${indent}<Card className="gap-0">
14651
14951
  ${headingJSX}
14652
- ${indent} <CardContent className="space-y-4 p-5">
14952
+ ${indent} <CardContent className="space-y-5 p-5">
14653
14953
  ${fieldsJSX}
14654
14954
  ${indent} </CardContent>
14655
14955
  ${indent}</Card>`;
@@ -14701,42 +15001,63 @@ ${cards.join("\n")}
14701
15001
  ).filter(Boolean).join("\n");
14702
15002
  return [commonMainFieldsJSX, tabFieldsJSX].filter(Boolean).join("\n");
14703
15003
  };
14704
- const renderTopLevelTabContent = (tab, commonMainFieldsJSX, commonSidebarEntries) => {
15004
+ const renderTopLevelTabFormBody = (tab, commonMainFieldsJSX, commonSidebarEntries) => {
14705
15005
  const tabMainFieldsJSX = renderTabMainFields(tab, commonMainFieldsJSX);
14706
15006
  const tabSidebarEntries = [
14707
15007
  ...commonSidebarEntries,
14708
15008
  ...collectSidebarFields(tab.fields || [], tab.name)
14709
15009
  ].map((entry) => ({ ...entry, jsx: renderStaticSidebarEntry(entry) })).filter((entry) => entry.jsx.length > 0);
14710
- const sidebarColumn = renderSidebarColumn(tabSidebarEntries, { scrollable: true });
14711
- return ` <TabsContent value="${tab.name}" className="min-h-0 w-full flex-1 overflow-hidden">
14712
- <div className="grid grid-cols-1 ${tabSidebarEntries.length > 0 ? "lg:grid-cols-[1fr_360px] " : ""}h-full min-h-0 gap-4 items-stretch">
14713
- <div className="h-full min-h-0 min-w-0 pb-4">
14714
- <Card className="relative min-h-0 min-w-0 space-y-4 overflow-y-auto p-8 gap-0 h-full">
15010
+ const sidebarColumn = renderSidebarColumn(tabSidebarEntries);
15011
+ return ` <div className="grid grid-cols-1 ${tabSidebarEntries.length > 0 ? "lg:grid-cols-[1fr_360px] " : ""}h-full min-h-0 gap-4 items-stretch">
15012
+ <div className="h-full min-h-0 min-w-0">
15013
+ <Card className="min-h-0 min-w-0 h-full">
15014
+ <CardContent className="space-y-5 gap-0 h-full min-h-0">
14715
15015
  ${tabMainFieldsJSX}
14716
- </Card>
14717
- </div>
15016
+ </CardContent>
15017
+ </Card>
15018
+ </div>
14718
15019
  ${sidebarColumn}
14719
- </div>
14720
- </TabsContent>`;
15020
+ </div>`;
14721
15021
  };
14722
- const renderTopLevelTabsLayout = (tabsField2, commonFields) => {
14723
- if (!tabsField2.tabs) return "";
15022
+ const topLevelTabComponentName = (tab) => `${Singular}FormTab${toPascalCase(tab.name)}`;
15023
+ const topLevelTabFileBaseName = (tab) => `${schema.name}-form-tab-${toKebabCase(tab.name)}`;
15024
+ const renderTopLevelTabComponentSpecs = (tabsField2, commonFields) => {
15025
+ if (!tabsField2.tabs) return [];
14724
15026
  const commonMainFieldsJSX = commonFields.map(renderMainField).filter(Boolean).join("\n");
14725
15027
  const commonSidebarEntries = collectSidebarFields(commonFields);
14726
- const tabsList = tabsField2.tabs.map((tab) => ` <TabsTrigger value="${tab.name}">${tab.label}</TabsTrigger>`).join("\n");
14727
- const tabsContent = tabsField2.tabs.map((tab) => renderTopLevelTabContent(tab, commonMainFieldsJSX, commonSidebarEntries)).join("\n");
14728
- return ` <Tabs value={activeTab} onValueChange={(value) => void setActiveTab(value as TabValue)} className="flex h-full min-h-0 w-full flex-col gap-4">
14729
- <TabsList className="sticky top-0 z-10 shrink-0 bg-background">
14730
- ${tabsList}
14731
- </TabsList>
14732
- ${tabsContent}
14733
- </Tabs>`;
15028
+ return tabsField2.tabs.map((tab) => ({
15029
+ tab,
15030
+ componentName: topLevelTabComponentName(tab),
15031
+ fileBaseName: topLevelTabFileBaseName(tab),
15032
+ body: renderTopLevelTabFormBody(tab, commonMainFieldsJSX, commonSidebarEntries)
15033
+ }));
14734
15034
  };
15035
+ const renderTopLevelTabComponentSwitch = (spec) => ` {tab === ${JSON.stringify(spec.tab.name)} ? <${spec.componentName} state={state} /> : null}`;
14735
15036
  const visibleFields = allFormFields.filter((f) => !(hasDraft && f.name === "published"));
14736
15037
  const topLevelTabsField = visibleFields.find(
14737
15038
  (field) => field.type === "tabs" && (field.slot ?? "main") === "main" && field.tabs?.length
14738
15039
  );
14739
15040
  const topLevelCommonFields = topLevelTabsField ? visibleFields.filter((field) => field !== topLevelTabsField) : visibleFields;
15041
+ const topLevelTabComponents = topLevelTabsField ? renderTopLevelTabComponentSpecs(topLevelTabsField, topLevelCommonFields) : [];
15042
+ const contentUsesTabsField = topLevelTabsField ? hasRenderableTabsField(topLevelCommonFields, topLevelTabsField) || topLevelTabsField.tabs?.some(
15043
+ (tab) => hasRenderableTabsField(tab.fields ?? [], topLevelTabsField)
15044
+ ) === true : hasTabsField;
15045
+ const topLevelTabChildFields = topLevelTabsField?.tabs?.flatMap((tab) => [
15046
+ ...tab.fields ?? [],
15047
+ ...tab.slot?.main?.fields ?? [],
15048
+ ...tab.slot?.sidebar?.fields ?? []
15049
+ ]) ?? [];
15050
+ const contentTabIconNames = collectTabIconNames(
15051
+ topLevelTabsField ? [...topLevelCommonFields, ...topLevelTabChildFields] : allFormFields
15052
+ );
15053
+ const topLevelTabIconNames = Array.from(
15054
+ new Set(
15055
+ topLevelTabsField?.tabs?.flatMap((tab) => {
15056
+ const icon = tab.icon?.trim();
15057
+ return icon ? [toPascalCase(icon)] : [];
15058
+ }) ?? []
15059
+ )
15060
+ );
14740
15061
  const mainFieldsJSX = topLevelCommonFields.map(renderMainField).filter(Boolean).join("\n");
14741
15062
  const sidebarEntries = collectSidebarFields(topLevelCommonFields).map((entry) => ({ ...entry, jsx: renderConditionalSidebarEntry(entry) })).filter((entry) => entry.jsx.length > 0);
14742
15063
  const hasSidebarFields = sidebarEntries.length > 0;
@@ -14759,10 +15080,10 @@ ${tabsContent}
14759
15080
  const sidebarCardJSX = !hasSidebarFields ? "" : shouldGuardSidebarByTab ? ` {hasVisibleSidebarFields ? (
14760
15081
  ${renderSidebarColumn(sidebarEntries, { conditionalTabs: true })}
14761
15082
  ) : null}` : renderSidebarColumn(sidebarEntries, { conditionalTabs: true });
14762
- const formBodyJSX = topLevelTabsField ? renderTopLevelTabsLayout(topLevelTabsField, topLevelCommonFields) : `${formGridOpen}
15083
+ const formBodyJSX = topLevelTabsField ? topLevelTabComponents.map(renderTopLevelTabComponentSwitch).join("\n") : `${formGridOpen}
14763
15084
  <div className="h-full min-w-0">
14764
15085
  <Card className="min-w-0 h-full">
14765
- <CardContent className="space-y-4 gap-0 h-full">
15086
+ <CardContent className="space-y-5 gap-0 h-full">
14766
15087
  ${mainFieldsJSX}
14767
15088
  </CardContent>
14768
15089
  </Card>
@@ -14850,7 +15171,7 @@ ${sidebarCardJSX}
14850
15171
  hasSelectCombobox,
14851
15172
  hasCreatableSelect,
14852
15173
  hasMultiSelect: multiSelectFields.length > 0,
14853
- hasTabsField,
15174
+ hasTabsField: contentUsesTabsField,
14854
15175
  hasRelationship,
14855
15176
  hasSimpleList,
14856
15177
  hasNestedList,
@@ -14868,7 +15189,7 @@ ${sidebarCardJSX}
14868
15189
  if (!lucideIcons.includes(icon)) lucideIcons.push(icon);
14869
15190
  }
14870
15191
  };
14871
- addLucideIcons(...sectionHeadingIconNames);
15192
+ addLucideIcons(...sectionHeadingIconNames, ...contentTabIconNames);
14872
15193
  if (hasRelationship || hasSelectCombobox) addLucideIcons("Check", "ChevronsUpDown");
14873
15194
  if (hasCreatableSelect) addLucideIcons("Plus");
14874
15195
  if (hasNestedList) {
@@ -15011,12 +15332,12 @@ ${sidebarCardJSX}
15011
15332
  "handleErrors"
15012
15333
  ]);
15013
15334
  const hookReturnObject = hookReturnNames.map((name) => ` ${name}`).join(",\n");
15014
- const contentUsesInitialData = hasDraft || hasSidebarFields && showMetadataWidget || topLevelTabsField && showMetadataWidget;
15015
- const contentStateNames = uniqueNames([
15335
+ const contentUsesInitialData = hasDraft || !topLevelTabsField && hasSidebarFields && showMetadataWidget;
15336
+ const standardContentStateNames = uniqueNames([
15016
15337
  ...contentUsesInitialData ? ["initialData"] : [],
15017
15338
  "form",
15018
15339
  ...contentUsesPendingState ? ["isPending"] : [],
15019
- ...hasTabsField ? ["activeTab", "setActiveTab"] : [],
15340
+ ...contentUsesTabsField ? ["activeTab", "setActiveTab"] : [],
15020
15341
  ...relationshipHookReturnNames,
15021
15342
  ...creatableSelectReturnNames,
15022
15343
  ...staticMultiSelectReturnNames,
@@ -15025,8 +15346,15 @@ ${sidebarCardJSX}
15025
15346
  "onSubmit",
15026
15347
  "handleErrors"
15027
15348
  ]);
15349
+ const topLevelTabRouterStateNames = uniqueNames([
15350
+ ...hasDraft ? ["initialData"] : [],
15351
+ "form",
15352
+ "onSubmit",
15353
+ "handleErrors"
15354
+ ]);
15355
+ const contentStateNames = topLevelTabsField ? topLevelTabRouterStateNames : standardContentStateNames;
15028
15356
  const contentStateDestructure = contentStateNames.map((name) => ` ${name}`).join(",\n");
15029
- const needsCn = hasRelationship || hasSelectCombobox || shouldGuardSidebarByTab || Boolean(topLevelTabsField && showMetadataWidget);
15357
+ const needsCn = hasRelationship || hasSelectCombobox || shouldGuardSidebarByTab;
15030
15358
  const metadataOffsetConst = showMetadataWidget ? "const SCROLL_BORDER_OFFSET = 8;\n\n" : "";
15031
15359
  const tabParserConst = hasTabsField ? `const TAB_VALUES = [${tabNames.map((name) => JSON.stringify(name)).join(", ")}] as const
15032
15360
  const TAB_PARSER = parseAsStringLiteral(TAB_VALUES).withDefault(TAB_VALUES[0])
@@ -15058,6 +15386,32 @@ export type TabValue = (typeof TAB_VALUES)[number]
15058
15386
  )}
15059
15387
 
15060
15388
  ` : "";
15389
+ const topLevelTabsClassName = showMetadataWidget ? `className={cn('min-h-[calc(100svh-56px)] w-full gap-0', {
15390
+ 'min-h-[calc(100svh-56px-56px)]': initialData?.id != null
15391
+ })}` : 'className="h-[calc(100svh-56px)] min-h-0 w-full gap-0"';
15392
+ const topLevelTabsShell = topLevelTabsField?.tabs ? ` <Tabs value={entityForm.activeTab} onValueChange={(value) => void entityForm.setActiveTab(value as TabValue)} ${topLevelTabsClassName}>
15393
+ <div className="flex h-14 shrink-0 w-full bg-background border-b border-border/80 px-4 items-center">
15394
+ <TabsList className="bg-transparent border-none">
15395
+ ${topLevelTabsField.tabs.map((tab) => renderTabsTrigger(tab, " ", { className: "px-3" })).join("\n")}
15396
+ </TabsList>
15397
+ </div>
15398
+
15399
+ <main className="w-full flex-1 min-h-0 p-4">
15400
+ ${topLevelTabsField.tabs.map(
15401
+ (tab) => ` <TabsContent value="${tab.name}" className="h-full min-h-0 w-full flex-1">
15402
+ <Form {...entityForm.form}>
15403
+ <${contentComponentName} state={entityForm} tab="${tab.name}" />
15404
+ </Form>
15405
+ </TabsContent>`
15406
+ ).join("\n")}
15407
+ </main>
15408
+ </Tabs>` : "";
15409
+ const defaultShellBody = ` <main className="w-full p-4 h-full">
15410
+ <Form {...entityForm.form}>
15411
+ <${contentComponentName} state={entityForm} />
15412
+ </Form>
15413
+ </main>`;
15414
+ const shellBody = topLevelTabsField ? topLevelTabsShell : defaultShellBody;
15061
15415
  const hookContent = `'use client'
15062
15416
 
15063
15417
  import * as React from 'react'
@@ -15177,27 +15531,63 @@ ${hookReturnObject}
15177
15531
  }
15178
15532
  }
15179
15533
  `;
15534
+ const topLevelTabComponentImports = topLevelTabComponents.map((spec) => `import { ${spec.componentName} } from './${spec.fileBaseName}'`).join("\n");
15535
+ const contentImports = topLevelTabsField ? `import type { TabValue, ${hookName} } from '${hookImportPath}'${topLevelTabComponentImports ? `
15536
+ ${topLevelTabComponentImports}` : ""}` : `${lucideIcons.length > 0 ? `import { ${lucideIcons.join(", ")} } from 'lucide-react'
15537
+ ` : ""}${hasTabsField ? `import type { TabValue, ${hookName} } from '${hookImportPath}'` : `import type { ${hookName} } from '${hookImportPath}'`}${needsCn ? "\nimport { cn } from '@admin/utils/shared/cn'" : ""}
15538
+ ${uiImports.join("\n")}`;
15539
+ const contentSidebarVisibilityState = topLevelTabsField ? "" : sidebarVisibilityState;
15540
+ const renderTopLevelTabComponentFile = (spec) => {
15541
+ const body = spec.body;
15542
+ const tabStateNames = hookReturnNames.filter((name) => usesIdentifier(body, name));
15543
+ const tabStateDestructure = tabStateNames.length > 0 ? ` const {
15544
+ ${tabStateNames.map((name) => ` ${name}`).join(",\n")}
15545
+ } = state
15546
+
15547
+ ` : "";
15548
+ const tabLucideIcons = lucideIcons.filter((icon) => usesIdentifier(body, icon));
15549
+ const tabNeedsCn = /\bcn\s*\(/.test(body);
15550
+ const tabImports = [
15551
+ tabLucideIcons.length > 0 ? `import { ${tabLucideIcons.join(", ")} } from 'lucide-react'` : "",
15552
+ `import type { ${hookName} } from '${hookImportPath}'`,
15553
+ tabNeedsCn ? "import { cn } from '@admin/utils/shared/cn'" : "",
15554
+ ...filterNamedImportsByUsage(uiImports, body)
15555
+ ].filter(Boolean).join("\n");
15556
+ return createGeneratedFile(
15557
+ `${pagesDir}/${schema.name}/${spec.fileBaseName}.tsx`,
15558
+ `'use client'
15559
+
15560
+ ${tabImports}
15561
+
15562
+ interface ${spec.componentName}Props {
15563
+ state: ReturnType<typeof ${hookName}>
15564
+ }
15565
+
15566
+ export function ${spec.componentName}({ state }: ${spec.componentName}Props) {
15567
+ ${tabStateDestructure} return (
15568
+ ${body}
15569
+ )
15570
+ }
15571
+ `
15572
+ );
15573
+ };
15574
+ const topLevelTabComponentFiles = topLevelTabComponents.map(renderTopLevelTabComponentFile);
15180
15575
  const content = `'use client'
15181
- ${lucideIcons.length > 0 ? `
15182
- import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}${hasTabsField ? `
15183
- import type { TabValue, ${hookName} } from '${hookImportPath}'` : `
15184
- import type { ${hookName} } from '${hookImportPath}'`}${needsCn ? "\nimport { cn } from '@admin/utils/shared/cn'" : ""}
15185
- ${uiImports.join("\n")}
15576
+
15577
+ ${contentImports}
15186
15578
 
15187
15579
  interface ${contentComponentName}Props {
15188
15580
  state: ReturnType<typeof ${hookName}>
15581
+ ${topLevelTabsField ? " tab: TabValue\n" : ""}
15189
15582
  }
15190
15583
 
15191
- export function ${contentComponentName}({ state }: ${contentComponentName}Props) {
15584
+ export function ${contentComponentName}({ state${topLevelTabsField ? ", tab" : ""} }: ${contentComponentName}Props) {
15192
15585
  const {
15193
15586
  ${contentStateDestructure}
15194
15587
  } = state
15195
- ${sidebarVisibilityState}
15588
+ ${contentSidebarVisibilityState}
15196
15589
  return (
15197
- <form id="${schema.name}-form" onSubmit={form.handleSubmit((values) => onSubmit(values${hasDraft ? ", initialData?.published ?? false" : ""}), handleErrors)} ${topLevelTabsField ? showMetadataWidget ? `className={cn('min-h-0', {
15198
- 'h-[calc(100svh-56px-56px-32px)]': initialData?.id != null,
15199
- 'h-[calc(100svh-56px-32px)]': initialData?.id == null
15200
- })}` : 'className="h-[calc(100svh-56px-32px)] min-h-0"' : 'className="space-y-6 h-full"'}>
15590
+ <form id="${schema.name}-form" onSubmit={form.handleSubmit((values) => onSubmit(values${hasDraft ? ", initialData?.published ?? false" : ""}), handleErrors)} ${topLevelTabsField ? 'className="h-full"' : 'className="space-y-6 h-full"'}>
15201
15591
  ${formBodyJSX}
15202
15592
  </form>
15203
15593
  )
@@ -15205,11 +15595,15 @@ ${formBodyJSX}
15205
15595
  `;
15206
15596
  const shellContent = `'use client'
15207
15597
  ${showMetadataWidget ? "\nimport { usePageScrollThreshold } from '@admin/hooks/use-page-scroll-threshold'" : ""}
15598
+ ${topLevelTabIconNames.length > 0 ? `import { ${topLevelTabIconNames.join(", ")} } from 'lucide-react'
15599
+ ` : ""}
15208
15600
  import { Button } from '@admin/components/ui/button'
15209
15601
  import { Form } from '@admin/components/ui/form'
15210
- import { PageHeader } from '@admin/components/shared/page-header'${showMetadataWidget ? "\nimport { EntityMetadata } from '@admin/components/shared/entity-metadata'\nimport { cn } from '@admin/utils/shared/cn'" : ""}
15602
+ ${topLevelTabsField ? "import { Tabs, TabsContent, TabsList, TabsTrigger } from '@admin/components/ui/tabs'\n" : ""}import { PageHeader } from '@admin/components/shared/page-header'${showMetadataWidget ? "\nimport { EntityMetadata } from '@admin/components/shared/entity-metadata'\nimport { cn } from '@admin/utils/shared/cn'" : ""}
15211
15603
  import type { ${Singular} } from '@admin/actions/${actionImportPath}'
15212
15604
  ${showMetadataWidget ? `import { getVersionsBy${Singular}Id, restore${Singular}Version } from '@admin/actions/${actionImportPath}'` : ""}
15605
+ ${topLevelTabsField ? `import type { TabValue } from '${hookImportPath}'
15606
+ ` : ""}
15213
15607
  import { ${hookName} } from '${hookImportPath}'
15214
15608
  import { ${contentComponentName} } from './${schema.name}-form-content'
15215
15609
 
@@ -15257,11 +15651,7 @@ ${hasDraft ? ` <div className="flex items-center gap-1">
15257
15651
  </Button>`}
15258
15652
  }
15259
15653
  />
15260
- ${metadataStrip} <main className="w-full p-4 h-full">
15261
- <Form {...entityForm.form}>
15262
- <${contentComponentName} state={entityForm} />
15263
- </Form>
15264
- </main>
15654
+ ${metadataStrip}${shellBody}
15265
15655
  </>
15266
15656
  )
15267
15657
  }
@@ -15270,6 +15660,7 @@ ${metadataStrip} <main className="w-full p-4 h-full">
15270
15660
  files: [
15271
15661
  createGeneratedFile(hookPath, hookContent),
15272
15662
  createGeneratedFile(`${pagesDir}/${schema.name}/${schema.name}-form-content.tsx`, content),
15663
+ ...topLevelTabComponentFiles,
15273
15664
  createGeneratedFile(`${pagesDir}/${schema.name}/${schema.name}-form.tsx`, shellContent)
15274
15665
  ]
15275
15666
  };
@@ -15398,7 +15789,7 @@ function buildGroupFieldsJSX(group2, analysis, defaultValueFromSourceNames, crea
15398
15789
  const indent = " ";
15399
15790
  return group2.fields.map((f) => {
15400
15791
  if (f.type === "tabs" && f.tabs) {
15401
- const tabsList = f.tabs.map((t) => ` <TabsTrigger value="${t.name}">${t.label}</TabsTrigger>`).join("\n");
15792
+ const tabsList = f.tabs.map((t) => renderTabsTrigger(t, " ")).join("\n");
15402
15793
  const tabsContent = f.tabs.map((t) => {
15403
15794
  const tabFields = (t.fields || []).map(
15404
15795
  (tf) => generateFieldJSX2(tf, " ", {
@@ -15621,7 +16012,7 @@ ${creatableSelectHandlers ? `${creatableSelectHandlers}
15621
16012
  <CardHeader>
15622
16013
  <CardTitle>${group2.title}</CardTitle>${descriptionLine}
15623
16014
  </CardHeader>
15624
- <CardContent className="space-y-6">
16015
+ <CardContent className="space-y-5">
15625
16016
  ${fieldsJSX}
15626
16017
  </CardContent>
15627
16018
  <CardFooter>${group2.hint ? `
@@ -15702,6 +16093,7 @@ function generateSingleForm(schema, pagesDir, options = {}) {
15702
16093
  (group2) => analyzeGroup(group2.fields).hasNestedObjectList
15703
16094
  );
15704
16095
  const hasSimpleList = hasList && hasSimpleListField2(allFormFields);
16096
+ const tabIconNames = collectTabIconNames(allFormFields);
15705
16097
  const allRelFields = [];
15706
16098
  for (const g of cardGroups) {
15707
16099
  const a = analyzeGroup(g.fields);
@@ -15747,12 +16139,17 @@ function generateSingleForm(schema, pagesDir, options = {}) {
15747
16139
  CardHeader,
15748
16140
  CardTitle
15749
16141
  } from '@admin/components/ui/card'`);
15750
- const lucideIcons = ["LoaderCircle"];
15751
- if (hasRelationship || hasSelectCombobox) lucideIcons.push("Check", "ChevronsUpDown");
15752
- if (hasCreatableSelect) lucideIcons.push("Plus");
16142
+ const lucideIcons = [];
16143
+ const addLucideIcons = (...icons) => {
16144
+ for (const icon of icons) {
16145
+ if (!lucideIcons.includes(icon)) lucideIcons.push(icon);
16146
+ }
16147
+ };
16148
+ addLucideIcons("LoaderCircle", ...tabIconNames);
16149
+ if (hasRelationship || hasSelectCombobox) addLucideIcons("Check", "ChevronsUpDown");
16150
+ if (hasCreatableSelect) addLucideIcons("Plus");
15753
16151
  if (hasNestedList) {
15754
- if (!lucideIcons.includes("Plus")) lucideIcons.push("Plus");
15755
- if (!lucideIcons.includes("X")) lucideIcons.push("X");
16152
+ addLucideIcons("Plus", "X");
15756
16153
  }
15757
16154
  const relHookImports = allRelFields.map((f) => {
15758
16155
  const relPlural = toPascalCase(pluralize(f.relationship || ""));
@@ -18529,6 +18926,7 @@ import fs24 from "fs";
18529
18926
  import path29 from "path";
18530
18927
  var ENTITY_SINGLE_ADD_FIELD_TYPES = [
18531
18928
  "string",
18929
+ "email",
18532
18930
  "varchar",
18533
18931
  "text",
18534
18932
  "markdown",
@@ -18631,6 +19029,11 @@ function loadAuthoredGeneratedSchema(schemasDir, name) {
18631
19029
  const filePath = filePaths[0];
18632
19030
  const parsed = parseJsonObject(fs24.readFileSync(filePath, "utf-8"), filePath);
18633
19031
  const type = parsed.type;
19032
+ if (type === void 0) {
19033
+ throw new Error(
19034
+ `Schema "${name}" is missing required top-level "type". Expected "entity", "single", or "form".`
19035
+ );
19036
+ }
18634
19037
  if (type === "form") {
18635
19038
  return {
18636
19039
  kind: "form",
@@ -20305,7 +20708,7 @@ async function promptEntityIntegrationOptions(schema, field) {
20305
20708
  summary.push(`table column: ${field.name}`);
20306
20709
  }
20307
20710
  }
20308
- if (["string", "varchar", "text", "markdown", "richtext"].includes(field.type)) {
20711
+ if (["string", "email", "varchar", "text", "markdown", "richtext"].includes(field.type)) {
20309
20712
  const addSearch = await promptConfirm2("Add this field to search?", false);
20310
20713
  if (addSearch) {
20311
20714
  schema.search = schema.search ?? { fields: [] };
@@ -20365,6 +20768,8 @@ function schemaColumnTypeForField(field) {
20365
20768
  return "boolean";
20366
20769
  case "select":
20367
20770
  return "badge";
20771
+ case "email":
20772
+ return "email";
20368
20773
  case "string":
20369
20774
  case "varchar":
20370
20775
  case "text":
@@ -24801,7 +25206,7 @@ var STATIC_CUSTOM_DEPENDENCIES = {
24801
25206
  "content-editor-rich-extensions",
24802
25207
  "content-editor-editor-view-utils",
24803
25208
  "markdown-utils",
24804
- "media-gallery-field",
25209
+ "media-field",
24805
25210
  "use-content-editor",
24806
25211
  "use-content-editor-table-add-controls",
24807
25212
  "use-content-editor-source-mode",
@@ -24891,7 +25296,7 @@ var STATIC_CUSTOM_DEPENDENCIES = {
24891
25296
  "content-editor/use-tiptap-editor",
24892
25297
  "tiptap-utils"
24893
25298
  ],
24894
- "content-editor/media-gallery-block": ["media-gallery-field"],
25299
+ "content-editor/media-gallery-block": ["media-field"],
24895
25300
  "content-editor/mobile-toolbar-content": [
24896
25301
  "content-editor/math-popover",
24897
25302
  "content-editor/use-tiptap-editor",
@@ -24916,20 +25321,25 @@ var STATIC_CUSTOM_DEPENDENCIES = {
24916
25321
  ],
24917
25322
  "date-range-picker": ["date-utils"],
24918
25323
  "gallery-field": [
24919
- "gallery-thumbnail",
25324
+ "card",
25325
+ "carousel",
25326
+ "media-field",
25327
+ "media-field-empty-state",
24920
25328
  "media-gallery-dialog",
25329
+ "skeleton",
24921
25330
  "sortable-gallery-item",
24922
25331
  "use-media"
24923
25332
  ],
24924
25333
  "icon-picker": ["icons-column-skeleton", "icons-data"],
24925
- "sortable-gallery-item": ["gallery-thumbnail"],
24926
- "media-gallery-field": [
25334
+ "media-field": [
25335
+ "media-field-empty-state",
24927
25336
  "media-gallery-dialog",
24928
25337
  "media-preview",
24929
25338
  "media-fallback-utils",
24930
25339
  "use-copy-to-clipboard",
24931
25340
  "use-media"
24932
25341
  ],
25342
+ "media-field-empty-state": ["button", "card", "cn", "media-gallery-dialog"],
24933
25343
  "nested-object-list-field": ["form-list-field-utils"],
24934
25344
  "progressive-blur": ["use-page-boundary-blur-visibility"],
24935
25345
  "upload-dropzone": ["upload-utils"]
@@ -24962,7 +25372,7 @@ var TIPTAP_TEMPLATE_DEPENDENCIES = [
24962
25372
  "content-editor/source-mode",
24963
25373
  "tiptap-utils",
24964
25374
  "content-editor/media-gallery-block",
24965
- "media-gallery-field"
25375
+ "media-field"
24966
25376
  ];
24967
25377
  var TIPTAP_CONTENT_EDITOR_DIRECTORIES = ["tiptap-extension", "tiptap-node", "tiptap-ui"];
24968
25378
  var CODEMIRROR_PACKAGE_DEPS = [
@@ -24970,6 +25380,7 @@ var CODEMIRROR_PACKAGE_DEPS = [
24970
25380
  "@codemirror/lang-javascript",
24971
25381
  "@codemirror/lang-json",
24972
25382
  "@codemirror/lang-markdown",
25383
+ "@codemirror/state",
24973
25384
  "@codemirror/view",
24974
25385
  "@uiw/codemirror-theme-github",
24975
25386
  "@uiw/react-codemirror"
@@ -25371,7 +25782,7 @@ var TEMPLATE_REGISTRY = {
25371
25782
  relPath: "hooks/content-editor/use-content-editor-source-mode.tsx",
25372
25783
  content: () => readTemplate("hooks/content-editor/use-content-editor-source-mode.tsx"),
25373
25784
  dependencies: [
25374
- "media-gallery-field",
25785
+ "media-field",
25375
25786
  "use-admin-theme",
25376
25787
  "markdown-utils",
25377
25788
  "content-editor-source-media-utils",
@@ -25392,7 +25803,7 @@ var TEMPLATE_REGISTRY = {
25392
25803
  "tiptap",
25393
25804
  "tiptap-utils",
25394
25805
  "content-editor/media-gallery-block",
25395
- "media-gallery-field"
25806
+ "media-field"
25396
25807
  ]
25397
25808
  },
25398
25809
  "use-content-editor-media-insertion": {