@svadmin/create 0.26.2 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The official scaffolding CLI for `headless-admin-svelte` (svadmin).
4
4
 
5
- Quickly bootstrap a completely configured, headless admin panel project built on Svelte 5, Shadcn Svelte, and TanStack Query.
5
+ Quickly bootstrap a completely configured, headless admin panel project built on Svelte 5, the native-CSS `@svadmin/ui` components, and TanStack Query.
6
6
 
7
7
  ## Quick Start
8
8
 
@@ -21,7 +21,7 @@ Follow the interactive prompts to:
21
21
 
22
22
  The generated project is pre-configured with:
23
23
  - **Svelte 5** + **Vite**
24
- - **Tailwind CSS** + **Shadcn Svelte** UI components
24
+ - **@svadmin/ui** native CSS, semantic tokens and Bits UI interactions; no host CSS compiler is required
25
25
  - **@svadmin/core**: The headless business logic and hooks (useTable, useForm, useAuth, etc.)
26
26
  - **@svadmin/ai-elements**: Composable Svelte 5 AI conversation, tool, reasoning, and source components.
27
27
  - **@svadmin/ui**: Beautiful default dashboard UI, standalone CRUD buttons, and data tables.
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  return cached;
20
20
  }
21
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
22
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ const to = isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
23
  if (mod && typeof mod === "object" || typeof mod === "function") {
24
24
  for (let key of __getOwnPropNames(mod))
25
25
  if (!__hasOwnProp.call(to, key))
@@ -259,7 +259,7 @@ var require_clear = __commonJS(function(exports, module) {
259
259
  if (it)
260
260
  o = it;
261
261
  var i = 0;
262
- var F = function F2() {};
262
+ var F = function F() {};
263
263
  return { s: F, n: function n() {
264
264
  if (i >= o.length)
265
265
  return { done: true };
@@ -876,7 +876,7 @@ var require_select = __commonJS(function(exports, module) {
876
876
  else
877
877
  this.out.write(clear(this.outputText, this.out.columns));
878
878
  super.render();
879
- let _entriesToDisplay = entriesToDisplay(this.cursor, this.choices.length, this.optionsPerPage), startIndex = _entriesToDisplay.startIndex, endIndex = _entriesToDisplay.endIndex;
879
+ let _entriesToDisplay = entriesToDisplay(this.cursor, this.choices.length, this.optionsPerPage), { startIndex, endIndex } = _entriesToDisplay;
880
880
  this.outputText = [style.symbol(this.done, this.aborted), color.bold(this.msg), style.delimiter(false), this.done ? this.selection.title : this.selection.disabled ? color.yellow(this.warn) : color.gray(this.hint)].join(" ");
881
881
  if (!this.done) {
882
882
  this.outputText += `
@@ -1885,14 +1885,14 @@ Instructions:
1885
1885
  }
1886
1886
  return "";
1887
1887
  }
1888
- renderOption(cursor2, v, i, arrowIndicator) {
1888
+ renderOption(cursor, v, i, arrowIndicator) {
1889
1889
  const prefix = (v.selected ? color.green(figures.radioOn) : figures.radioOff) + " " + arrowIndicator + " ";
1890
1890
  let title, desc;
1891
1891
  if (v.disabled) {
1892
- title = cursor2 === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
1892
+ title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
1893
1893
  } else {
1894
- title = cursor2 === i ? color.cyan().underline(v.title) : v.title;
1895
- if (cursor2 === i && v.description) {
1894
+ title = cursor === i ? color.cyan().underline(v.title) : v.title;
1895
+ if (cursor === i && v.description) {
1896
1896
  desc = ` - ${v.description}`;
1897
1897
  if (prefix.length + title.length + desc.length >= this.out.columns || v.description.split(/\r?\n/).length > 1) {
1898
1898
  desc = `
@@ -1909,7 +1909,7 @@ Instructions:
1909
1909
  if (options.length === 0) {
1910
1910
  return color.red("No matches for this query.");
1911
1911
  }
1912
- let _entriesToDisplay = entriesToDisplay(this.cursor, options.length, this.optionsPerPage), startIndex = _entriesToDisplay.startIndex, endIndex = _entriesToDisplay.endIndex;
1912
+ let _entriesToDisplay = entriesToDisplay(this.cursor, options.length, this.optionsPerPage), { startIndex, endIndex } = _entriesToDisplay;
1913
1913
  let prefix, styledOptions = [];
1914
1914
  for (let i = startIndex;i < endIndex; i++) {
1915
1915
  if (i === startIndex && startIndex > 0) {
@@ -2217,7 +2217,7 @@ var require_autocomplete = __commonJS(function(exports, module) {
2217
2217
  else
2218
2218
  this.out.write(clear(this.outputText, this.out.columns));
2219
2219
  super.render();
2220
- let _entriesToDisplay = entriesToDisplay(this.select, this.choices.length, this.limit), startIndex = _entriesToDisplay.startIndex, endIndex = _entriesToDisplay.endIndex;
2220
+ let _entriesToDisplay = entriesToDisplay(this.select, this.choices.length, this.limit), { startIndex, endIndex } = _entriesToDisplay;
2221
2221
  this.outputText = [style.symbol(this.done, this.aborted, this.exited), color.bold(this.msg), style.delimiter(this.completing), this.done && this.suggestions[this.select] ? this.suggestions[this.select].title : this.rendered = this.transform.render(this.input)].join(" ");
2222
2222
  if (!this.done) {
2223
2223
  const suggestions = this.suggestions.slice(startIndex, endIndex).map((item, i) => this.renderOption(item, this.select === i + startIndex, i === 0 && startIndex > 0, i + startIndex === endIndex - 1 && endIndex < this.choices.length)).join(`
@@ -2356,12 +2356,12 @@ Instructions:
2356
2356
  Filtered results for: ${this.inputValue ? this.inputValue : color.gray("Enter something to filter")}
2357
2357
  `;
2358
2358
  }
2359
- renderOption(cursor2, v, i) {
2359
+ renderOption(cursor, v, i) {
2360
2360
  let title;
2361
2361
  if (v.disabled)
2362
- title = cursor2 === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
2362
+ title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
2363
2363
  else
2364
- title = cursor2 === i ? color.cyan().underline(v.title) : v.title;
2364
+ title = cursor === i ? color.cyan().underline(v.title) : v.title;
2365
2365
  return (v.selected ? color.green(figures.radioOn) : figures.radioOff) + " " + title;
2366
2366
  }
2367
2367
  renderDoneOrInstructions() {
@@ -2591,7 +2591,7 @@ var require_dist = __commonJS(function(exports, module) {
2591
2591
  if (it)
2592
2592
  o = it;
2593
2593
  var i = 0;
2594
- var F = function F2() {};
2594
+ var F = function F() {};
2595
2595
  return { s: F, n: function n() {
2596
2596
  if (i >= o.length)
2597
2597
  return { done: true };
@@ -2684,17 +2684,17 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
2684
2684
  onCancel = noop
2685
2685
  } = {}) {
2686
2686
  const answers = {};
2687
- const override2 = prompt._override || {};
2687
+ const override = prompt._override || {};
2688
2688
  questions = [].concat(questions);
2689
2689
  let answer, question, quit, name, type, lastPrompt;
2690
2690
  const getFormattedAnswer = /* @__PURE__ */ function() {
2691
- var _ref = _asyncToGenerator(function* (question2, answer2, skipValidation = false) {
2692
- if (!skipValidation && question2.validate && question2.validate(answer2) !== true) {
2691
+ var _ref = _asyncToGenerator(function* (question, answer, skipValidation = false) {
2692
+ if (!skipValidation && question.validate && question.validate(answer) !== true) {
2693
2693
  return;
2694
2694
  }
2695
- return question2.format ? yield question2.format(answer2, answers) : answer2;
2695
+ return question.format ? yield question.format(answer, answers) : answer;
2696
2696
  });
2697
- return function getFormattedAnswer2(_x, _x2) {
2697
+ return function getFormattedAnswer(_x, _x2) {
2698
2698
  return _ref.apply(this, arguments);
2699
2699
  };
2700
2700
  }();
@@ -2727,8 +2727,8 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
2727
2727
  if (prompts[type] === undefined) {
2728
2728
  throw new Error(`prompt type (${type}) is not defined`);
2729
2729
  }
2730
- if (override2[question.name] !== undefined) {
2731
- answer = yield getFormattedAnswer(question, override2[question.name]);
2730
+ if (override[question.name] !== undefined) {
2731
+ answer = yield getFormattedAnswer(question, override[question.name]);
2732
2732
  if (answer !== undefined) {
2733
2733
  answers[name] = answer;
2734
2734
  continue;
@@ -4870,14 +4870,14 @@ var require_lib = __commonJS(function(exports, module) {
4870
4870
  var noop = () => {};
4871
4871
  async function prompt(questions = [], { onSubmit = noop, onCancel = noop } = {}) {
4872
4872
  const answers = {};
4873
- const override2 = prompt._override || {};
4873
+ const override = prompt._override || {};
4874
4874
  questions = [].concat(questions);
4875
4875
  let answer, question, quit, name, type, lastPrompt;
4876
- const getFormattedAnswer = async (question2, answer2, skipValidation = false) => {
4877
- if (!skipValidation && question2.validate && question2.validate(answer2) !== true) {
4876
+ const getFormattedAnswer = async (question, answer, skipValidation = false) => {
4877
+ if (!skipValidation && question.validate && question.validate(answer) !== true) {
4878
4878
  return;
4879
4879
  }
4880
- return question2.format ? await question2.format(answer2, answers) : answer2;
4880
+ return question.format ? await question.format(answer, answers) : answer;
4881
4881
  };
4882
4882
  for (question of questions) {
4883
4883
  ({ name, type } = question);
@@ -4901,8 +4901,8 @@ var require_lib = __commonJS(function(exports, module) {
4901
4901
  if (prompts[type] === undefined) {
4902
4902
  throw new Error(`prompt type (${type}) is not defined`);
4903
4903
  }
4904
- if (override2[question.name] !== undefined) {
4905
- answer = await getFormattedAnswer(question, override2[question.name]);
4904
+ if (override[question.name] !== undefined) {
4905
+ answer = await getFormattedAnswer(question, override[question.name]);
4906
4906
  if (answer !== undefined) {
4907
4907
  answers[name] = answer;
4908
4908
  continue;
@@ -5075,22 +5075,22 @@ function assertReferencedPacksExist(dependencyPacks, selectedPacks) {
5075
5075
  }
5076
5076
  function assertScaffoldManifest(candidate) {
5077
5077
  assertJsonObject(candidate, "scaffold manifest");
5078
- assertNonEmptyString(candidate.name, "scaffold manifest.name");
5079
- assertNonEmptyString(candidate.version, "scaffold manifest.version");
5080
- if (candidate.private !== true) {
5078
+ assertNonEmptyString(candidate["name"], "scaffold manifest.name");
5079
+ assertNonEmptyString(candidate["version"], "scaffold manifest.version");
5080
+ if (candidate["private"] !== true) {
5081
5081
  throw new Error("scaffold manifest.private must be true");
5082
5082
  }
5083
- assertNonEmptyString(candidate.type, "scaffold manifest.type");
5084
- assertStringRecord(candidate.scripts, "scaffold manifest.scripts");
5085
- assertStringRecord(candidate.dependencies, "scaffold manifest.dependencies");
5086
- assertStringRecord(candidate.devDependencies, "scaffold manifest.devDependencies");
5087
- assertJsonObject(candidate.svadmin, "scaffold manifest.svadmin");
5088
- assertDependencyPacks(candidate.svadmin.dependencyPacks);
5089
- assertSelectionMap(candidate.svadmin.dataProviders, DATA_PROVIDER_CHOICES, "scaffold manifest.svadmin.dataProviders");
5090
- assertSelectionMap(candidate.svadmin.authProviders, AUTH_PROVIDER_CHOICES, "scaffold manifest.svadmin.authProviders");
5091
- assertReferencedPacksExist(candidate.svadmin.dependencyPacks, [
5092
- ...Object.values(candidate.svadmin.dataProviders).flat(),
5093
- ...Object.values(candidate.svadmin.authProviders).flat()
5083
+ assertNonEmptyString(candidate["type"], "scaffold manifest.type");
5084
+ assertStringRecord(candidate["scripts"], "scaffold manifest.scripts");
5085
+ assertStringRecord(candidate["dependencies"], "scaffold manifest.dependencies");
5086
+ assertStringRecord(candidate["devDependencies"], "scaffold manifest.devDependencies");
5087
+ assertJsonObject(candidate["svadmin"], "scaffold manifest.svadmin");
5088
+ assertDependencyPacks(candidate["svadmin"]["dependencyPacks"]);
5089
+ assertSelectionMap(candidate["svadmin"]["dataProviders"], DATA_PROVIDER_CHOICES, "scaffold manifest.svadmin.dataProviders");
5090
+ assertSelectionMap(candidate["svadmin"]["authProviders"], AUTH_PROVIDER_CHOICES, "scaffold manifest.svadmin.authProviders");
5091
+ assertReferencedPacksExist(candidate["svadmin"]["dependencyPacks"], [
5092
+ ...Object.values(candidate["svadmin"]["dataProviders"]).flat(),
5093
+ ...Object.values(candidate["svadmin"]["authProviders"]).flat()
5094
5094
  ]);
5095
5095
  }
5096
5096
  function loadScaffoldManifest(manifestPath) {
@@ -5114,7 +5114,10 @@ function createProjectPackageJson(scaffold, options) {
5114
5114
  ...scaffold.svadmin.authProviders[options.authProvider]
5115
5115
  ]);
5116
5116
  for (const packName of selectedPacks) {
5117
- mergeDependencyPack(dependencies, scaffold.svadmin.dependencyPacks[packName], packName);
5117
+ const pack = scaffold.svadmin.dependencyPacks[packName];
5118
+ if (pack === undefined)
5119
+ throw new Error(`Unknown dependency pack: ${packName}`);
5120
+ mergeDependencyPack(dependencies, pack, packName);
5118
5121
  }
5119
5122
  return {
5120
5123
  name: options.projectName.trim(),
@@ -5431,8 +5434,10 @@ function parseGraphQLSDL(sdl) {
5431
5434
  const enumRegex = /enum\s+([A-Za-z0-9_]+)\s*\{([^}]+)\}/g;
5432
5435
  let match;
5433
5436
  while ((match = enumRegex.exec(cleanSDL)) !== null) {
5434
- const enumName = match[1];
5435
- const rawValues = match[2].split(/\s+/).filter(Boolean);
5437
+ const [, enumName, enumBody] = match;
5438
+ if (enumName === undefined || enumBody === undefined)
5439
+ continue;
5440
+ const rawValues = enumBody.split(/\s+/).filter(Boolean);
5436
5441
  types.push({
5437
5442
  kind: "ENUM",
5438
5443
  name: enumName,
@@ -5441,8 +5446,9 @@ function parseGraphQLSDL(sdl) {
5441
5446
  }
5442
5447
  const typeRegex = /type\s+([A-Za-z0-9_]+)\s*(?:implements\s+[A-Za-z0-9_&,\s]+)?\s*\{([^}]+)\}/g;
5443
5448
  while ((match = typeRegex.exec(cleanSDL)) !== null) {
5444
- const typeName = match[1];
5445
- const fieldsBody = match[2];
5449
+ const [, typeName, fieldsBody] = match;
5450
+ if (typeName === undefined || fieldsBody === undefined)
5451
+ continue;
5446
5452
  const fieldLines = fieldsBody.split(`
5447
5453
  `).map((l) => l.trim()).filter(Boolean);
5448
5454
  const fields = [];
@@ -5450,8 +5456,9 @@ function parseGraphQLSDL(sdl) {
5450
5456
  const fieldMatch = /^([A-Za-z0-9_]+)(?:\([^)]*\))?\s*:\s*([[\]A-Za-z0-9_!]+)/.exec(line);
5451
5457
  if (!fieldMatch)
5452
5458
  continue;
5453
- const fName = fieldMatch[1];
5454
- const rawType = fieldMatch[2];
5459
+ const [, fName, rawType] = fieldMatch;
5460
+ if (fName === undefined || rawType === undefined)
5461
+ continue;
5455
5462
  const isNonNullable = rawType.endsWith("!");
5456
5463
  const cleanType = isNonNullable ? rawType.slice(0, -1) : rawType;
5457
5464
  const isList = cleanType.startsWith("[") && cleanType.endsWith("]");
@@ -5884,6 +5891,13 @@ function generateTypeBoxSchemaCode(resource) {
5884
5891
  case "date":
5885
5892
  typeDef = "Type.String({ format: 'date-time' })";
5886
5893
  break;
5894
+ case "datetime":
5895
+ case "time":
5896
+ typeDef = "Type.String()";
5897
+ break;
5898
+ case "daterange":
5899
+ typeDef = "Type.Union([Type.Object({ start: Type.Union([Type.String(), Type.Null()]), end: Type.Union([Type.String(), Type.Null()]) }, { additionalProperties: false }), Type.Null()])";
5900
+ break;
5887
5901
  case "email":
5888
5902
  typeDef = "Type.String({ format: 'email' })";
5889
5903
  break;
@@ -5966,74 +5980,99 @@ function parseInferArguments(args) {
5966
5980
  method: "GET",
5967
5981
  format: "all"
5968
5982
  };
5969
- for (let i = 0;i < args.length; i++) {
5970
- const arg = args[i];
5983
+ const argumentsIterator = args.values();
5984
+ function requireArgument(flag) {
5985
+ const next = argumentsIterator.next();
5986
+ if (next.done || !next.value.trim() || next.value.startsWith("-")) {
5987
+ throw new Error(`Missing value for ${flag}`);
5988
+ }
5989
+ return next.value;
5990
+ }
5991
+ function sourceType(value) {
5992
+ switch (value) {
5993
+ case "rest":
5994
+ case "openapi":
5995
+ case "graphql":
5996
+ case "auto":
5997
+ return value;
5998
+ default:
5999
+ throw new Error(`Invalid source type: ${value}`);
6000
+ }
6001
+ }
6002
+ function outputFormat(value) {
6003
+ switch (value) {
6004
+ case "all":
6005
+ case "resource":
6006
+ case "typebox":
6007
+ case "components":
6008
+ return value;
6009
+ default:
6010
+ throw new Error(`Invalid output format: ${value}`);
6011
+ }
6012
+ }
6013
+ function addHeader(headerLine) {
6014
+ const colonIndex = headerLine.indexOf(":");
6015
+ const key = headerLine.slice(0, colonIndex).trim();
6016
+ if (colonIndex <= 0 || !key) {
6017
+ throw new Error(`Invalid header: ${headerLine}`);
6018
+ }
6019
+ options.headers ??= {};
6020
+ options.headers[key] = headerLine.slice(colonIndex + 1).trim();
6021
+ }
6022
+ for (const arg of argumentsIterator) {
6023
+ if (arg.startsWith("--") && arg.indexOf("=") === arg.length - 1) {
6024
+ throw new Error(`Missing value for ${arg.slice(0, -1)}`);
6025
+ }
5971
6026
  if (arg === "--write" || arg === "-w") {
5972
6027
  options.write = true;
5973
6028
  } else if (arg === "--dry-run") {
5974
6029
  options.write = false;
5975
6030
  } else if (arg === "--url" || arg === "-u") {
5976
- options.url = args[++i];
6031
+ options.url = requireArgument(arg);
5977
6032
  } else if (arg.startsWith("--url=")) {
5978
6033
  options.url = arg.slice(6);
5979
6034
  } else if (arg === "--file" || arg === "-f") {
5980
- options.file = args[++i];
6035
+ options.file = requireArgument(arg);
5981
6036
  } else if (arg.startsWith("--file=")) {
5982
6037
  options.file = arg.slice(7);
5983
6038
  } else if (arg === "--type" || arg === "-t") {
5984
- options.type = args[++i];
6039
+ options.type = sourceType(requireArgument(arg));
5985
6040
  } else if (arg.startsWith("--type=")) {
5986
- options.type = arg.slice(7);
6041
+ options.type = sourceType(arg.slice(7));
5987
6042
  } else if (arg === "--resource" || arg === "-r") {
5988
- options.resource = args[++i];
6043
+ options.resource = requireArgument(arg);
5989
6044
  } else if (arg.startsWith("--resource=")) {
5990
6045
  options.resource = arg.slice(11);
5991
6046
  } else if (arg === "--out-dir" || arg === "-o" || arg === "--output") {
5992
- options.outDir = args[++i];
6047
+ options.outDir = requireArgument(arg);
5993
6048
  } else if (arg.startsWith("--out-dir=")) {
5994
6049
  options.outDir = arg.slice(10);
5995
6050
  } else if (arg.startsWith("--output=")) {
5996
6051
  options.outDir = arg.slice(9);
5997
6052
  } else if (arg === "--primary-key" || arg === "-k") {
5998
- options.primaryKey = args[++i];
6053
+ options.primaryKey = requireArgument(arg);
5999
6054
  } else if (arg.startsWith("--primary-key=")) {
6000
6055
  options.primaryKey = arg.slice(14);
6001
6056
  } else if (arg === "--fields") {
6002
- options.fields = args[++i];
6057
+ options.fields = requireArgument(arg);
6003
6058
  } else if (arg.startsWith("--fields=")) {
6004
6059
  options.fields = arg.slice(9);
6005
6060
  } else if (arg === "--header" || arg === "-H") {
6006
- const headerLine = args[++i] ?? "";
6007
- const colonIndex = headerLine.indexOf(":");
6008
- if (colonIndex > 0) {
6009
- const key = headerLine.slice(0, colonIndex).trim();
6010
- const value = headerLine.slice(colonIndex + 1).trim();
6011
- if (!options.headers)
6012
- options.headers = {};
6013
- options.headers[key] = value;
6014
- }
6061
+ addHeader(requireArgument(arg));
6015
6062
  } else if (arg.startsWith("--header=")) {
6016
- const headerLine = arg.slice(9);
6017
- const colonIndex = headerLine.indexOf(":");
6018
- if (colonIndex > 0) {
6019
- const key = headerLine.slice(0, colonIndex).trim();
6020
- const value = headerLine.slice(colonIndex + 1).trim();
6021
- if (!options.headers)
6022
- options.headers = {};
6023
- options.headers[key] = value;
6024
- }
6063
+ addHeader(arg.slice(9));
6025
6064
  } else if (arg === "--method" || arg === "-m") {
6026
- options.method = (args[++i] ?? "GET").toUpperCase();
6065
+ options.method = requireArgument(arg).toUpperCase();
6027
6066
  } else if (arg.startsWith("--method=")) {
6028
6067
  options.method = arg.slice(9).toUpperCase();
6029
6068
  } else if (arg === "--body" || arg === "-b") {
6030
- options.body = args[++i];
6069
+ options.body = requireArgument(arg);
6031
6070
  } else if (arg.startsWith("--body=")) {
6032
6071
  options.body = arg.slice(7);
6033
6072
  } else if (arg === "--format") {
6034
- options.format = args[++i];
6073
+ options.format = outputFormat(requireArgument(arg));
6035
6074
  } else if (arg.startsWith("--format=")) {
6036
- options.format = arg.slice(9);
6075
+ options.format = outputFormat(arg.slice(9));
6037
6076
  } else if (arg === "--help" || arg === "-h") {
6038
6077
  printInferHelp();
6039
6078
  process.exit(0);
@@ -6045,6 +6084,8 @@ function parseInferArguments(args) {
6045
6084
  } else {
6046
6085
  options.resource = arg;
6047
6086
  }
6087
+ } else {
6088
+ throw new Error(`Unexpected argument: ${arg}`);
6048
6089
  }
6049
6090
  }
6050
6091
  return options;
@@ -6080,6 +6121,22 @@ ${import_picocolors.default.bold("EXAMPLES:")}
6080
6121
  svadmin infer --file sample-posts.json --resource posts --out-dir src/resources --write
6081
6122
  `);
6082
6123
  }
6124
+ function isRecord(value) {
6125
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6126
+ }
6127
+ function isOpenAPIDocument(value) {
6128
+ return isRecord(value) && (("openapi" in value) || ("swagger" in value) || ("paths" in value) && ("components" in value));
6129
+ }
6130
+ function isGraphQLDocument(value) {
6131
+ return isRecord(value) && (("__schema" in value) || isRecord(value["data"]) && ("__schema" in value["data"]) || Array.isArray(value["types"]));
6132
+ }
6133
+ function readSampleRecords(value) {
6134
+ const records = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value["data"]) ? value["data"] : isRecord(value) && Array.isArray(value["items"]) ? value["items"] : [value];
6135
+ if (!records.every(isRecord)) {
6136
+ throw new Error("REST samples must be objects or arrays of objects");
6137
+ }
6138
+ return records;
6139
+ }
6083
6140
  async function loadSourceData(options, customFetch = fetch) {
6084
6141
  if (options.file) {
6085
6142
  const filePath = path.resolve(process.cwd(), options.file);
@@ -6097,31 +6154,9 @@ async function loadSourceData(options, customFetch = fetch) {
6097
6154
  sourceDescription: options.file
6098
6155
  };
6099
6156
  }
6157
+ let json;
6100
6158
  try {
6101
- const json = JSON.parse(content);
6102
- if (typeof json === "object" && json !== null && (("openapi" in json) || ("swagger" in json) || ("paths" in json) && ("components" in json))) {
6103
- return {
6104
- sourceType: "openapi",
6105
- data: json,
6106
- derivedResourceName: options.resource,
6107
- sourceDescription: options.file
6108
- };
6109
- }
6110
- if (typeof json === "object" && json !== null && (("__schema" in json) || ("data" in json) && json.data && ("__schema" in json.data) || ("types" in json) && Array.isArray(json.types))) {
6111
- return {
6112
- sourceType: "graphql",
6113
- data: json,
6114
- derivedResourceName: options.resource,
6115
- sourceDescription: options.file
6116
- };
6117
- }
6118
- const records = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [json];
6119
- return {
6120
- sourceType: "rest",
6121
- data: records,
6122
- derivedResourceName: options.resource || baseName,
6123
- sourceDescription: options.file
6124
- };
6159
+ json = JSON.parse(content);
6125
6160
  } catch {
6126
6161
  if (content.includes("type ") || content.includes("enum ") || content.includes("schema ")) {
6127
6162
  return {
@@ -6133,12 +6168,35 @@ async function loadSourceData(options, customFetch = fetch) {
6133
6168
  }
6134
6169
  throw new Error(`Unable to parse file ${filePath}. Expected valid JSON or GraphQL SDL schema.`);
6135
6170
  }
6171
+ if (isOpenAPIDocument(json)) {
6172
+ return {
6173
+ sourceType: "openapi",
6174
+ data: json,
6175
+ ...options.resource === undefined ? {} : { derivedResourceName: options.resource },
6176
+ sourceDescription: options.file
6177
+ };
6178
+ }
6179
+ if (isGraphQLDocument(json)) {
6180
+ return {
6181
+ sourceType: "graphql",
6182
+ data: json,
6183
+ ...options.resource === undefined ? {} : { derivedResourceName: options.resource },
6184
+ sourceDescription: options.file
6185
+ };
6186
+ }
6187
+ const records = readSampleRecords(json);
6188
+ return {
6189
+ sourceType: "rest",
6190
+ data: records,
6191
+ derivedResourceName: options.resource || baseName,
6192
+ sourceDescription: options.file
6193
+ };
6136
6194
  }
6137
6195
  if (options.url) {
6138
6196
  const url = options.url;
6139
6197
  const isGraphQL = options.type === "graphql" || url.endsWith("/graphql") || url.includes("/graphql?");
6140
6198
  if (isGraphQL) {
6141
- const response2 = await customFetch(url, {
6199
+ const response = await customFetch(url, {
6142
6200
  method: "POST",
6143
6201
  headers: {
6144
6202
  "Content-Type": "application/json",
@@ -6147,14 +6205,14 @@ async function loadSourceData(options, customFetch = fetch) {
6147
6205
  },
6148
6206
  body: JSON.stringify({ query: GRAPHQL_INTROSPECTION_QUERY })
6149
6207
  });
6150
- if (!response2.ok) {
6151
- throw new Error(`GraphQL introspection request failed: HTTP ${response2.status} ${response2.statusText}`);
6208
+ if (!response.ok) {
6209
+ throw new Error(`GraphQL introspection request failed: HTTP ${response.status} ${response.statusText}`);
6152
6210
  }
6153
- const json2 = await response2.json();
6211
+ const json = await response.json();
6154
6212
  return {
6155
6213
  sourceType: "graphql",
6156
- data: json2,
6157
- derivedResourceName: options.resource,
6214
+ data: json,
6215
+ ...options.resource === undefined ? {} : { derivedResourceName: options.resource },
6158
6216
  sourceDescription: url
6159
6217
  };
6160
6218
  }
@@ -6164,25 +6222,25 @@ async function loadSourceData(options, customFetch = fetch) {
6164
6222
  Accept: "application/json",
6165
6223
  ...options.headers ?? {}
6166
6224
  },
6167
- body: options.body
6225
+ ...options.body === undefined ? {} : { body: options.body }
6168
6226
  });
6169
6227
  if (!response.ok) {
6170
6228
  throw new Error(`HTTP request failed: HTTP ${response.status} ${response.statusText}`);
6171
6229
  }
6172
6230
  const json = await response.json();
6173
- if (typeof json === "object" && json !== null && (("openapi" in json) || ("swagger" in json) || ("paths" in json) && ("components" in json))) {
6231
+ if (isOpenAPIDocument(json)) {
6174
6232
  return {
6175
6233
  sourceType: "openapi",
6176
6234
  data: json,
6177
- derivedResourceName: options.resource,
6235
+ ...options.resource === undefined ? {} : { derivedResourceName: options.resource },
6178
6236
  sourceDescription: url
6179
6237
  };
6180
6238
  }
6181
- if (typeof json === "object" && json !== null && (("__schema" in json) || ("data" in json) && json.data?.__schema)) {
6239
+ if (isGraphQLDocument(json)) {
6182
6240
  return {
6183
6241
  sourceType: "graphql",
6184
6242
  data: json,
6185
- derivedResourceName: options.resource,
6243
+ ...options.resource === undefined ? {} : { derivedResourceName: options.resource },
6186
6244
  sourceDescription: url
6187
6245
  };
6188
6246
  }
@@ -6196,7 +6254,7 @@ async function loadSourceData(options, customFetch = fetch) {
6196
6254
  derivedName = "items";
6197
6255
  }
6198
6256
  }
6199
- const records = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [json];
6257
+ const records = readSampleRecords(json);
6200
6258
  return {
6201
6259
  sourceType: "rest",
6202
6260
  data: records,
@@ -6270,9 +6328,11 @@ async function executeInfer(options, customFetch = fetch) {
6270
6328
  let resources;
6271
6329
  const bundles = new Map;
6272
6330
  if (loaded.sourceType === "openapi") {
6331
+ if (!isRecord(loaded.data))
6332
+ throw new Error("OpenAPI source must be an object");
6273
6333
  resources = inferFromOpenAPI(loaded.data, {
6274
6334
  primaryKey,
6275
- include: options.resource ? [options.resource] : undefined
6335
+ ...options.resource ? { include: [options.resource] } : {}
6276
6336
  });
6277
6337
  for (const res of resources) {
6278
6338
  bundles.set(res.name, generateResourceBundle(res));
@@ -6280,14 +6340,14 @@ async function executeInfer(options, customFetch = fetch) {
6280
6340
  } else if (loaded.sourceType === "graphql") {
6281
6341
  resources = inferFromGraphQL(loaded.data, {
6282
6342
  primaryKey,
6283
- include: options.resource ? [options.resource] : undefined
6343
+ ...options.resource ? { include: [options.resource] } : {}
6284
6344
  });
6285
6345
  for (const res of resources) {
6286
6346
  bundles.set(res.name, generateResourceBundle(res));
6287
6347
  }
6288
6348
  } else {
6289
6349
  const resName = options.resource || loaded.derivedResourceName || "items";
6290
- const sampleArray = Array.isArray(loaded.data) ? loaded.data : [loaded.data];
6350
+ const sampleArray = readSampleRecords(loaded.data);
6291
6351
  const inferRes = inferResource(resName, sampleArray, { primaryKey });
6292
6352
  resources = [inferRes.resource];
6293
6353
  bundles.set(resName, inferRes);
@@ -6312,7 +6372,7 @@ async function executeInfer(options, customFetch = fetch) {
6312
6372
  files,
6313
6373
  sourceDescription: loaded.sourceDescription,
6314
6374
  wrote,
6315
- outDir: options.outDir
6375
+ ...options.outDir === undefined ? {} : { outDir: options.outDir }
6316
6376
  };
6317
6377
  }
6318
6378
  function printInferResult(result) {
@@ -6409,14 +6469,33 @@ async function promptInferWizard() {
6409
6469
  initial: true
6410
6470
  }
6411
6471
  ]);
6412
- const targetPath = answers.pathOrUrl || answers.endpointUrl;
6413
- const isUrl = targetPath?.startsWith("http://") || targetPath?.startsWith("https://");
6472
+ return parseInferWizardAnswers(answers);
6473
+ }
6474
+ function parseInferWizardAnswers(answers) {
6475
+ if (!isRecord(answers))
6476
+ throw new Error("Invalid inference wizard answers");
6477
+ const sourceType = answers["sourceType"];
6478
+ if (sourceType !== "rest-file" && sourceType !== "rest-url" && sourceType !== "openapi" && sourceType !== "graphql") {
6479
+ throw new Error("Invalid inference wizard source type");
6480
+ }
6481
+ const targetPath = sourceType === "rest-file" || sourceType === "openapi" ? answers["pathOrUrl"] : answers["endpointUrl"];
6482
+ if (typeof targetPath !== "string" || !targetPath.trim()) {
6483
+ throw new Error("Inference wizard requires a URL or file path");
6484
+ }
6485
+ const outDir = answers["outDir"];
6486
+ const write = answers["write"];
6487
+ if (typeof outDir !== "string" || !outDir.trim() || typeof write !== "boolean") {
6488
+ throw new Error("Invalid inference wizard output options");
6489
+ }
6490
+ const isUrl = targetPath.startsWith("http://") || targetPath.startsWith("https://");
6491
+ if ((sourceType === "rest-url" || sourceType === "graphql") && !isUrl) {
6492
+ throw new Error("Inference wizard requires an HTTP(S) endpoint");
6493
+ }
6414
6494
  return {
6415
- url: isUrl ? targetPath : undefined,
6416
- file: !isUrl ? targetPath : undefined,
6417
- type: answers.sourceType.startsWith("graphql") ? "graphql" : answers.sourceType.startsWith("openapi") ? "openapi" : "rest",
6418
- outDir: answers.outDir,
6419
- write: answers.write
6495
+ ...isUrl ? { url: targetPath } : { file: targetPath },
6496
+ type: sourceType === "graphql" ? "graphql" : sourceType === "openapi" ? "openapi" : "rest",
6497
+ outDir,
6498
+ write
6420
6499
  };
6421
6500
  }
6422
6501
  async function inferCommand(args) {
@@ -6442,34 +6521,81 @@ import path2 from "node:path";
6442
6521
  function parseGenerateArguments(args) {
6443
6522
  return parseInferArguments(args);
6444
6523
  }
6524
+ var fieldTypes = {
6525
+ text: true,
6526
+ number: true,
6527
+ boolean: true,
6528
+ date: true,
6529
+ time: true,
6530
+ datetime: true,
6531
+ daterange: true,
6532
+ select: true,
6533
+ multiselect: true,
6534
+ tags: true,
6535
+ textarea: true,
6536
+ richtext: true,
6537
+ image: true,
6538
+ images: true,
6539
+ json: true,
6540
+ relation: true,
6541
+ color: true,
6542
+ url: true,
6543
+ email: true,
6544
+ phone: true,
6545
+ currency: true,
6546
+ percent: true,
6547
+ file: true,
6548
+ markdown: true,
6549
+ password: true,
6550
+ array: true,
6551
+ "tree-select": true,
6552
+ treeselect: true,
6553
+ cascader: true,
6554
+ transfer: true,
6555
+ rate: true,
6556
+ rating: true,
6557
+ avatar: true,
6558
+ copy: true,
6559
+ code: true
6560
+ };
6561
+ function isFieldType(value) {
6562
+ return Object.hasOwn(fieldTypes, value);
6563
+ }
6564
+ function parseManualFields(fields, primaryKey) {
6565
+ const keys = new Set;
6566
+ return fields.split(",").map((field) => {
6567
+ const [rawKey, rawType, extra] = field.split(":");
6568
+ const key = rawKey?.trim();
6569
+ const type = rawType === undefined ? "text" : rawType.trim();
6570
+ if (!key || extra !== undefined)
6571
+ throw new Error(`Invalid field definition: ${field}`);
6572
+ if (!isFieldType(type))
6573
+ throw new Error(`Invalid field type: ${type}`);
6574
+ if (keys.has(key))
6575
+ throw new Error(`Duplicate field: ${key}`);
6576
+ keys.add(key);
6577
+ return {
6578
+ key,
6579
+ label: key.charAt(0).toUpperCase() + key.slice(1),
6580
+ type,
6581
+ required: key === primaryKey
6582
+ };
6583
+ });
6584
+ }
6445
6585
  async function generateCommand(args) {
6446
6586
  const options = parseGenerateArguments(args);
6447
6587
  if (options.resource && options.fields) {
6448
6588
  const resourceName = options.resource;
6449
6589
  const primaryKey = options.primaryKey ?? "id";
6450
- const fieldDefs = options.fields.split(",").map((f) => {
6451
- const [key, typeRaw] = f.split(":");
6452
- const type = typeRaw || "text";
6453
- return {
6454
- key: key.trim(),
6455
- label: key.trim().charAt(0).toUpperCase() + key.trim().slice(1),
6456
- type,
6457
- required: key.trim() === primaryKey
6458
- };
6459
- });
6460
- const mockSample = {};
6461
- for (const f of fieldDefs) {
6462
- mockSample[f.key] = f.type === "number" ? 1 : f.type === "boolean" ? true : `sample_${f.key}`;
6463
- }
6464
- const inferRes = inferResource(resourceName, [mockSample], { primaryKey });
6465
- const resources = [
6466
- {
6467
- name: resourceName,
6468
- label: resourceName.charAt(0).toUpperCase() + resourceName.slice(1),
6469
- primaryKey,
6470
- fields: fieldDefs
6471
- }
6472
- ];
6590
+ const fieldDefs = parseManualFields(options.fields, primaryKey);
6591
+ const resource = {
6592
+ name: resourceName,
6593
+ label: resourceName.charAt(0).toUpperCase() + resourceName.slice(1),
6594
+ primaryKey,
6595
+ fields: fieldDefs
6596
+ };
6597
+ const resources = [resource];
6598
+ const inferRes = generateResourceBundle(resource);
6473
6599
  const bundles = new Map([[resourceName, inferRes]]);
6474
6600
  const files = planGeneratedFiles(resources, bundles, options.format);
6475
6601
  let wrote = false;
@@ -6488,7 +6614,7 @@ async function generateCommand(args) {
6488
6614
  files,
6489
6615
  sourceDescription: `manual schema: ${options.fields}`,
6490
6616
  wrote,
6491
- outDir: options.outDir
6617
+ ...options.outDir === undefined ? {} : { outDir: options.outDir }
6492
6618
  });
6493
6619
  return;
6494
6620
  }
@@ -6781,17 +6907,17 @@ import {
6781
6907
  unlinkSync,
6782
6908
  writeFileSync
6783
6909
  } from "node:fs";
6784
- function parseDependencyMap(candidate, path4) {
6910
+ function parseDependencyMap(candidate, path) {
6785
6911
  if (candidate === undefined)
6786
6912
  return;
6787
- assertStringRecord(candidate, path4);
6913
+ assertStringRecord(candidate, path);
6788
6914
  return { ...candidate };
6789
6915
  }
6790
6916
  function parseMaintainedPackageJson(packageJsonCandidate) {
6791
6917
  assertJsonObject(packageJsonCandidate, "package.json");
6792
6918
  const packageJson = structuredClone(packageJsonCandidate);
6793
- const dependencies = parseDependencyMap(packageJsonCandidate.dependencies, "package.json.dependencies");
6794
- const devDependencies = parseDependencyMap(packageJsonCandidate.devDependencies, "package.json.devDependencies");
6919
+ const dependencies = parseDependencyMap(packageJsonCandidate["dependencies"], "package.json.dependencies");
6920
+ const devDependencies = parseDependencyMap(packageJsonCandidate["devDependencies"], "package.json.devDependencies");
6795
6921
  if (dependencies !== undefined)
6796
6922
  packageJson.dependencies = dependencies;
6797
6923
  if (devDependencies !== undefined)
@@ -6837,6 +6963,8 @@ function desiredDependencies(project, scaffold) {
6837
6963
  }
6838
6964
  for (const packName of selectedDependencyPacks(project, scaffold)) {
6839
6965
  const pack = scaffold.svadmin.dependencyPacks[packName];
6966
+ if (pack === undefined)
6967
+ throw new Error(`Unknown dependency pack: ${packName}`);
6840
6968
  for (const [packageName, version] of Object.entries(pack)) {
6841
6969
  const existing = desired.get(packageName);
6842
6970
  if (existing !== undefined && (existing.version !== version || existing.section !== "dependencies")) {
@@ -6947,12 +7075,12 @@ function duplicateDependencyIssue(desired, state) {
6947
7075
  function doctorIssueForDependency(project, desired) {
6948
7076
  const state = dependencyState(project, desired);
6949
7077
  if (state.canonicalVersion === undefined) {
6950
- return state.alternateVersion === undefined ? missingDependencyIssue(desired) : misplacedDependencyIssue(desired, state);
7078
+ return state.alternateVersion === undefined ? missingDependencyIssue(desired) : misplacedDependencyIssue(desired, { ...state, alternateVersion: state.alternateVersion });
6951
7079
  }
6952
7080
  if (state.canonicalVersion !== desired.version) {
6953
7081
  return versionDependencyIssue(desired, state.canonicalVersion);
6954
7082
  }
6955
- return state.alternateVersion === undefined ? null : duplicateDependencyIssue(desired, state);
7083
+ return state.alternateVersion === undefined ? null : duplicateDependencyIssue(desired, { ...state, alternateVersion: state.alternateVersion });
6956
7084
  }
6957
7085
  function doctorProjectPackageJson(packageJsonCandidate, scaffold) {
6958
7086
  const project = parseMaintainedPackageJson(packageJsonCandidate);
@@ -6960,10 +7088,10 @@ function doctorProjectPackageJson(packageJsonCandidate, scaffold) {
6960
7088
  return issues.length === 0 ? { status: "clean", exitCode: 0, issues: [] } : { status: "issues", exitCode: 1, issues };
6961
7089
  }
6962
7090
  function upgradeChangeForDependency(desired, state) {
6963
- if (state.canonicalVersion === undefined && state.alternateVersion === undefined) {
6964
- return { action: "add", packageName: desired.packageName, to: desired.version, section: desired.section };
6965
- }
6966
7091
  if (state.canonicalVersion === undefined) {
7092
+ if (state.alternateVersion === undefined) {
7093
+ return { action: "add", packageName: desired.packageName, to: desired.version, section: desired.section };
7094
+ }
6967
7095
  return {
6968
7096
  action: "move",
6969
7097
  packageName: desired.packageName,
@@ -6981,7 +7109,7 @@ function upgradeChangeForDependency(desired, state) {
6981
7109
  from: state.canonicalVersion,
6982
7110
  to: desired.version,
6983
7111
  section: desired.section,
6984
- previousSection: state.alternateVersion === undefined ? undefined : state.alternateSection
7112
+ ...state.alternateVersion === undefined ? {} : { previousSection: state.alternateSection }
6985
7113
  };
6986
7114
  }
6987
7115
  function applyDesiredDependency(updatedPackageJson, desired) {
@@ -7325,7 +7453,7 @@ bun run dev
7325
7453
 
7326
7454
  ## Stack
7327
7455
 
7328
- - **UI**: Svelte 5 + Shadcn Svelte + TailwindCSS
7456
+ - **UI**: Svelte 5 + Bits UI + precompiled Panda CSS
7329
7457
  - **Data**: ${dpLabel} DataProvider
7330
7458
  - **Auth**: ${authLabel}
7331
7459
  - **State**: TanStack Query v6
@@ -13,6 +13,45 @@ acceptance checks in this project.
13
13
  custom markup or raw colors.
14
14
  5. Verify loading, empty, partial, error, permission, success, and mobile states.
15
15
 
16
+ ## Type-safe AI development
17
+
18
+ - Read the actual provider API and resource models before generating a page.
19
+ Do not invent fields or duplicate server/schema types.
20
+ - Define module-level contracts with `defineResource(name, schemas)` and derive
21
+ models from TypeBox `Static<typeof Schema>`. Do not duplicate models or use
22
+ declaration merging as proof of runtime validation.
23
+ - Use `useList`, `useOne`, `useShow`, and `useMany` with a resource contract.
24
+ `useTable` also infers query rows and `clientData`; `useInfiniteList` and `useSelect`
25
+ use the same contract. String resources, implicit routes, and data generics are
26
+ rejected by the default entry point. Use getters for reactive query inputs.
27
+ - Treat network responses, storage contents, and AI tool arguments as `unknown`
28
+ until validated at their boundary. Resource contracts validate CRUD requests
29
+ and responses automatically, but they do not authorize a mutation.
30
+ - Provide the full closed record schema with a required string/number `id`.
31
+ Supply create and update schemas explicitly; missing schemas disable writes.
32
+ Extra fields, nested `any`, open dictionaries, and unsupported schema kinds
33
+ are rejected. Do not coerce values or silently remove unexpected fields.
34
+ - Define custom calls with `defineCommand` and use `useCustom` or
35
+ `useCustomMutation`. Inputs and outputs must have schemas; URLs and methods
36
+ cannot be overridden by individual calls.
37
+ - Bind `useCreate`, `useUpdate`, `useDelete`, and their `Many` variants to an
38
+ explicit contract to infer mutation inputs and results. A bound mutation
39
+ cannot target another resource. `useUpdate` and `useDelete` require an ID.
40
+ - Keep required delete variables in `useDeleteMany` calls; both native batch
41
+ providers and single-record fallbacks receive them.
42
+ - Bind forms to a contract and a fixed create/edit action. `defaultValues` and
43
+ `setFieldValue` follow the operation schema, including required fields.
44
+ Edit backfill excludes record-only fields.
45
+ - Do not import `@svadmin/core/unsafe` in new application pages. It is reserved
46
+ for reviewed metadata-driven framework boundaries, not compatibility fallback.
47
+ - Do not automatically retry an invalid write response: inspect the structured
48
+ `writeMayHaveSucceeded` diagnostic and read back the server state first.
49
+ - Never silence a mismatch with `any`, double assertions, `@ts-ignore`, or a
50
+ broader index signature. Fix the source contract or validate and narrow it.
51
+ - Run the generated project's `bun run check` and focused tests after each
52
+ change. Cover invalid fields and payloads as well as successful paths.
53
+ Passing runtime tests alone does not prove compile-time type safety.
54
+
16
55
  ## Feedback invariant
17
56
 
18
57
  `one event -> one primary feedback surface`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/create",
3
- "version": "0.26.2",
3
+ "version": "0.29.0",
4
4
  "description": "Scaffolding tool for svadmin projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "@types/node": "^26.4.0",
28
28
  "@types/prompts": "^2.4.9",
29
29
  "typescript": "^7.0.2",
30
- "@svadmin/core": "^0.50.0"
30
+ "@svadmin/core": "^0.53.0"
31
31
  },
32
32
  "keywords": [
33
33
  "svadmin",
@@ -39,7 +39,7 @@
39
39
  "license": "MIT",
40
40
  "repository": {
41
41
  "type": "git",
42
- "url": "https://github.com/vibeunion/svadmin.git",
42
+ "url": "git+https://github.com/vibeunion/svadmin.git",
43
43
  "directory": "packages/create-svadmin"
44
44
  }
45
45
  }
@@ -10,20 +10,17 @@
10
10
  "check": "svelte-check --tsconfig ./tsconfig.json"
11
11
  },
12
12
  "dependencies": {
13
- "@svadmin/core": "^0.50.0",
14
- "@svadmin/ai-elements": "^0.5.1",
15
- "@svadmin/ui": "^0.70.0",
13
+ "@svadmin/core": "^0.53.0",
14
+ "@svadmin/ai-elements": "^0.8.0",
15
+ "@svadmin/ui": "^0.73.0",
16
16
  "@sinclair/typebox": "^0.34.52",
17
17
  "@tanstack/svelte-query": "^6.1.48",
18
18
  "@lucide/svelte": "^1.35.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
22
- "@tailwindcss/vite": "^4.3.3",
23
22
  "svelte": "^5.56.10",
24
23
  "svelte-check": "^4.7.6",
25
- "tailwindcss": "^4.3.3",
26
- "tw-animate-css": "^1.4.0",
27
24
  "typescript": "~6.0.3",
28
25
  "vite": "^8.2.2"
29
26
  },
@@ -33,16 +30,16 @@
33
30
  "@refinedev/core": "^5.0.12"
34
31
  },
35
32
  "simple-rest": {
36
- "@svadmin/simple-rest": "^0.9.25",
33
+ "@svadmin/simple-rest": "^0.10.7",
37
34
  "@refinedev/simple-rest": "^6.0.1"
38
35
  },
39
36
  "supabase": {
40
- "@svadmin/supabase": "^0.13.9",
37
+ "@svadmin/supabase": "^0.15.1",
41
38
  "@supabase/supabase-js": "^2.112.4",
42
39
  "@refinedev/supabase": "^6.0.2"
43
40
  },
44
41
  "graphql": {
45
- "@svadmin/graphql": "^0.9.25",
42
+ "@svadmin/graphql": "^0.10.7",
46
43
  "@refinedev/graphql": "^8.0.1",
47
44
  "graphql-request": "^7.4.0",
48
45
  "graphql": "^16.8.0"
@@ -1,8 +1,2 @@
1
- @import "tailwindcss";
2
- @import "tw-animate-css";
3
-
4
- /* Tailwind source tokens and component discovery are owned by SVAdmin packages. */
5
- @import "@svadmin/ui/app.theme.css";
1
+ @import "@svadmin/ui/app.css";
6
2
  @import "@svadmin/ai-elements/ai.css";
7
-
8
- @custom-variant dark (&:is(.dark *));
@@ -1,79 +1,92 @@
1
1
  <script lang="ts">
2
2
  import { useList } from '@svadmin/core';
3
- import { FileText, Users, MessageCircle, Loader2 } from '@lucide/svelte';
3
+ import { posts, users, comments } from '../resource-contracts';
4
+ import { Button, DataState, MetricBlock } from '@svadmin/ui';
4
5
 
5
- const postsQuery = useList({ resource: 'posts', pagination: { current: 1, pageSize: 1 } });
6
- const usersQuery = useList({ resource: 'users', pagination: { current: 1, pageSize: 1 } });
7
- const commentsQuery = useList({ resource: 'comments', pagination: { current: 1, pageSize: 1 } });
6
+ const postsQuery = useList({ resource: posts, pagination: { current: 1, pageSize: 1 } });
7
+ const usersQuery = useList({ resource: users, pagination: { current: 1, pageSize: 1 } });
8
+ const commentsQuery = useList({ resource: comments, pagination: { current: 1, pageSize: 1 } });
8
9
 
9
10
  const stats = $derived([
10
- { label: 'Total Posts', value: $postsQuery.data?.total ?? '—', Icon: FileText, color: 'bg-blue-50 text-blue-600' },
11
- { label: 'Total Users', value: $usersQuery.data?.total ?? '—', Icon: Users, color: 'bg-green-50 text-green-600' },
12
- { label: 'Total Comments', value: $commentsQuery.data?.total ?? '—', Icon: MessageCircle, color: 'bg-purple-50 text-purple-600' },
11
+ { label: 'Total Posts', query: postsQuery },
12
+ { label: 'Total Users', query: usersQuery },
13
+ { label: 'Total Comments', query: commentsQuery },
13
14
  ]);
14
15
 
15
- const isLoading = $derived($postsQuery.isLoading || $usersQuery.isLoading || $commentsQuery.isLoading);
16
+ const hasMetricError = $derived(stats.some(stat => stat.query.isError));
16
17
 
17
- const recentPosts = useList({ resource: 'posts', pagination: { current: 1, pageSize: 5 } });
18
- const recentUsers = useList({ resource: 'users', pagination: { current: 1, pageSize: 5 } });
18
+ const recentPosts = useList({ resource: posts, pagination: { current: 1, pageSize: 5 } });
19
+ const recentUsers = useList({ resource: users, pagination: { current: 1, pageSize: 5 } });
19
20
  </script>
20
21
 
21
- <div class="space-y-6">
22
- <h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
22
+ <div class="space-y-4">
23
+ <h1 class="text-xl font-semibold text-foreground">Dashboard</h1>
23
24
 
24
- <!-- Stats -->
25
+ {#if hasMetricError}
26
+ <DataState state="error" title="Some totals are unavailable" retry={() => {
27
+ for (const stat of stats) if (stat.query.isError && !stat.query.isFetching) void stat.query.refetch();
28
+ }} />
29
+ {/if}
25
30
  <div class="grid gap-4 sm:grid-cols-3">
26
- {#each stats as stat, _i (_i)}
27
- <div class="flex items-center gap-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
28
- <div class="flex h-12 w-12 items-center justify-center rounded-xl {stat.color}">
29
- <stat.Icon class="h-6 w-6" />
30
- </div>
31
- <div>
32
- <p class="text-sm text-gray-500">{stat.label}</p>
33
- {#if isLoading}
34
- <Loader2 class="mt-1 h-5 w-5 animate-spin text-gray-300" />
35
- {:else}
36
- <p class="text-2xl font-bold text-gray-900">{stat.value}</p>
37
- {/if}
38
- </div>
39
- </div>
31
+ {#each stats as stat (stat.label)}
32
+ <MetricBlock label={stat.label} value={stat.query.isError ? '—' : stat.query.data?.total ?? '—'} loading={stat.query.isLoading} />
40
33
  {/each}
41
34
  </div>
42
35
 
43
36
  <!-- Recent Data -->
44
37
  <div class="grid gap-6 lg:grid-cols-2">
45
- <div class="rounded-xl border border-gray-200 bg-white shadow-sm">
46
- <div class="flex items-center justify-between border-b border-gray-100 px-5 py-4">
47
- <h2 class="font-semibold text-gray-900">Recent Posts</h2>
38
+ <section class="min-w-0">
39
+ <div class="flex items-center justify-between border-b py-3">
40
+ <h2 class="font-semibold text-foreground">Recent Posts</h2>
48
41
  <a href="#/posts" class="text-sm text-primary hover:underline">View all</a>
49
42
  </div>
50
- <div class="divide-y divide-gray-50">
51
- {#each $recentPosts.data?.data ?? [] as post, _i (_i)}
52
- <div class="flex items-center justify-between px-5 py-3">
53
- <div>
54
- <p class="text-sm font-medium text-gray-900">{post.title}</p>
55
- <p class="text-xs text-gray-500">User #{post.userId}</p>
43
+ {#if recentPosts.isLoading}
44
+ <DataState state="loading" />
45
+ {:else if recentPosts.isError}
46
+ <DataState state="error" retry={() => { if (!recentPosts.isFetching) void recentPosts.refetch(); }} />
47
+ {:else}
48
+ <div class="divide-y">
49
+ {#each recentPosts.data?.data ?? [] as post, _i (_i)}
50
+ <div class="flex items-center justify-between py-3">
51
+ <div class="min-w-0">
52
+ <p class="break-words text-sm font-medium text-foreground">{post.title}</p>
53
+ <p class="text-xs text-muted-foreground">User #{post.userId}</p>
56
54
  </div>
57
55
  </div>
56
+ {:else}
57
+ <DataState state="empty">
58
+ {#snippet action()}<Button href="#/posts/create">Create post</Button>{/snippet}
59
+ </DataState>
58
60
  {/each}
59
61
  </div>
60
- </div>
62
+ {/if}
63
+ </section>
61
64
 
62
- <div class="rounded-xl border border-gray-200 bg-white shadow-sm">
63
- <div class="flex items-center justify-between border-b border-gray-100 px-5 py-4">
64
- <h2 class="font-semibold text-gray-900">Users</h2>
65
+ <section class="min-w-0">
66
+ <div class="flex items-center justify-between border-b py-3">
67
+ <h2 class="font-semibold text-foreground">Users</h2>
65
68
  <a href="#/users" class="text-sm text-primary hover:underline">View all</a>
66
69
  </div>
67
- <div class="divide-y divide-gray-50">
68
- {#each $recentUsers.data?.data ?? [] as user, _i (_i)}
69
- <div class="flex items-center justify-between px-5 py-3">
70
- <div>
71
- <p class="text-sm font-medium text-gray-900">{user.name}</p>
72
- <p class="text-xs text-gray-500">{user.email}</p>
70
+ {#if recentUsers.isLoading}
71
+ <DataState state="loading" />
72
+ {:else if recentUsers.isError}
73
+ <DataState state="error" retry={() => { if (!recentUsers.isFetching) void recentUsers.refetch(); }} />
74
+ {:else}
75
+ <div class="divide-y">
76
+ {#each recentUsers.data?.data ?? [] as user, _i (_i)}
77
+ <div class="flex items-center justify-between py-3">
78
+ <div class="min-w-0">
79
+ <p class="break-words text-sm font-medium text-foreground">{user.name}</p>
80
+ <p class="break-words text-xs text-muted-foreground">{user.email}</p>
73
81
  </div>
74
82
  </div>
83
+ {:else}
84
+ <DataState state="empty">
85
+ {#snippet action()}<Button href="#/users/create">Create user</Button>{/snippet}
86
+ </DataState>
75
87
  {/each}
76
88
  </div>
77
- </div>
89
+ {/if}
90
+ </section>
78
91
  </div>
79
92
  </div>
@@ -0,0 +1,29 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { defineResource } from '@svadmin/core';
3
+
4
+ export const posts = defineResource('posts', {
5
+ record: Type.Object({
6
+ id: Type.Number(), userId: Type.Number(), title: Type.String(), body: Type.String(),
7
+ }),
8
+ });
9
+ export const users = defineResource('users', {
10
+ record: Type.Object({
11
+ id: Type.Number(), name: Type.String(), username: Type.String(), email: Type.String(),
12
+ phone: Type.String(), website: Type.String(),
13
+ address: Type.Object({
14
+ street: Type.String(), suite: Type.String(), city: Type.String(), zipcode: Type.String(),
15
+ geo: Type.Object({ lat: Type.String(), lng: Type.String() }),
16
+ }),
17
+ company: Type.Object({ name: Type.String(), catchPhrase: Type.String(), bs: Type.String() }),
18
+ }),
19
+ });
20
+ export const comments = defineResource('comments', {
21
+ record: Type.Object({
22
+ id: Type.Number(), postId: Type.Number(), name: Type.String(), email: Type.String(), body: Type.String(),
23
+ }),
24
+ });
25
+ export const todos = defineResource('todos', {
26
+ record: Type.Object({
27
+ id: Type.Number(), userId: Type.Number(), title: Type.String(), completed: Type.Boolean(),
28
+ }),
29
+ });
@@ -1,10 +1,12 @@
1
1
  // Example resource definitions — generic demo data (JSONPlaceholder-style)
2
2
 
3
3
  import type { ResourceDefinition } from '@svadmin/core';
4
+ import { posts, users, comments, todos } from './resource-contracts';
4
5
 
5
6
  export const resources: ResourceDefinition[] = [
6
7
  {
7
8
  name: 'posts',
9
+ contract: posts,
8
10
  label: 'Posts',
9
11
  icon: 'file-text',
10
12
  fields: [
@@ -18,6 +20,7 @@ export const resources: ResourceDefinition[] = [
18
20
 
19
21
  {
20
22
  name: 'users',
23
+ contract: users,
21
24
  label: 'Users',
22
25
  icon: 'users',
23
26
  canCreate: false,
@@ -34,6 +37,7 @@ export const resources: ResourceDefinition[] = [
34
37
 
35
38
  {
36
39
  name: 'comments',
40
+ contract: comments,
37
41
  label: 'Comments',
38
42
  icon: 'message-circle',
39
43
  fields: [
@@ -48,6 +52,7 @@ export const resources: ResourceDefinition[] = [
48
52
 
49
53
  {
50
54
  name: 'todos',
55
+ contract: todos,
51
56
  label: 'Todos',
52
57
  icon: 'check-square',
53
58
  fields: [
@@ -1,10 +1,8 @@
1
1
  import { defineConfig } from 'vite';
2
2
  import { svelte } from '@sveltejs/vite-plugin-svelte';
3
- import tailwindcss from '@tailwindcss/vite';
4
3
 
5
4
  export default defineConfig({
6
5
  plugins: [
7
- tailwindcss(),
8
6
  svelte(),
9
7
  ],
10
8
  server: {