@sparkerp/plugin-sdk 0.1.0 → 1.1.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.
Files changed (176) hide show
  1. package/bundle/blocks.json +169 -0
  2. package/bundle/catalog.json +614 -5
  3. package/bundle/docs/applications/hcm/admin-access-policies.md +38 -0
  4. package/bundle/docs/applications/hcm/admin-approval-hierarchies.md +33 -0
  5. package/bundle/docs/applications/hcm/admin-data-transfer.md +40 -0
  6. package/bundle/docs/applications/hcm/admin-hcm-users.md +35 -0
  7. package/bundle/docs/applications/hcm/admin-logs.md +57 -0
  8. package/bundle/docs/applications/hcm/admin-permissions-catalog.md +64 -0
  9. package/bundle/docs/applications/hcm/ai-intelligence-overview.md +37 -0
  10. package/bundle/docs/applications/hcm/ai-intelligence-people-risk.md +33 -0
  11. package/bundle/docs/applications/hcm/ai-intelligence-recruitment.md +37 -0
  12. package/bundle/docs/applications/hcm/ai-intelligence-tools.md +39 -0
  13. package/bundle/docs/applications/hcm/analytics-operations.md +31 -0
  14. package/bundle/docs/applications/hcm/analytics-overview.md +31 -0
  15. package/bundle/docs/applications/hcm/analytics-people.md +36 -0
  16. package/bundle/docs/applications/hcm/analytics-tools.md +35 -0
  17. package/bundle/docs/applications/hcm/assets-audits.md +30 -0
  18. package/bundle/docs/applications/hcm/assets-custody.md +36 -0
  19. package/bundle/docs/applications/hcm/assets-inventory.md +34 -0
  20. package/bundle/docs/applications/hcm/assets-maintenance.md +25 -0
  21. package/bundle/docs/applications/hcm/assets-software-licenses.md +27 -0
  22. package/bundle/docs/applications/hcm/attendance-core.md +46 -0
  23. package/bundle/docs/applications/hcm/attendance-exceptions.md +33 -0
  24. package/bundle/docs/applications/hcm/attendance-location.md +25 -0
  25. package/bundle/docs/applications/hcm/attendance-policies.md +23 -0
  26. package/bundle/docs/applications/hcm/attendance-reports.md +20 -0
  27. package/bundle/docs/applications/hcm/benefits-allowances.md +39 -0
  28. package/bundle/docs/applications/hcm/benefits-analytics.md +25 -0
  29. package/bundle/docs/applications/hcm/benefits-employee-processes.md +44 -0
  30. package/bundle/docs/applications/hcm/benefits-plans.md +60 -0
  31. package/bundle/docs/applications/hcm/communications-announcements.md +31 -0
  32. package/bundle/docs/applications/hcm/communications-history.md +23 -0
  33. package/bundle/docs/applications/hcm/communications-notifications.md +31 -0
  34. package/bundle/docs/applications/hcm/communications-templates.md +35 -0
  35. package/bundle/docs/applications/hcm/compensation-allowances-benefits.md +44 -0
  36. package/bundle/docs/applications/hcm/compensation-analytics.md +30 -0
  37. package/bundle/docs/applications/hcm/compensation-bonus-incentive.md +35 -0
  38. package/bundle/docs/applications/hcm/compensation-cycles.md +49 -0
  39. package/bundle/docs/applications/hcm/compensation-equity.md +36 -0
  40. package/bundle/docs/applications/hcm/compensation-salary.md +47 -0
  41. package/bundle/docs/applications/hcm/compliance-analytics.md +25 -0
  42. package/bundle/docs/applications/hcm/compliance-audits-calendar.md +52 -0
  43. package/bundle/docs/applications/hcm/compliance-employee-tracking.md +41 -0
  44. package/bundle/docs/applications/hcm/compliance-regulatory.md +31 -0
  45. package/bundle/docs/applications/hcm/country-packs-admin.md +33 -0
  46. package/bundle/docs/applications/hcm/country-packs-compliance.md +31 -0
  47. package/bundle/docs/applications/hcm/country-packs-localization.md +37 -0
  48. package/bundle/docs/applications/hcm/country-packs.md +102 -0
  49. package/bundle/docs/applications/hcm/dashboard.md +38 -0
  50. package/bundle/docs/applications/hcm/designations.md +32 -0
  51. package/bundle/docs/applications/hcm/emp-documents-analytics.md +27 -0
  52. package/bundle/docs/applications/hcm/emp-documents-core.md +33 -0
  53. package/bundle/docs/applications/hcm/emp-documents-letters.md +40 -0
  54. package/bundle/docs/applications/hcm/emp-documents-signatures.md +24 -0
  55. package/bundle/docs/applications/hcm/emp-documents-workflow.md +32 -0
  56. package/bundle/docs/applications/hcm/employee-directory.md +55 -0
  57. package/bundle/docs/applications/hcm/employee-documents.md +51 -0
  58. package/bundle/docs/applications/hcm/employee-info-background.md +41 -0
  59. package/bundle/docs/applications/hcm/employee-info-core.md +32 -0
  60. package/bundle/docs/applications/hcm/employee-info-documents.md +31 -0
  61. package/bundle/docs/applications/hcm/employee-info-other.md +21 -0
  62. package/bundle/docs/applications/hcm/employee-movements.md +46 -0
  63. package/bundle/docs/applications/hcm/employee-reference-data.md +40 -0
  64. package/bundle/docs/applications/hcm/employee-relations-analytics.md +23 -0
  65. package/bundle/docs/applications/hcm/employee-relations-cases.md +26 -0
  66. package/bundle/docs/applications/hcm/employee-relations-conflicts-feedback.md +25 -0
  67. package/bundle/docs/applications/hcm/employee-relations-processes.md +34 -0
  68. package/bundle/docs/applications/hcm/employee-services-daily.md +32 -0
  69. package/bundle/docs/applications/hcm/employee-services-manager.md +21 -0
  70. package/bundle/docs/applications/hcm/employee-services-more.md +41 -0
  71. package/bundle/docs/applications/hcm/employee-services-overview.md +43 -0
  72. package/bundle/docs/applications/hcm/expenses-analytics.md +26 -0
  73. package/bundle/docs/applications/hcm/expenses-claims.md +47 -0
  74. package/bundle/docs/applications/hcm/expenses-self-service.md +30 -0
  75. package/bundle/docs/applications/hcm/expenses-setup.md +43 -0
  76. package/bundle/docs/applications/hcm/health-safety-analytics.md +26 -0
  77. package/bundle/docs/applications/hcm/health-safety-incidents.md +44 -0
  78. package/bundle/docs/applications/hcm/health-safety-medical.md +25 -0
  79. package/bundle/docs/applications/hcm/health-safety-training.md +29 -0
  80. package/bundle/docs/applications/hcm/helpdesk.md +28 -0
  81. package/bundle/docs/applications/hcm/holiday-calendar.md +69 -0
  82. package/bundle/docs/applications/hcm/hr-policies.md +64 -0
  83. package/bundle/docs/applications/hcm/hr-settings.md +98 -0
  84. package/bundle/docs/applications/hcm/index.md +272 -0
  85. package/bundle/docs/applications/hcm/industry-it-software.md +52 -0
  86. package/bundle/docs/applications/hcm/job-classifications.md +48 -0
  87. package/bundle/docs/applications/hcm/learning-analytics.md +21 -0
  88. package/bundle/docs/applications/hcm/learning-assessments.md +22 -0
  89. package/bundle/docs/applications/hcm/learning-catalog.md +25 -0
  90. package/bundle/docs/applications/hcm/learning-certifications.md +27 -0
  91. package/bundle/docs/applications/hcm/learning-enrollment.md +26 -0
  92. package/bundle/docs/applications/hcm/learning-instructors-providers.md +21 -0
  93. package/bundle/docs/applications/hcm/leave-accrual-and-carryforward.md +30 -0
  94. package/bundle/docs/applications/hcm/leave-balance.md +26 -0
  95. package/bundle/docs/applications/hcm/leave-dashboard-and-reports.md +32 -0
  96. package/bundle/docs/applications/hcm/leave-encashment.md +27 -0
  97. package/bundle/docs/applications/hcm/leave-requests.md +26 -0
  98. package/bundle/docs/applications/hcm/leave-setup.md +30 -0
  99. package/bundle/docs/applications/hcm/leave-team-and-calendar.md +25 -0
  100. package/bundle/docs/applications/hcm/navigation-and-approvals.md +61 -0
  101. package/bundle/docs/applications/hcm/offboarding-and-exit.md +71 -0
  102. package/bundle/docs/applications/hcm/onboarding-documents-verification.md +43 -0
  103. package/bundle/docs/applications/hcm/onboarding-orientation-probation.md +65 -0
  104. package/bundle/docs/applications/hcm/onboarding-overview.md +47 -0
  105. package/bundle/docs/applications/hcm/onboarding-provisioning-assets.md +48 -0
  106. package/bundle/docs/applications/hcm/onboarding-reports.md +38 -0
  107. package/bundle/docs/applications/hcm/onboarding-templates-checklists.md +38 -0
  108. package/bundle/docs/applications/hcm/onboarding-to-confirmation.md +45 -0
  109. package/bundle/docs/applications/hcm/org-structure.md +53 -0
  110. package/bundle/docs/applications/hcm/org-units.md +86 -0
  111. package/bundle/docs/applications/hcm/organizations.md +81 -0
  112. package/bundle/docs/applications/hcm/payroll-analytics.md +31 -0
  113. package/bundle/docs/applications/hcm/payroll-post-run.md +40 -0
  114. package/bundle/docs/applications/hcm/payroll-runs.md +30 -0
  115. package/bundle/docs/applications/hcm/payroll-setup.md +38 -0
  116. package/bundle/docs/applications/hcm/payroll-tax.md +21 -0
  117. package/bundle/docs/applications/hcm/payroll-transactions.md +43 -0
  118. package/bundle/docs/applications/hcm/performance-analytics.md +23 -0
  119. package/bundle/docs/applications/hcm/performance-appraisals.md +50 -0
  120. package/bundle/docs/applications/hcm/performance-continuous-feedback.md +23 -0
  121. package/bundle/docs/applications/hcm/performance-cycles-and-goals.md +54 -0
  122. package/bundle/docs/applications/hcm/performance-okrs.md +33 -0
  123. package/bundle/docs/applications/hcm/performance-pips.md +29 -0
  124. package/bundle/docs/applications/hcm/performance-ratings.md +25 -0
  125. package/bundle/docs/applications/hcm/positions.md +67 -0
  126. package/bundle/docs/applications/hcm/recruitment-agencies-and-sources.md +26 -0
  127. package/bundle/docs/applications/hcm/recruitment-analytics.md +25 -0
  128. package/bundle/docs/applications/hcm/recruitment-candidates.md +33 -0
  129. package/bundle/docs/applications/hcm/recruitment-offers.md +32 -0
  130. package/bundle/docs/applications/hcm/recruitment-pipeline.md +46 -0
  131. package/bundle/docs/applications/hcm/recruitment-requisitions-and-openings.md +33 -0
  132. package/bundle/docs/applications/hcm/roles-permissions.md +129 -0
  133. package/bundle/docs/applications/hcm/separation-clearance.md +33 -0
  134. package/bundle/docs/applications/hcm/separation-documents.md +28 -0
  135. package/bundle/docs/applications/hcm/separation-final-settlement.md +24 -0
  136. package/bundle/docs/applications/hcm/separation-reports.md +23 -0
  137. package/bundle/docs/applications/hcm/separation-resignation.md +43 -0
  138. package/bundle/docs/applications/hcm/settings-extensibility.md +31 -0
  139. package/bundle/docs/applications/hcm/settings-general.md +29 -0
  140. package/bundle/docs/applications/hcm/settings-integrations.md +14 -0
  141. package/bundle/docs/applications/hcm/settings-module-defaults.md +41 -0
  142. package/bundle/docs/applications/hcm/settings-process.md +28 -0
  143. package/bundle/docs/applications/hcm/shift-scheduling.md +33 -0
  144. package/bundle/docs/applications/hcm/talent-analytics.md +26 -0
  145. package/bundle/docs/applications/hcm/talent-career.md +31 -0
  146. package/bundle/docs/applications/hcm/talent-competencies-skills.md +30 -0
  147. package/bundle/docs/applications/hcm/talent-profiles.md +40 -0
  148. package/bundle/docs/applications/hcm/talent-succession.md +42 -0
  149. package/bundle/docs/applications/hcm/teams-and-tags.md +46 -0
  150. package/bundle/docs/applications/hcm/timesheets.md +33 -0
  151. package/bundle/docs/applications/hcm/travel-advances-expenses.md +34 -0
  152. package/bundle/docs/applications/hcm/travel-analytics.md +28 -0
  153. package/bundle/docs/applications/hcm/travel-bookings.md +35 -0
  154. package/bundle/docs/applications/hcm/travel-requests.md +38 -0
  155. package/bundle/docs/applications/hcm/workforce-org-design.md +22 -0
  156. package/bundle/docs/applications/hcm/workforce-planning-analytics.md +26 -0
  157. package/bundle/docs/applications/hcm/workforce-planning-core.md +36 -0
  158. package/bundle/docs/applications/hcm/workforce-planning-scenarios.md +29 -0
  159. package/bundle/docs/docs.json +1 -0
  160. package/bundle/docs/guides/add-app-owned-roles-and-permissions.md +144 -0
  161. package/bundle/docs/guides/checkout-an-installed-plugin.md +151 -0
  162. package/bundle/docs/guides/extend-a-shipped-application.md +132 -0
  163. package/bundle/docs/guides/index.md +3 -0
  164. package/bundle/docs/reference/entity-aggregation-config.md +1 -1
  165. package/bundle/docs/reference/entity-document-generator-config.md +1 -1
  166. package/bundle/docs/tutorial/01-create-the-plugin.md +7 -1
  167. package/bundle/docs/tutorial/07-return-due-reminder-job.md +28 -5
  168. package/bundle/docs/tutorial/08-menus-i18n-publish.md +5 -5
  169. package/bundle/manifest.json +4 -4
  170. package/bundle/schemas/page.schema.json +13 -0
  171. package/bundle/schemas/plugin-manifest.schema.json +13 -0
  172. package/bundle/validators/block-engine.mjs +167 -7
  173. package/bundle/validators/page-engine.mjs +226 -21
  174. package/erp-cli/authoring-root.mjs +12 -1
  175. package/erp-cli/erp.mjs +679 -15
  176. package/package.json +1 -1
@@ -3061,6 +3061,25 @@ function endsWith(args) {
3061
3061
  const [s, suffix] = args;
3062
3062
  return rvBool(str(s).endsWith(str(suffix)));
3063
3063
  }
3064
+ function displayString(v) {
3065
+ switch (v.type) {
3066
+ case "String":
3067
+ return v.value;
3068
+ case "Integer":
3069
+ return v.value.toString();
3070
+ case "Decimal":
3071
+ return v.value.toString();
3072
+ case "Boolean":
3073
+ return String(v.value);
3074
+ case "Null":
3075
+ return "";
3076
+ default:
3077
+ throw new EvaluationError(`concat() does not support ${v.type} operands`);
3078
+ }
3079
+ }
3080
+ function concatFn(args) {
3081
+ return rvString(args.map(displayString).join(""));
3082
+ }
3064
3083
  function registerStringFunctions(registry, version) {
3065
3084
  registry.register(version, "upper", upper);
3066
3085
  registry.register(version, "lower", lower);
@@ -3069,6 +3088,7 @@ function registerStringFunctions(registry, version) {
3069
3088
  registry.register(version, "substring", substring);
3070
3089
  registry.register(version, "startsWith", startsWith);
3071
3090
  registry.register(version, "endsWith", endsWith);
3091
+ registry.register(version, "concat", concatFn);
3072
3092
  }
3073
3093
  function contains(args) {
3074
3094
  const [collection, needle] = args;
@@ -3115,12 +3135,19 @@ function size(args) {
3115
3135
  function isEmpty(args) {
3116
3136
  return rvBool(sizeOf(args[0]) === 0);
3117
3137
  }
3138
+ function pluck(args) {
3139
+ const [arr, key] = args;
3140
+ if (arr.type !== "Array") throw new EvaluationError(`pluck() expects an Array as its first argument, got ${arr.type}`);
3141
+ if (key.type !== "String") throw new EvaluationError("pluck() expects a String field name as its second argument");
3142
+ return rvArray(arr.value.map((row) => row.type === "Object" ? row.value[key.value] ?? RV_NULL : RV_NULL));
3143
+ }
3118
3144
  function registerCollectionFunctions(registry, version) {
3119
3145
  registry.register(version, "contains", contains);
3120
3146
  registry.register(version, "first", first);
3121
3147
  registry.register(version, "last", last);
3122
3148
  registry.register(version, "size", size);
3123
3149
  registry.register(version, "isEmpty", isEmpty);
3150
+ registry.register(version, "pluck", pluck);
3124
3151
  }
3125
3152
  function ifImpl(args) {
3126
3153
  const [cond, thenValue, elseValue] = args;
@@ -3193,12 +3220,14 @@ var FUNCTION_CATALOG = [
3193
3220
  c({ name: "substring", minArity: 2, maxArity: 3, argKinds: ["String", "Numeric", "Numeric"], returnType: "String", category: "String", description: "Part of a string from a 0-based start position, optionally limited to a length." }),
3194
3221
  c({ name: "startsWith", minArity: 2, maxArity: 2, argKinds: ["String", "String"], returnType: "Boolean", category: "String", description: "True when the string begins with the given prefix." }),
3195
3222
  c({ name: "endsWith", minArity: 2, maxArity: 2, argKinds: ["String", "String"], returnType: "Boolean", category: "String", description: "True when the string ends with the given suffix." }),
3223
+ c({ name: "concat", minArity: 2, maxArity: Infinity, argKinds: ["Any"], returnType: "String", category: "String", description: `Joins 2+ values' display text into one string, e.g. concat("Total: ", page.count).` }),
3196
3224
  // Collection
3197
3225
  c({ name: "contains", minArity: 2, maxArity: 2, argKinds: ["Collection", "Any"], returnType: "Boolean", category: "Collection", description: "True when a list, string, or object contains the given element/substring/key." }),
3198
3226
  c({ name: "first", minArity: 1, maxArity: 1, argKinds: ["Array"], returnType: "Any", category: "Collection", description: "The first element of a list." }),
3199
3227
  c({ name: "last", minArity: 1, maxArity: 1, argKinds: ["Array"], returnType: "Any", category: "Collection", description: "The last element of a list." }),
3200
3228
  c({ name: "size", minArity: 1, maxArity: 1, argKinds: ["Collection"], returnType: "Integer", category: "Collection", description: "Number of elements in a list, characters in a string, or keys in an object." }),
3201
3229
  c({ name: "isEmpty", minArity: 1, maxArity: 1, argKinds: ["Collection"], returnType: "Boolean", category: "Collection", description: "True when a list, string, or object has no elements." }),
3230
+ c({ name: "pluck", minArity: 2, maxArity: 2, argKinds: ["Array", "String"], returnType: "Array", category: "Collection", description: `pluck(rows, "fieldName") \u2014 a new list of that field's value from each object in the list, e.g. turning [{date, value}, ...] into [value, value, ...].` }),
3202
3231
  // Utility
3203
3232
  c({ name: "if", minArity: 3, maxArity: 3, argKinds: ["Boolean", "Any", "Any"], returnType: "Any", category: "Utility", description: "if(condition, thenValue, elseValue) \u2014 returns one of two values based on a condition." }),
3204
3233
  c({ name: "coalesce", minArity: 1, maxArity: Infinity, argKinds: ["Any"], returnType: "Any", category: "Utility", description: "The first argument that is not null." }),
@@ -4425,9 +4454,9 @@ function resolvePath(scopes, rawPath) {
4425
4454
  return value;
4426
4455
  }
4427
4456
  function resolveString(scopes, text) {
4428
- const pluck = PLUCK_TEMPLATE.exec(text);
4429
- if (pluck) {
4430
- const [, path, field] = pluck;
4457
+ const pluck2 = PLUCK_TEMPLATE.exec(text);
4458
+ if (pluck2) {
4459
+ const [, path, field] = pluck2;
4431
4460
  const resolved = runtimeValueToJs(resolvePath(scopes, path));
4432
4461
  if (!Array.isArray(resolved)) {
4433
4462
  throw new ActionError("unresolved-variable", `'${path}|pluck:${field}' requires an array at '${path}'`);
@@ -10320,6 +10349,26 @@ ${failedSteps}` : "")
10320
10349
  });
10321
10350
  if (faulted && this.phase === "ready") this.present();
10322
10351
  }
10352
+ /**
10353
+ * A real, previously-undiscovered gap (2026-09-22, found live authoring the HCM Permissions
10354
+ * "Create Permission" dialog): a `core.text-input` (or any other inputClass block) whose page
10355
+ * JSON only ever sets `properties.value.binding` — the "obviously two-way" way EVERY such field
10356
+ * in this codebase's own already-shipped pages is authored (`hcm-users.json`'s Create User
10357
+ * dialog included) — never actually wrote a keystroke back to that bound page-scope key. `commit`
10358
+ * → `writeOutput` only ever consulted `config.outputs[name]`, a SEPARATE, explicit redirect
10359
+ * nothing in this codebase's own page-authoring convention ever sets; absent that, every write
10360
+ * landed in a `{scope:"block", scopeKey:this.instanceId}` bucket no page action chain can ever
10361
+ * read — the DOM still showed the typed characters (via `RevealableTextField`'s own local echo
10362
+ * state), so this was invisible in every purely-visual check, only surfacing when a create
10363
+ * dialog's Submit button's `${page.createX}` params were inspected against the REAL network
10364
+ * payload, which showed every field empty. Fixed by falling back to the SAME resolved binding
10365
+ * target `resolveProperties()` already computed for the identically-named PROPERTY's own read
10366
+ * side (`this.boundProps`) when no explicit `config.outputs[name]` redirect exists — making a
10367
+ * `source:"binding"` property's own binding genuinely two-way by default, matching what every
10368
+ * page author already (reasonably) assumed it meant. An explicit `config.outputs[name]` still
10369
+ * wins unchanged (e.g. `core.file-upload`'s own redirect to a different scope/key than its
10370
+ * `value` property reads from); this only fills in the previously-silent gap.
10371
+ */
10323
10372
  async writeOutput(name, value) {
10324
10373
  const port = (this.type.definition.outputs ?? []).find((o) => o.name === name);
10325
10374
  if (!port) {
@@ -10327,7 +10376,7 @@ ${failedSteps}` : "")
10327
10376
  return;
10328
10377
  }
10329
10378
  const redirect = this.config.outputs?.[name];
10330
- const target = redirect ? resolveBoundKey(redirect, this.bindingContext()) : { scope: "block", scopeKey: this.instanceId, key: name };
10379
+ const target = redirect ? resolveBoundKey(redirect, this.bindingContext()) : this.boundProps.get(name) ?? { scope: "block", scopeKey: this.instanceId, key: name };
10331
10380
  await this.ctx.state.set(target, value);
10332
10381
  }
10333
10382
  /* ---------------- error isolation & standard states (W2-04) ---------------- */
@@ -10918,6 +10967,47 @@ var imageDefinition = definition({
10918
10967
  type: "string",
10919
10968
  sources: ["static"],
10920
10969
  designer: { group: "content", editor: "text", labelKey: "core.image.property.height" }
10970
+ },
10971
+ {
10972
+ // 2026-09-17 (page-builder "Customize" panel parity) — a clip-path-class
10973
+ // shape, the same fixed vocabulary a real page-builder's own "Shape"
10974
+ // picker offers (never an arbitrary path — that stays a `style`
10975
+ // escape-hatch concern).
10976
+ name: "shape",
10977
+ type: "string",
10978
+ sources: ["static"],
10979
+ default: "none",
10980
+ constraints: { options: [{ value: "none", labelKey: "core.image.shape.none" }, { value: "circle", labelKey: "core.image.shape.circle" }, { value: "rounded", labelKey: "core.image.shape.rounded" }] },
10981
+ designer: { group: "content", editor: "select", labelKey: "core.image.property.shape" }
10982
+ },
10983
+ {
10984
+ /** A native browser tooltip (HTML `title` attribute) — distinct from `alt` (accessibility text, always present) and from a rich hover-card (out of scope here, a real different feature). */
10985
+ name: "tooltip",
10986
+ type: "string",
10987
+ sources: ["static"],
10988
+ designer: { group: "content", editor: "text", labelKey: "core.image.property.tooltip" }
10989
+ },
10990
+ {
10991
+ /** A raw CSS `transform` value, e.g. "rotate(4deg)" — same "text travels as data" posture as `PageTheme.customCss`, never `dangerouslySetInnerHTML`, just an inline style value. */
10992
+ name: "transform",
10993
+ type: "string",
10994
+ sources: ["static"],
10995
+ designer: { group: "content", editor: "text", labelKey: "core.image.property.transform" }
10996
+ },
10997
+ {
10998
+ name: "filter",
10999
+ type: "string",
11000
+ sources: ["static"],
11001
+ default: "none",
11002
+ constraints: {
11003
+ options: [
11004
+ { value: "none", labelKey: "core.image.filter.none" },
11005
+ { value: "grayscale", labelKey: "core.image.filter.grayscale" },
11006
+ { value: "sepia", labelKey: "core.image.filter.sepia" },
11007
+ { value: "blur", labelKey: "core.image.filter.blur" }
11008
+ ]
11009
+ },
11010
+ designer: { group: "content", editor: "select", labelKey: "core.image.property.filter" }
10921
11011
  }
10922
11012
  ],
10923
11013
  events: [],
@@ -13562,6 +13652,31 @@ var gridDefinition = definition({
13562
13652
  designer: { group: "data", editor: "binding", labelKey: "core.grid.property.refreshTrigger" }
13563
13653
  }
13564
13654
  ],
13655
+ // 2026-09-23 — these are the block's own internal state (written via
13656
+ // `io.setOwnState` in this file's `query()`/handlers, read back into
13657
+ // `node.props` for the adapter), not authored properties: a page author
13658
+ // never sets them, but a plugin/theme developer inspecting `core.grid`
13659
+ // via `erp blocks list --type core.grid` had no way to discover they
13660
+ // exist at all — the empty-grid production bug this session traced back
13661
+ // to exactly this state (rows/total both came from the same query
13662
+ // response, one reached the render, the other didn't) took a live
13663
+ // Playwright session plus a source read to pin down, when this list
13664
+ // alone would have shown the full internal-state surface immediately.
13665
+ outputs: [
13666
+ { name: "rows", type: "json" },
13667
+ { name: "total", type: "number" },
13668
+ { name: "pageIndex", type: "number" },
13669
+ { name: "sort", type: "json" },
13670
+ { name: "filter", type: "json" },
13671
+ { name: "groups", type: "json" },
13672
+ { name: "aggregates", type: "json" },
13673
+ { name: "expandedGroups", type: "json" },
13674
+ { name: "error", type: "string" },
13675
+ { name: "circuitOpen", type: "boolean" },
13676
+ { name: "selectedIds", type: "json" },
13677
+ { name: "density", type: "string" },
13678
+ { name: "groupBy", type: "string" }
13679
+ ],
13565
13680
  events: [
13566
13681
  { name: "selectionChanged", catalogType: "GridSelectionChanged" },
13567
13682
  { name: "rowClicked", catalogType: "GridRowClicked" },
@@ -14311,6 +14426,30 @@ var fileUploadDefinition = definition({
14311
14426
  type: "string",
14312
14427
  sources: ["static", "expression"],
14313
14428
  designer: { group: "data", editor: "text", labelKey: "core.file-upload.property.cabinetId" }
14429
+ },
14430
+ {
14431
+ // 2026-09-21 (HCM Users spec gap-closing pass) — every pre-existing
14432
+ // page never sets this (undefined === "dms"), so it's a zero-risk
14433
+ // addition: `"dms"` keeps today's exact behavior (a real DMS cabinet
14434
+ // upload via the injected `FileUploadSource`, `value` becomes real
14435
+ // `UploadedFileRef`s). `"raw"` skips the DMS upload entirely — `value`
14436
+ // becomes the picked file's own `{filename, contentType, dataUrl}`
14437
+ // straight from the browser's file picker, with no `id`/`url` (nothing
14438
+ // was uploaded anywhere) — meant to feed a `callApi` action's
14439
+ // `multipart: true` body (see that handler's own doc comment), the
14440
+ // real fix for the disclosed "core.file-upload cannot emit raw bytes
14441
+ // into a callApi body" gap (`ai/patterns/import.md`).
14442
+ name: "mode",
14443
+ type: "string",
14444
+ sources: ["static"],
14445
+ default: "dms",
14446
+ constraints: {
14447
+ options: [
14448
+ { value: "dms", labelKey: "core.file-upload.mode.dms" },
14449
+ { value: "raw", labelKey: "core.file-upload.mode.raw" }
14450
+ ]
14451
+ },
14452
+ designer: { group: "behavior", editor: "select", labelKey: "core.file-upload.property.mode" }
14314
14453
  }
14315
14454
  ],
14316
14455
  outputs: [{ name: "value", type: "json" }],
@@ -14323,15 +14462,23 @@ var fileUploadDefinition = definition({
14323
14462
  category: "input",
14324
14463
  propertyGroups: [
14325
14464
  { id: "data", titleKey: "core.designer.group.data" },
14326
- { id: "content", titleKey: "core.designer.group.content" }
14465
+ { id: "content", titleKey: "core.designer.group.content" },
14466
+ { id: "behavior", titleKey: "core.designer.group.behavior" }
14327
14467
  ],
14328
14468
  allowedTargets: ["page", "form", "container", "dashboard"],
14329
14469
  preview: { kind: "field" }
14330
14470
  }
14331
14471
  });
14472
+ var rawFileIdCounter = 0;
14473
+ function nextRawFileId() {
14474
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
14475
+ rawFileIdCounter += 1;
14476
+ return `raw-file-${Date.now()}-${rawFileIdCounter}`;
14477
+ }
14332
14478
  function createFileUploadLogic(uploadSource) {
14333
14479
  return {
14334
14480
  render(io) {
14481
+ const mode = io.props["mode"] === "raw" ? "raw" : "dms";
14335
14482
  const files = Array.isArray(io.props["value"]) ? io.props["value"] : [];
14336
14483
  const accept = typeof io.props["accept"] === "string" ? io.props["accept"] : void 0;
14337
14484
  const multiple = io.props["multiple"] !== false;
@@ -14340,8 +14487,17 @@ function createFileUploadLogic(uploadSource) {
14340
14487
  props: { value: files, accept, multiple },
14341
14488
  handlers: {
14342
14489
  upload: async (payload) => {
14343
- if (!uploadSource) return;
14344
14490
  const incoming = payload ?? [];
14491
+ if (mode === "raw") {
14492
+ await io.runLoading(async () => {
14493
+ const picked = incoming.map((f) => ({ id: nextRawFileId(), ...f }));
14494
+ const next = [...files, ...picked];
14495
+ await io.writeOutput("value", next);
14496
+ await io.emit("committed", { field: "value", old: files, new: next });
14497
+ });
14498
+ return;
14499
+ }
14500
+ if (!uploadSource) return;
14345
14501
  await io.runLoading(async () => {
14346
14502
  const uploaded = [];
14347
14503
  for (const f of incoming) uploaded.push(await uploadSource.upload(f, cabinetId));
@@ -18681,6 +18837,7 @@ async function applyRuleBindings(definition2, changedField, options) {
18681
18837
  continue;
18682
18838
  }
18683
18839
  mergeBlockEntry(options.scopes, "fieldOptions", rule.targetId, result);
18840
+ options.scopes.set("form", `__fieldOptions.${rule.targetId}`, result);
18684
18841
  let optionsReset = false;
18685
18842
  if (rule.resetOnChange) {
18686
18843
  const current2 = options.scopes.values("form")[rule.targetId];
@@ -18963,7 +19120,7 @@ var FormPermissionEvaluator = class {
18963
19120
  return this.base.masked(subject) || this.staticSurface(subject.instanceId).masked;
18964
19121
  }
18965
19122
  };
18966
- function fieldProperties(field, value, blockDef) {
19123
+ function fieldProperties(field, value, blockDef, ctx, definition2) {
18967
19124
  const portName = blockDef.outputs?.[0]?.name ?? (blockDef.type === "core.label" ? "text" : "value");
18968
19125
  const props = { [portName]: value };
18969
19126
  const c2 = field.constraints;
@@ -18979,7 +19136,13 @@ function fieldProperties(field, value, blockDef) {
18979
19136
  maybe("icon", c2.icon);
18980
19137
  maybe("revealToggle", c2.revealToggle);
18981
19138
  if (c2.options && blockDef.properties.some((p) => p.name === "options")) {
18982
- props["options"] = { source: "static", value: c2.options };
19139
+ const dynamicRule = (definition2.rules ?? []).find((r) => r.output === "options" && r.targetKind === "field" && r.targetId === field.name);
19140
+ if (dynamicRule) {
19141
+ ctx.scopes.set("form", `__fieldOptions.${field.name}`, jsToRuntimeValue(c2.options));
19142
+ props["options"] = { source: "binding", binding: { scope: "form", key: `__fieldOptions.${field.name}` } };
19143
+ } else {
19144
+ props["options"] = { source: "static", value: c2.options };
19145
+ }
18983
19146
  }
18984
19147
  }
18985
19148
  return props;
@@ -18989,7 +19152,7 @@ function fieldOutputs(field, blockDef) {
18989
19152
  const portName = blockDef.outputs?.[0]?.name;
18990
19153
  return portName ? { [portName]: { scope: "form", key: field.name } } : void 0;
18991
19154
  }
18992
- async function mountField(field, ctx, registry) {
19155
+ async function mountField(field, ctx, registry, definition2) {
18993
19156
  const blockType = field.kind === "computed" ? "core.label" : field.blockType;
18994
19157
  if (!blockType) {
18995
19158
  return { ok: false, diagnostics: [formDiagnostic("unknown-field", `Field "${field.name}" declares no blockType`, { path: field.name })] };
@@ -19007,7 +19170,7 @@ async function mountField(field, ctx, registry) {
19007
19170
  instanceId: field.name,
19008
19171
  blockType,
19009
19172
  blockVersion: registered.definition.version,
19010
- properties: fieldProperties(field, value, registered.definition),
19173
+ properties: fieldProperties(field, value, registered.definition, ctx, definition2),
19011
19174
  outputs: fieldOutputs(field, registered.definition),
19012
19175
  // W10: the authored labelKey becomes the instance's accessible name,
19013
19176
  // resolved per locale by the runtime i18n resolver at render.
@@ -19111,7 +19274,7 @@ async function mountForm(options) {
19111
19274
  const fieldInstances = /* @__PURE__ */ new Map();
19112
19275
  const diagnostics = [];
19113
19276
  for (const field of options.definition.fields) {
19114
- const result = field.kind === "child-table" ? await mountChildTable(field, rootCtx, options.blockRegistry) : await mountField(field, rootCtx, options.blockRegistry);
19277
+ const result = field.kind === "child-table" ? await mountChildTable(field, rootCtx, options.blockRegistry) : await mountField(field, rootCtx, options.blockRegistry, options.definition);
19115
19278
  if (!result.ok) {
19116
19279
  diagnostics.push(...result.diagnostics);
19117
19280
  continue;
@@ -19180,6 +19343,8 @@ var FormInstance = class {
19180
19343
  const result = await openForm(this.definition, this.instanceConfig, this.lifecycleOptions());
19181
19344
  this.subscription = this.bus.subscribe(this.tenantId, "ValueCommitted", (e) => this.onValueCommitted(e));
19182
19345
  this.lastSaved = snapshotFormValues(this.scopes);
19346
+ const { effects } = await applyRuleBindings(this.definition, "onLoad", { tenantId: this.tenantId, scopes: this.scopes });
19347
+ await Promise.all(effects.filter((e) => e.targetKind === "field").map((e) => this.fieldInstances.get(e.targetId)?.refresh()));
19183
19348
  this.present();
19184
19349
  return result;
19185
19350
  }
@@ -20244,7 +20409,7 @@ function resolveVariant(definition2, context) {
20244
20409
  }
20245
20410
  return { variantId: void 0, template: definition2.template, slotFills: definition2.slotFills, rows: definition2.rows };
20246
20411
  }
20247
- var XS_BASE = { span: void 0, width: void 0, height: void 0, fontSize: void 0, spacing: void 0 };
20412
+ var XS_BASE = { span: void 0, offset: void 0, width: void 0, height: void 0, fontSize: void 0, spacing: void 0 };
20248
20413
  function resolveBreakpoint(breakpoints, target, hiddenAtXs) {
20249
20414
  const targetIndex = BREAKPOINTS2.indexOf(target);
20250
20415
  let resolved = { ...XS_BASE, hidden: hiddenAtXs ?? false };
@@ -20254,6 +20419,7 @@ function resolveBreakpoint(breakpoints, target, hiddenAtXs) {
20254
20419
  if (!override) continue;
20255
20420
  if (override.hidden !== void 0) resolved = { ...resolved, hidden: override.hidden };
20256
20421
  if (override.span !== void 0) resolved = { ...resolved, span: override.span };
20422
+ if (override.offset !== void 0) resolved = { ...resolved, offset: override.offset };
20257
20423
  if (override.width !== void 0) resolved = { ...resolved, width: override.width };
20258
20424
  if (override.height !== void 0) resolved = { ...resolved, height: override.height };
20259
20425
  if (override.fontSize !== void 0) resolved = { ...resolved, fontSize: override.fontSize };
@@ -20642,7 +20808,8 @@ async function mountPage(options) {
20642
20808
  rows: mountedRows,
20643
20809
  overlays: options.definition.overlays ?? [],
20644
20810
  wizardSteps: wizardSteps === void 0 ? void 0 : mountedSteps,
20645
- canvasItems: mountedCanvasItems
20811
+ canvasItems: mountedCanvasItems,
20812
+ disableEmptyCollapse: options.disableEmptyCollapse
20646
20813
  });
20647
20814
  await instance.open();
20648
20815
  currentPageByRegistry.set(options.actionRegistry, instance);
@@ -20663,6 +20830,7 @@ var PageInstance = class {
20663
20830
  overlays = /* @__PURE__ */ new Map();
20664
20831
  wizardSteps;
20665
20832
  canvasItems;
20833
+ disableEmptyCollapse;
20666
20834
  wizardStep = 0;
20667
20835
  openCounter = 0;
20668
20836
  rowVisible = /* @__PURE__ */ new Map();
@@ -20685,6 +20853,7 @@ var PageInstance = class {
20685
20853
  }
20686
20854
  this.wizardSteps = options.wizardSteps;
20687
20855
  this.canvasItems = options.canvasItems;
20856
+ this.disableEmptyCollapse = options.disableEmptyCollapse ?? false;
20688
20857
  }
20689
20858
  get pageId() {
20690
20859
  return this.ctx.pageId;
@@ -20714,6 +20883,34 @@ var PageInstance = class {
20714
20883
  this.consumeOverlayIntent();
20715
20884
  void this.evaluateVisibility();
20716
20885
  });
20886
+ await this.fireOnLoad();
20887
+ }
20888
+ /**
20889
+ * Page-level `onLoad` (2026-09-22, 12-Column spec §23 gap-closing pass) —
20890
+ * fires once, after the page's own content has mounted and its first
20891
+ * visibility pass has run (so an `onLoad` action's `setValue`s land on a
20892
+ * page whose rows/columns/items already exist to react to them). Mirrors
20893
+ * `runLifecycleHook`'s own `onLoad` branch in `@erp/form-engine` — same
20894
+ * `executeChain` call, same "fire and forget, never a page-open blocker"
20895
+ * posture: a failing/invalid onLoad chain is logged to the console and
20896
+ * swallowed, never thrown back into `mountPage`'s caller — an author
20897
+ * mistake in a page-level hook must not take the whole page down the way
20898
+ * one bad block instance already doesn't (see the diagnostics-collection
20899
+ * loop in `mountPage` above).
20900
+ */
20901
+ async fireOnLoad() {
20902
+ const binding = this.definition.events?.find((e) => e.hook === "onLoad");
20903
+ if (!binding || binding.actions.length === 0) return;
20904
+ try {
20905
+ await executeChain(binding.actions, {
20906
+ tenantId: this.ctx.tenantId,
20907
+ registry: this.deps.actionRegistry,
20908
+ scopes: this.ctx.scopes,
20909
+ resolveConnection: this.ctx.resolveConnection
20910
+ });
20911
+ } catch (e) {
20912
+ console.error(`Page "${this.definition.id}" onLoad action chain failed:`, e);
20913
+ }
20717
20914
  }
20718
20915
  /**
20719
20916
  * Overlay intents (2026-07-16 audit): the Phase 2 `openDialog`/`closeDialog`
@@ -20809,11 +21006,12 @@ var PageInstance = class {
20809
21006
  `page-${this.pageId}`
20810
21007
  );
20811
21008
  }
20812
- /** Empty-region collapse (W2-03, gap 5): a column with no currently-visible items emits no node at all. */
20813
- columnNode(mountedColumn, rowAlign) {
21009
+ /** Empty-region collapse (W2-03, gap 5): a column with no currently-visible items emits no node at all — skipped when `disableEmptyCollapse` is set (see that option's own doc comment). */
21010
+ columnNode(mountedColumn, rowAlign, rowLayout) {
20814
21011
  if (this.columnVisible.get(mountedColumn.column.id) === false) return void 0;
20815
21012
  const itemNodes = mountedColumn.items.filter((i) => this.itemVisible.get(i.item.id) !== false).map((i) => this.itemNode(i));
20816
- if (isEmptyContainer(itemNodes.map(() => true))) return void 0;
21013
+ const empty = isEmptyContainer(itemNodes.map(() => true));
21014
+ if (empty && !this.disableEmptyCollapse) return void 0;
20817
21015
  return node("container", `${mountedColumn.column.id}-wrapper`, {
20818
21016
  props: {
20819
21017
  pageLayoutRole: "column",
@@ -20821,20 +21019,25 @@ var PageInstance = class {
20821
21019
  hiddenAtXs: mountedColumn.column.hiddenAtXs,
20822
21020
  spanAtXs: mountedColumn.column.spanAtXs,
20823
21021
  card: mountedColumn.column.card,
21022
+ style: mountedColumn.column.style,
20824
21023
  align: rowAlign,
20825
- widthOverride: this.columnWidth.get(mountedColumn.column.id)
21024
+ rowLayout,
21025
+ widthOverride: this.columnWidth.get(mountedColumn.column.id),
21026
+ // Only ever true when `disableEmptyCollapse` kept an otherwise-collapsed column alive — the render
21027
+ // adapter's own signal to show a real "drop a component here" placeholder instead of a blank box.
21028
+ emptyPlaceholder: empty && this.disableEmptyCollapse
20826
21029
  },
20827
21030
  children: itemNodes
20828
21031
  });
20829
21032
  }
20830
- /** Empty-region collapse (W2-03, gap 5): a row whose every column collapsed emits no node at all — proven on xs-stacked layout where an empty row would otherwise be a visible hole. */
21033
+ /** Empty-region collapse (W2-03, gap 5): a row whose every column collapsed emits no node at all — proven on xs-stacked layout where an empty row would otherwise be a visible hole; skipped when `disableEmptyCollapse` is set. */
20831
21034
  rowNode(mountedRow) {
20832
21035
  if (!mountedRow.activated) {
20833
21036
  return node("skeleton", `${mountedRow.row.id}-deferred`, { a11y: { role: "status" } });
20834
21037
  }
20835
21038
  if (this.rowVisible.get(mountedRow.row.id) === false) return void 0;
20836
- const columnNodes = mountedRow.columns.map((c2) => this.columnNode(c2, mountedRow.row.align)).filter((n) => n !== void 0);
20837
- if (isEmptyContainer(columnNodes.map(() => true))) return void 0;
21039
+ const columnNodes = mountedRow.columns.map((c2) => this.columnNode(c2, mountedRow.row.align, mountedRow.row.layout)).filter((n) => n !== void 0);
21040
+ if (isEmptyContainer(columnNodes.map(() => true)) && !this.disableEmptyCollapse) return void 0;
20838
21041
  return node("container", `${mountedRow.row.id}-wrapper`, {
20839
21042
  props: {
20840
21043
  pageLayoutRole: "row",
@@ -20842,7 +21045,9 @@ var PageInstance = class {
20842
21045
  hiddenAtXs: mountedRow.row.hiddenAtXs,
20843
21046
  align: mountedRow.row.align,
20844
21047
  edgeSpacing: mountedRow.row.edgeSpacing,
20845
- sticky: mountedRow.row.sticky
21048
+ sticky: mountedRow.row.sticky,
21049
+ style: mountedRow.row.style,
21050
+ layout: mountedRow.row.layout
20846
21051
  },
20847
21052
  children: columnNodes
20848
21053
  });
@@ -246,7 +246,18 @@ export function docsSearchRoots() {
246
246
  if (existsSync(bun)) roots.push({ dir: bun, source: "developer-docs (bundled)", relBase: bun });
247
247
  return roots;
248
248
  }
249
- return [{ dir: path.join(REPO_ROOT, "ai"), source: "ai/domains doc", relBase: REPO_ROOT }];
249
+ // Monorepo mode real bug found 2026-09-22: this used to return ONLY
250
+ // ai/, so every developer-docs/ page (the actual tenant-facing content —
251
+ // what the SDK bundle ships, what erp docs search's own packaged-mode
252
+ // branch above searches) was invisible to `erp docs search`/MCP's
253
+ // `erp_search_docs` while working directly in this repo, the one place a
254
+ // platform contributor is most likely to be adding new pages. Both roots
255
+ // now included; developer-docs/ first since it's the tenant-facing
256
+ // surface this tool exists to help developers not re-derive.
257
+ return [
258
+ { dir: path.join(REPO_ROOT, "developer-docs"), source: "developer-docs", relBase: REPO_ROOT },
259
+ { dir: path.join(REPO_ROOT, "ai"), source: "ai/domains doc", relBase: REPO_ROOT },
260
+ ];
250
261
  }
251
262
 
252
263
  /** backend/modules root for filename-substring example search. */