@mohasinac/appkit 4.14.0 → 4.15.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 (157) hide show
  1. package/dist/_internal/server/features/auctions/actions.js +2 -2
  2. package/dist/_internal/server/features/cart/service.d.ts +3 -3
  3. package/dist/_internal/server/features/checkout/actions.js +19 -0
  4. package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
  5. package/dist/_internal/server/features/checkout/source-context.js +157 -0
  6. package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
  7. package/dist/_internal/server/features/offers/adapters.js +88 -0
  8. package/dist/_internal/server/features/offers/index.d.ts +2 -0
  9. package/dist/_internal/server/features/offers/index.js +1 -0
  10. package/dist/_internal/server/features/orders/adapters.js +24 -0
  11. package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
  12. package/dist/_internal/shared/actions/action-registry.js +3 -1
  13. package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
  14. package/dist/_internal/shared/features/auctions/schema.js +13 -1
  15. package/dist/_internal/shared/features/cart/schema.d.ts +14 -9
  16. package/dist/_internal/shared/features/cart/schema.js +15 -0
  17. package/dist/_internal/shared/features/products/schema.d.ts +2 -2
  18. package/dist/_internal/shared/history/history.d.ts +22 -0
  19. package/dist/_internal/shared/history/history.js +41 -1
  20. package/dist/client.d.ts +13 -2
  21. package/dist/client.js +20 -2
  22. package/dist/constants/field-names.d.ts +8 -6
  23. package/dist/constants/field-names.js +48 -6
  24. package/dist/features/account/components/UserBidsView.js +12 -4
  25. package/dist/features/account/components/UserReturnsView.d.ts +3 -0
  26. package/dist/features/account/components/UserReturnsView.js +17 -1
  27. package/dist/features/admin/components/AdminAuditLogView.js +1 -0
  28. package/dist/features/admin/components/AdminBidsView.js +15 -2
  29. package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
  30. package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
  31. package/dist/features/admin/components/AdminNavEditorView.js +15 -5
  32. package/dist/features/admin/components/AdminNewsletterView.js +18 -2
  33. package/dist/features/admin/components/AdminOffersView.js +62 -18
  34. package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
  35. package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
  36. package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
  37. package/dist/features/admin/components/AdminScammersView.js +3 -8
  38. package/dist/features/admin/components/AdminSectionsView.js +32 -23
  39. package/dist/features/admin/components/AdminSessionsView.js +17 -2
  40. package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
  41. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
  42. package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
  43. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  44. package/dist/features/admin/constants/filter-tabs.js +1 -0
  45. package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
  46. package/dist/features/admin/schemas/homepage-section-form.js +68 -0
  47. package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
  48. package/dist/features/admin/schemas/nav-item-form.js +72 -0
  49. package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
  50. package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
  51. package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
  52. package/dist/features/auctions/components/bid-detail-fields.js +103 -0
  53. package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
  54. package/dist/features/audit-log/schemas/firestore.js +7 -0
  55. package/dist/features/blog/schemas/blog-form.d.ts +80 -0
  56. package/dist/features/blog/schemas/blog-form.js +100 -0
  57. package/dist/features/events/components/AdminEventEditorView.js +32 -54
  58. package/dist/features/events/components/AdminEventEntriesView.js +22 -3
  59. package/dist/features/events/schemas/event-form.d.ts +113 -0
  60. package/dist/features/events/schemas/event-form.js +82 -0
  61. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
  62. package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
  63. package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
  64. package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
  65. package/dist/features/homepage/schemas/index.d.ts +31 -0
  66. package/dist/features/homepage/schemas/index.js +24 -0
  67. package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
  68. package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
  69. package/dist/features/orders/repository/orders.repository.js +5 -0
  70. package/dist/features/orders/schemas/firestore.d.ts +36 -1
  71. package/dist/features/orders/types/index.d.ts +35 -7
  72. package/dist/features/products/components/GroupSettingsPanel.js +5 -1
  73. package/dist/features/products/schemas/index.d.ts +2 -2
  74. package/dist/features/products/schemas/product-features.validators.d.ts +73 -0
  75. package/dist/features/products/schemas/product-features.validators.js +63 -0
  76. package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
  77. package/dist/features/scams/constants/scammer-status-display.js +79 -0
  78. package/dist/features/seller/actions/offer-actions.js +32 -2
  79. package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
  80. package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
  81. package/dist/features/seller/components/SellerBidsView.js +11 -4
  82. package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
  83. package/dist/features/seller/components/SellerOffersView.js +2 -1
  84. package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
  85. package/dist/features/seller/repository/offer.repository.d.ts +69 -8
  86. package/dist/features/seller/repository/offer.repository.js +127 -23
  87. package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
  88. package/dist/features/seller/schemas/coupon-form.js +116 -0
  89. package/dist/features/seller/schemas/firestore.d.ts +56 -2
  90. package/dist/features/seller/schemas/firestore.js +27 -0
  91. package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
  92. package/dist/features/seller/schemas/offer-forms.js +21 -0
  93. package/dist/features/shell/build-sections.d.ts +63 -0
  94. package/dist/features/shell/build-sections.js +207 -0
  95. package/dist/features/shell/field-ui-meta.d.ts +115 -0
  96. package/dist/features/shell/field-ui-meta.js +324 -0
  97. package/dist/features/shell/index.d.ts +4 -0
  98. package/dist/features/shell/index.js +2 -0
  99. package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
  100. package/dist/features/status-history/components/StatusTimeline.js +46 -0
  101. package/dist/features/status-history/index.d.ts +11 -0
  102. package/dist/features/status-history/index.js +10 -0
  103. package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
  104. package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
  105. package/dist/features/store-extensions/index.d.ts +12 -0
  106. package/dist/features/store-extensions/index.js +12 -0
  107. package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
  108. package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
  109. package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
  110. package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
  111. package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
  112. package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
  113. package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
  114. package/dist/features/store-extensions/schemas/firestore.js +60 -0
  115. package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
  116. package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
  117. package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
  118. package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
  119. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
  120. package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
  121. package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
  122. package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
  123. package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
  124. package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
  125. package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
  126. package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
  127. package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
  128. package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
  129. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
  130. package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
  131. package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
  132. package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
  133. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +326 -0
  134. package/dist/index.d.ts +12 -2
  135. package/dist/index.js +12 -2
  136. package/dist/next/routing/route-map.d.ts +16 -2
  137. package/dist/next/routing/route-map.js +9 -2
  138. package/dist/schemas/firestore-value.d.ts +8 -0
  139. package/dist/schemas/firestore-value.js +44 -0
  140. package/dist/schemas/index.d.ts +1 -1
  141. package/dist/schemas/index.js +1 -1
  142. package/dist/schemas/registry.d.ts +634 -1
  143. package/dist/schemas/registry.js +64 -1
  144. package/dist/seed/bids-seed-data.js +30 -0
  145. package/dist/seed/offers-seed-data.js +212 -7
  146. package/dist/seed/orders-seed-data.js +336 -1
  147. package/dist/seed/products-auctions-seed-data.js +41 -0
  148. package/dist/server.d.ts +2 -0
  149. package/dist/server.js +5 -0
  150. package/dist/styles.css +1 -1
  151. package/dist/tailwind-utilities.css +1 -1
  152. package/dist/ui/components/RecordDetailModal.d.ts +10 -1
  153. package/dist/ui/components/RecordDetailModal.js +2 -2
  154. package/package.json +2 -1
  155. package/scripts/node-esm-loader.mjs +65 -0
  156. package/scripts/seed-cli-loader.mjs +6 -49
  157. package/scripts/seed-cli.mjs +1 -1
@@ -0,0 +1,207 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /*
4
+ * WHY: A `SectionDef[]` written by hand is a second source of truth that has to
5
+ * stay aligned with the schema that validates it. Nothing enforces the
6
+ * alignment, so a field added to the schema renders nowhere, and a field
7
+ * removed from the schema keeps rendering into a key that is stripped on
8
+ * save. `audit-form-field-roundtrip` exists to catch exactly that — this
9
+ * file removes the opportunity instead.
10
+ * WHAT: `buildSectionsFromSchema` — walk a schema's shape, group by
11
+ * `FieldUiMeta.section`, pack consecutive fields into `<FormGroup>` rows
12
+ * by their `row` span, and pick each control from its `kind`.
13
+ *
14
+ * Sections become DERIVED, exactly as quick mode already is. Adding a field to
15
+ * the schema puts it in the right section, in the right row, in the right mode,
16
+ * with one edit rather than three.
17
+ *
18
+ * ## The escape hatch is per field, not per form
19
+ *
20
+ * `renderers` overrides a single field's JSX while everything around it stays
21
+ * derived. That matters: a form with one bespoke control should not have to
22
+ * hand-write its other twelve fields, which is what an all-or-nothing escape
23
+ * hatch would force and is how hand-written section arrays got normalised in
24
+ * the first place.
25
+ *
26
+ * EXPORTS:
27
+ * buildSectionsFromSchema, groupFieldsBySection, packRows,
28
+ * type FieldRenderer, type BuildSectionsOptions
29
+ *
30
+ * @tag domain:forms
31
+ * @tag layer:shared
32
+ * @tag pattern:slot-shell
33
+ * @tag access:client
34
+ * @tag consumers:SectionForm callsites
35
+ * @tag sideEffects:none
36
+ */
37
+ import { Fragment } from "react";
38
+ import { FormGroup } from "../../ui/components/Form";
39
+ import { FieldInput } from "../../ui/forms/FieldInput";
40
+ import { FieldSelect } from "../../ui/forms/FieldSelect";
41
+ import { FieldTextarea } from "../../ui/forms/FieldTextarea";
42
+ import { FieldCheckbox } from "../../ui/forms/FieldCheckbox";
43
+ import { humaniseFieldName } from "./entity-form";
44
+ import { ROW_COLUMNS, inferFieldUiMeta, objectShape, schemaEnumOptions, schemaIsOptional, ui, } from "./field-ui-meta";
45
+ /**
46
+ * Enum members as `<FieldSelect>` options.
47
+ *
48
+ * Both the zod-version handling and the optional/required test live in
49
+ * `field-ui-meta.ts` — this file must not grow a second copy, which is how the
50
+ * two majors' differing `_def` shapes would come to be handled inconsistently.
51
+ */
52
+ function selectOptions(schema) {
53
+ const options = schemaEnumOptions(schema);
54
+ return options ? options.map((o) => ({ value: o, label: humaniseFieldName(o) })) : null;
55
+ }
56
+ /**
57
+ * Resolve every field in a schema's shape.
58
+ *
59
+ * Fields with no registration are still returned, carrying inferred metadata
60
+ * and a `section` of `""`. Dropping them silently would hide the exact defect
61
+ * `audit-field-ui-meta` is meant to report — an unregistered field, or one
62
+ * whose `.register()` was not the last call in the chain and was therefore
63
+ * discarded by a subsequent `.optional()`.
64
+ */
65
+ export function resolveFields(schema) {
66
+ const shape = objectShape(schema);
67
+ if (!shape)
68
+ return [];
69
+ return Object.entries(shape).map(([name, fieldSchema]) => {
70
+ const explicit = ui.get(fieldSchema);
71
+ const meta = {
72
+ section: "",
73
+ ...inferFieldUiMeta(name, fieldSchema),
74
+ ...(explicit ?? {}),
75
+ };
76
+ return { name, schema: fieldSchema, meta, required: !schemaIsOptional(fieldSchema) };
77
+ });
78
+ }
79
+ /** Group resolved fields by section, preserving declaration order within each. */
80
+ export function groupFieldsBySection(fields) {
81
+ const bySection = new Map();
82
+ for (const field of fields) {
83
+ if (field.meta.derived)
84
+ continue; // server-owned — never an input
85
+ const key = field.meta.section || "advanced";
86
+ const bucket = bySection.get(key);
87
+ if (bucket)
88
+ bucket.push(field);
89
+ else
90
+ bySection.set(key, [field]);
91
+ }
92
+ for (const bucket of bySection.values()) {
93
+ bucket.sort((a, b) => (a.meta.order ?? Number.MAX_SAFE_INTEGER) - (b.meta.order ?? Number.MAX_SAFE_INTEGER));
94
+ }
95
+ return bySection;
96
+ }
97
+ /**
98
+ * Pack consecutive fields into rows by their span.
99
+ *
100
+ * A `full` field always breaks the current row — that is what stops a media
101
+ * upload or a textarea being squeezed alongside a short input. Mixed spans
102
+ * start a new row too, rather than being crammed together: two `pair` fields
103
+ * make a clean 2-up, a `pair` next to a `quarter` makes neither.
104
+ */
105
+ export function packRows(fields) {
106
+ const rows = [];
107
+ let current = [];
108
+ let currentSpan = null;
109
+ const flush = () => {
110
+ if (current.length > 0)
111
+ rows.push(current);
112
+ current = [];
113
+ currentSpan = null;
114
+ };
115
+ for (const field of fields) {
116
+ const span = field.meta.row ?? "pair";
117
+ if (span === "full") {
118
+ flush();
119
+ rows.push([field]);
120
+ continue;
121
+ }
122
+ if (currentSpan !== null && (span !== currentSpan || current.length >= ROW_COLUMNS[span])) {
123
+ flush();
124
+ }
125
+ current.push(field);
126
+ currentSpan = span;
127
+ }
128
+ flush();
129
+ return rows;
130
+ }
131
+ /**
132
+ * Build the `SectionDef[]` a `<SectionForm>` renders.
133
+ *
134
+ * Section order: `sectionRequired` first (there is normally exactly one — the
135
+ * fields without which the record cannot be saved), then `sectionOrder`, then
136
+ * first-declaration order.
137
+ */
138
+ export function buildSectionsFromSchema(schema, opts = {}) {
139
+ const fields = resolveFields(schema);
140
+ const bySection = groupFieldsBySection(fields);
141
+ const declarationOrder = [...bySection.keys()];
142
+ const explicitOrder = opts.sectionOrder ?? [];
143
+ const rank = (id) => {
144
+ const bucket = bySection.get(id) ?? [];
145
+ if (bucket.some((f) => f.meta.sectionRequired))
146
+ return -1;
147
+ const i = explicitOrder.indexOf(id);
148
+ return i === -1 ? explicitOrder.length + declarationOrder.indexOf(id) : i;
149
+ };
150
+ return declarationOrder
151
+ .slice()
152
+ .sort((a, b) => rank(a) - rank(b))
153
+ .map((sectionId) => {
154
+ const sectionFields = bySection.get(sectionId) ?? [];
155
+ const label = sectionFields.find((f) => f.meta.sectionLabel)?.meta.sectionLabel ??
156
+ opts.sectionLabels?.[sectionId] ??
157
+ humaniseFieldName(sectionId);
158
+ return {
159
+ id: sectionId,
160
+ label,
161
+ required: sectionFields.some((f) => f.meta.sectionRequired),
162
+ quick: sectionFields.some((f) => f.meta.quick),
163
+ // Any section holding uncommitted state must stay mounted while
164
+ // collapsed — `<Collapse>` unmounts children, which would discard an
165
+ // in-flight media upload. Inference sets this for every media field.
166
+ keepMounted: sectionFields.some((f) => f.meta.sectionKeepMounted),
167
+ fields: sectionFields.map((f) => f.name),
168
+ render: ({ values, onChange, errors }) => (_jsx(_Fragment, { children: packRows(sectionFields).map((row, i) => (_jsx(FormGroup, { columns: ROW_COLUMNS[row[0].meta.row ?? "pair"], children: row.map((field) => renderField(field, { values, onChange, errors }, opts)) }, `${sectionId}-row-${i}`))) })),
169
+ };
170
+ });
171
+ }
172
+ function renderField(field, ctx, opts) {
173
+ const override = opts.renderers?.[field.name];
174
+ // A keyed Fragment, not a wrapper element: an override may legitimately
175
+ // return a block-level control (a media uploader, a repeatable list editor),
176
+ // and wrapping that in a <span> would put block content inside an inline box.
177
+ if (override)
178
+ return _jsx(Fragment, { children: override({ field, ...ctx }) }, field.name);
179
+ const { name, meta, required } = field;
180
+ const label = meta.label ?? humaniseFieldName(name);
181
+ const value = ctx.values[name];
182
+ const set = (v) => ctx.onChange({ [name]: v });
183
+ switch (meta.kind) {
184
+ case "toggle":
185
+ return (_jsx(FieldCheckbox, { name: name, label: label, hint: meta.help, checked: Boolean(value), onChange: set }, name));
186
+ case "textarea":
187
+ return (_jsx(FieldTextarea, { name: name, label: label, hint: meta.help, required: required, value: typeof value === "string" ? value : "", onChange: set }, name));
188
+ case "select": {
189
+ const options = opts.options?.[name] ?? selectOptions(field.schema) ?? [];
190
+ return (_jsx(FieldSelect, { name: name, label: label, hint: meta.help === "paginated" ? undefined : meta.help, required: required, value: typeof value === "string" ? value : "", options: options, onChange: set }, name));
191
+ }
192
+ case "number":
193
+ return (_jsx(FieldInput, { name: name, type: "number", label: label, hint: meta.help, required: required, value: value == null ? "" : String(value), onChange: (v) => set(v === "" ? undefined : Number(v)) }, name));
194
+ case "date":
195
+ return (_jsx(FieldInput, { name: name, type: "date", label: label, hint: meta.help, required: required, value: typeof value === "string" ? value.slice(0, 10) : "", onChange: set }, name));
196
+ // `media` and `list` have no derived control on purpose. A MediaUploadField
197
+ // needs an upload context, a staging path and a finalize call that only the
198
+ // owning feature can supply, and a list editor needs to know what an item
199
+ // looks like. Deriving a placeholder for either would render a control that
200
+ // silently fails to save — worse than requiring an explicit renderer.
201
+ case "media":
202
+ case "list":
203
+ return (_jsx(FieldInput, { name: name, label: label, hint: meta.help ?? `Provide a renderer for "${name}" — this field needs a custom control.`, required: required, disabled: true, value: "" }, name));
204
+ default:
205
+ return (_jsx(FieldInput, { name: name, label: label, hint: meta.help, required: required, value: typeof value === "string" ? value : value == null ? "" : String(value), onChange: set }, name));
206
+ }
207
+ }
@@ -0,0 +1,115 @@
1
+ import type { ZodTypeAny } from "zod";
2
+ /** How much of a row a field claims on desktop. Everything is 1-up on mobile. */
3
+ export type FieldRow = "full" | "pair" | "third" | "quarter";
4
+ /** Which input primitive renders the field, and its default row span. */
5
+ export type FieldKind = "text" | "number" | "date" | "select" | "toggle" | "textarea" | "media" | "list";
6
+ /**
7
+ * Which tier owns a value that is not typed by the user.
8
+ * T1 client `derive()` · T2 server-on-write · T3 mirror · T4 background job.
9
+ * The tier decides the correctness rule — see the calculated-field dictionary.
10
+ */
11
+ export type CalculatedTier = "t1-derive" | "t2-server" | "t3-mirror" | "t4-aggregate";
12
+ export interface FieldUiMeta {
13
+ /**
14
+ * Section id this field belongs to — the grouping key, and the ONE key that
15
+ * cannot be inferred. Whether `gstin` belongs under "Tax" or "Business
16
+ * details" is a product decision, and a wrong section renders perfectly
17
+ * fine in the wrong place with nothing failing. Deliberately NOT inferred
18
+ * from a name prefix (`billing_city` → "billing"): that would work for a
19
+ * handful of schemas and quietly mis-group the rest.
20
+ */
21
+ section: string;
22
+ /** Human label for the section. The first field to declare it wins. */
23
+ sectionLabel?: string;
24
+ /** Marks its section required-first, open on mount, non-collapsible. */
25
+ sectionRequired?: boolean;
26
+ /**
27
+ * Section keeps its panel mounted while collapsed. Mandatory for any
28
+ * section holding uncommitted state — `<Collapse>` unmounts its children,
29
+ * which discards an in-flight media upload.
30
+ */
31
+ sectionKeepMounted?: boolean;
32
+ /** Row span. Drives the `<FormGroup columns>` packing. */
33
+ row?: FieldRow;
34
+ /** Control kind. Picks the primitive and the default row span. */
35
+ kind?: FieldKind;
36
+ /** Sort order within the section. Unset sorts after ordered fields. */
37
+ order?: number;
38
+ /** Belongs in the self-submitting quick drawer. */
39
+ quick?: boolean;
40
+ /** Produced by `derive()` or the server — never rendered as a bare input. */
41
+ derived?: boolean;
42
+ /** Which calculated-field tier owns it, when it is not user-entered. */
43
+ tier?: CalculatedTier;
44
+ label?: string;
45
+ help?: string;
46
+ }
47
+ export declare const ui: {
48
+ /** Attach metadata to a schema instance. Returns the SAME instance. */
49
+ add<S extends ZodTypeAny>(schema: S, meta: FieldUiMeta): S;
50
+ get(schema: ZodTypeAny): FieldUiMeta | undefined;
51
+ has(schema: ZodTypeAny): boolean;
52
+ };
53
+ /**
54
+ * Annotate a field. **Must be the outermost call in the chain** — see the
55
+ * ordering trap in the header.
56
+ *
57
+ * name: annotate(z.string().min(1), { section: "basics", quick: true }),
58
+ * photo: annotate(z.string().optional(), { section: "media" }),
59
+ *
60
+ * Wrapping the call rather than chaining off it (`.register()`-style) is
61
+ * deliberate: it puts the schema visually INSIDE the annotation, so appending
62
+ * `.optional()` afterwards looks as wrong as it is.
63
+ */
64
+ export declare function annotate<S extends ZodTypeAny>(schema: S, meta: FieldUiMeta): S;
65
+ /** `row` → the `<FormGroup columns>` value it packs into. */
66
+ export declare const ROW_COLUMNS: Record<FieldRow, 1 | 2 | 3 | 4>;
67
+ /**
68
+ * Strip `.optional()` / `.nullable()` / `.default()` / `.catch()` to reach the
69
+ * type that actually decides the control. Returns the innermost schema and its
70
+ * normalised type discriminator.
71
+ *
72
+ * Reads `_def` because zod exposes no public accessor for this, on either
73
+ * major. Depth-bounded rather than trusting the shape: `_def` is internal, and
74
+ * this must degrade to "text" rather than loop if a future zod reshapes it.
75
+ */
76
+ export declare function unwrapSchema(schema: ZodTypeAny): {
77
+ schema: ZodTypeAny;
78
+ type: string;
79
+ };
80
+ /** Public: the option list for a select-shaped field, or null. */
81
+ export declare function schemaEnumOptions(schema: ZodTypeAny): string[] | null;
82
+ /** Public: whether a field's type is optional-wrapped (i.e. not required). */
83
+ export declare function schemaIsOptional(schema: ZodTypeAny): boolean;
84
+ /**
85
+ * The `.shape` of an object schema, seeing through effects wrappers.
86
+ *
87
+ * `.superRefine()` / `.refine()` / `.transform()` return a wrapper that does
88
+ * NOT expose `.shape` — verified: `z.object({...}).superRefine(fn)` has
89
+ * `_def.typeName === "ZodEffects"`, `.shape === undefined`, and the real object
90
+ * on `_def.schema`. Reading `.shape` directly therefore returns nothing for any
91
+ * schema with a cross-field rule, which is most of the interesting ones (the
92
+ * payout form's UPI-vs-bank branch is exactly this shape).
93
+ *
94
+ * Failing to unwrap would not error — it would silently derive a form with
95
+ * ZERO sections, which is the quiet-wrong-answer failure mode this whole wave
96
+ * exists to eliminate.
97
+ */
98
+ export declare function objectShape(schema: ZodTypeAny): Record<string, ZodTypeAny> | null;
99
+ /**
100
+ * Everything about a field's layout that can be derived from its name and its
101
+ * Zod type. `section` is never inferred, so the return type is a Partial —
102
+ * `resolveFieldUiMeta` is what merges it with the explicit registration.
103
+ *
104
+ * This is what makes annotation cheap: a 13-field form is 13 short
105
+ * `.register(ui, { section: "…" })` calls, not 13 full metadata objects.
106
+ */
107
+ export declare function inferFieldUiMeta(name: string, schema: ZodTypeAny): Partial<FieldUiMeta>;
108
+ /**
109
+ * The field's final metadata: inference first, explicit registration on top.
110
+ *
111
+ * Returns `null` when the field has no registration at all — the caller
112
+ * decides whether that is a hard error (a registered form schema, where
113
+ * `section` is mandatory) or a fallback (an ad-hoc schema being previewed).
114
+ */
115
+ export declare function resolveFieldUiMeta(name: string, schema: ZodTypeAny): FieldUiMeta | null;
@@ -0,0 +1,324 @@
1
+ /*
2
+ * WHY: Which section a field belongs to, and whether it shares a row with its
3
+ * neighbour, were conventions written in a table in a plan document. A
4
+ * convention drifts; 70 bare `grid-cols-2` in the tree — each one two
5
+ * columns wide at 320px — are what that drift looks like. Zod 4 lets the
6
+ * rule live on the field it describes, so the schema becomes the single
7
+ * source of validation, section membership AND layout.
8
+ * WHAT: `FieldUiMeta`, the typed `ui` registry, and `inferFieldUiMeta` — the
9
+ * heuristic that means only `section` has to be stated per field.
10
+ *
11
+ * ## 🛑 There are TWO zods in this repo, and that decided the design
12
+ *
13
+ * Measured 2026-08-24: the app depends on **zod 4.4.3**, but `appkit`'s own
14
+ * `package.json` declares `zod: ^3.24.0` and a v3 copy is nested at
15
+ * `node_modules/@mohasinac/appkit/node_modules/zod` (3.25.76). Verified with
16
+ * `createRequire`: appkit's shipped `dist` resolves `"zod"` to that nested v3,
17
+ * while consumer code resolves to root v4. Two instances, one bundle — the
18
+ * Root Cause #19 shape, on a package the dedupe script does not cover.
19
+ *
20
+ * So `z.registry<FieldUiMeta>()` is **not usable here**: it is a zod-4 API and
21
+ * appkit compiles against v3. Aligning the two is a dependency major across
22
+ * the 76 appkit files that import zod — its own wave, not a drive-by inside a
23
+ * spine wave.
24
+ *
25
+ * This registry is therefore a plain `WeakMap` keyed by schema instance, which
26
+ * is what `z.registry()` is internally. It behaves identically, works on both
27
+ * majors, and is strictly MORE robust to the split: object identity does not
28
+ * care which zod class produced the object. `annotate()` is typed, so a typo
29
+ * (`rows:` for `row:`) is a compile error — the property `.meta()` never had.
30
+ *
31
+ * Everything that introspects a schema below reads BOTH shapes for the same
32
+ * reason: zod 3 says `_def.typeName === "ZodOptional"`, zod 4 says
33
+ * `_def.type === "optional"`, and this file sees both at runtime.
34
+ *
35
+ * ## 🛑 The ordering trap — metadata MUST be the last call in the chain
36
+ *
37
+ * Verified at runtime:
38
+ *
39
+ * annotate(z.string(), {...}).optional() // ✗ meta LOST → lookup undefined
40
+ * annotate(z.string().optional(), {...}) // ✓ survives
41
+ *
42
+ * Every wrapper (`.optional()`, `.nullable()`, `.default()`, `.catch()`)
43
+ * returns a NEW schema instance, and the registry is keyed by instance. Most
44
+ * fields are optional, so getting this backwards silently drops layout for the
45
+ * majority of them. `audit-field-ui-meta` enforces the ordering.
46
+ *
47
+ * EXPORTS:
48
+ * FieldUiMeta, FieldRow, FieldKind, CalculatedTier, ui, annotate,
49
+ * inferFieldUiMeta, resolveFieldUiMeta, unwrapSchema, ROW_COLUMNS
50
+ *
51
+ * @tag domain:forms
52
+ * @tag layer:shared
53
+ * @tag pattern:registry
54
+ * @tag access:isomorphic
55
+ * @tag consumers:buildSectionsFromSchema,audit-field-ui-meta
56
+ * @tag sideEffects:none
57
+ */
58
+ /**
59
+ * The registry. A module-level singleton, because the whole point is that
60
+ * every form schema in the app annotates into the same place and can be
61
+ * queried as one ("every media field", "every quick field", "every field with
62
+ * no section").
63
+ *
64
+ * `WeakMap` so annotating a schema never keeps it alive.
65
+ */
66
+ const REGISTRY = new WeakMap();
67
+ export const ui = {
68
+ /** Attach metadata to a schema instance. Returns the SAME instance. */
69
+ add(schema, meta) {
70
+ REGISTRY.set(schema, meta);
71
+ return schema;
72
+ },
73
+ get(schema) {
74
+ return REGISTRY.get(schema);
75
+ },
76
+ has(schema) {
77
+ return REGISTRY.has(schema);
78
+ },
79
+ };
80
+ /**
81
+ * Annotate a field. **Must be the outermost call in the chain** — see the
82
+ * ordering trap in the header.
83
+ *
84
+ * name: annotate(z.string().min(1), { section: "basics", quick: true }),
85
+ * photo: annotate(z.string().optional(), { section: "media" }),
86
+ *
87
+ * Wrapping the call rather than chaining off it (`.register()`-style) is
88
+ * deliberate: it puts the schema visually INSIDE the annotation, so appending
89
+ * `.optional()` afterwards looks as wrong as it is.
90
+ */
91
+ export function annotate(schema, meta) {
92
+ return ui.add(schema, meta);
93
+ }
94
+ /** `row` → the `<FormGroup columns>` value it packs into. */
95
+ export const ROW_COLUMNS = {
96
+ full: 1,
97
+ pair: 2,
98
+ third: 3,
99
+ quarter: 4,
100
+ };
101
+ // ---------------------------------------------------------------------------
102
+ // Inference
103
+ // ---------------------------------------------------------------------------
104
+ /**
105
+ * Normalise a zod type discriminator across majors.
106
+ * zod 3: `_def.typeName === "ZodOptional"` · zod 4: `_def.type === "optional"`.
107
+ */
108
+ function typeOf(schema) {
109
+ const def = schema._def;
110
+ if (def?.type)
111
+ return def.type; // v4, already lowercase
112
+ const name = def?.typeName; // v3, "ZodOptional"
113
+ return name ? name.replace(/^Zod/, "").toLowerCase() : "";
114
+ }
115
+ /** Wrappers that produce a new instance around an inner type. */
116
+ const WRAPPERS = new Set(["optional", "nullable", "default", "catch", "readonly", "nonoptional"]);
117
+ /**
118
+ * Strip `.optional()` / `.nullable()` / `.default()` / `.catch()` to reach the
119
+ * type that actually decides the control. Returns the innermost schema and its
120
+ * normalised type discriminator.
121
+ *
122
+ * Reads `_def` because zod exposes no public accessor for this, on either
123
+ * major. Depth-bounded rather than trusting the shape: `_def` is internal, and
124
+ * this must degrade to "text" rather than loop if a future zod reshapes it.
125
+ */
126
+ export function unwrapSchema(schema) {
127
+ let current = schema;
128
+ for (let depth = 0; depth < 10; depth++) {
129
+ const def = current._def;
130
+ const type = typeOf(current);
131
+ if (!WRAPPERS.has(type) || !def?.innerType)
132
+ return { schema: current, type };
133
+ current = def.innerType;
134
+ }
135
+ return { schema: current, type: "" };
136
+ }
137
+ /**
138
+ * The `max()` bound on a string schema, when it declares one — the signal that
139
+ * separates a text input from a textarea.
140
+ *
141
+ * The two majors store checks completely differently:
142
+ * v3: `_def.checks = [{ kind: "max", value: 300 }]`
143
+ * v4: `_def.checks = [{ _zod: { def: { check: "max_length", maximum: 300 } } }]`
144
+ */
145
+ function stringMaxLength(schema) {
146
+ const checks = schema._def?.checks;
147
+ if (!Array.isArray(checks))
148
+ return undefined;
149
+ for (const check of checks) {
150
+ const v4 = check?._zod?.def;
151
+ if (v4?.check === "max_length" && typeof v4.maximum === "number")
152
+ return v4.maximum;
153
+ const v3 = check;
154
+ if (v3?.kind === "max" && typeof v3.value === "number")
155
+ return v3.value;
156
+ }
157
+ return undefined;
158
+ }
159
+ /**
160
+ * Enum members. v4 exposes `.options`; v3 keeps them on `_def.values`, and for
161
+ * a native enum on `_def.values` as an object.
162
+ */
163
+ function enumOptions(schema) {
164
+ const direct = schema.options;
165
+ if (Array.isArray(direct))
166
+ return direct.filter((o) => typeof o === "string");
167
+ const values = schema._def?.values;
168
+ if (Array.isArray(values))
169
+ return values.filter((o) => typeof o === "string");
170
+ if (values && typeof values === "object") {
171
+ return Object.values(values).filter((o) => typeof o === "string");
172
+ }
173
+ return undefined;
174
+ }
175
+ /** Public: the option list for a select-shaped field, or null. */
176
+ export function schemaEnumOptions(schema) {
177
+ const { schema: inner } = unwrapSchema(schema);
178
+ return enumOptions(inner) ?? null;
179
+ }
180
+ /** Public: whether a field's type is optional-wrapped (i.e. not required). */
181
+ export function schemaIsOptional(schema) {
182
+ const type = typeOf(schema);
183
+ return type === "optional" || type === "default" || type === "nullish";
184
+ }
185
+ /**
186
+ * The `.shape` of an object schema, seeing through effects wrappers.
187
+ *
188
+ * `.superRefine()` / `.refine()` / `.transform()` return a wrapper that does
189
+ * NOT expose `.shape` — verified: `z.object({...}).superRefine(fn)` has
190
+ * `_def.typeName === "ZodEffects"`, `.shape === undefined`, and the real object
191
+ * on `_def.schema`. Reading `.shape` directly therefore returns nothing for any
192
+ * schema with a cross-field rule, which is most of the interesting ones (the
193
+ * payout form's UPI-vs-bank branch is exactly this shape).
194
+ *
195
+ * Failing to unwrap would not error — it would silently derive a form with
196
+ * ZERO sections, which is the quiet-wrong-answer failure mode this whole wave
197
+ * exists to eliminate.
198
+ */
199
+ export function objectShape(schema) {
200
+ let current = schema;
201
+ for (let depth = 0; depth < 10; depth++) {
202
+ const direct = current.shape;
203
+ if (direct)
204
+ return direct;
205
+ const def = current._def;
206
+ const next = def?.schema ?? def?.innerType ?? def?.in;
207
+ if (!next)
208
+ return null;
209
+ current = next;
210
+ }
211
+ return null;
212
+ }
213
+ /**
214
+ * Name patterns override the Zod type, because a name carries intent a type
215
+ * cannot. `images` is `ZodArray` but it is a media field, not a list editor;
216
+ * `description` is `ZodString` but it is a textarea.
217
+ *
218
+ * Ordered — the first match wins, so the media test runs before the long-text
219
+ * one (`coverImage` must not become a textarea).
220
+ */
221
+ const NAME_RULES = [
222
+ {
223
+ // Media claims a full row ALWAYS, and forces its section to keepMounted:
224
+ // a MediaUploadField stacks a source-tab strip, a drop target, a progress
225
+ // bar and a preview, and collapsing it mid-upload discards the transfer.
226
+ test: /(^|[a-z])(image|images|photo|video|media|avatar|banner|logo|cover|thumbnail)([A-Z]|s?$)/i,
227
+ meta: { kind: "media", row: "full", sectionKeepMounted: true },
228
+ },
229
+ {
230
+ test: /^(description|body|content|notes?|bio|message|excerpt|answer|returnPolicy)$/i,
231
+ meta: { kind: "textarea", row: "full" },
232
+ },
233
+ {
234
+ // Long single inputs: truncating a URL or an address into half a row makes
235
+ // it unreadable, so they take the whole row even though they are short types.
236
+ test: /^(url|slug|email|title|name|displayName|headline|address|addressLine1|addressLine2|line1|line2)$/i,
237
+ meta: { row: "full" },
238
+ },
239
+ {
240
+ test: /^(firstName|lastName|city|state|postcode|pincode|postalCode|ifsc|gstin|pan|hsn|price|rate|percent|quantity|phone)$/i,
241
+ meta: { row: "pair" },
242
+ },
243
+ ];
244
+ /**
245
+ * Fields nothing should ever render as an input — ids, audit stamps and
246
+ * counters. Marked `derived` so `audit-form-field-roundtrip` reads them as
247
+ * server-owned rather than as unreachable schema fields.
248
+ */
249
+ const SERVER_OWNED = /^(id|createdAt|updatedAt|createdBy|updatedBy|ownerId|authorId|userId|views|viewCount|productCount|usageCount|helpfulCount|replyCount|reportCount)$/;
250
+ /** Zod type → its default control and row span. */
251
+ function fromZodType(type, inner) {
252
+ switch (type) {
253
+ case "boolean":
254
+ // Several per wrapping row — a switch plus label is short, and stacking
255
+ // them one-per-row makes a settings section endless.
256
+ return { kind: "toggle", row: "quarter" };
257
+ case "enum":
258
+ case "nativeEnum":
259
+ case "literal":
260
+ case "union":
261
+ return { kind: "select", row: "pair" };
262
+ case "date":
263
+ return { kind: "date", row: "pair" };
264
+ case "number":
265
+ case "bigint":
266
+ return { kind: "number", row: "pair" };
267
+ case "array":
268
+ return { kind: "list", row: "full" };
269
+ case "object":
270
+ case "record":
271
+ return { kind: "list", row: "full" };
272
+ case "string": {
273
+ const max = stringMaxLength(inner);
274
+ return max != null && max > 200
275
+ ? { kind: "textarea", row: "full" }
276
+ : { kind: "text", row: "pair" };
277
+ }
278
+ default:
279
+ return { kind: "text", row: "pair" };
280
+ }
281
+ }
282
+ /**
283
+ * Everything about a field's layout that can be derived from its name and its
284
+ * Zod type. `section` is never inferred, so the return type is a Partial —
285
+ * `resolveFieldUiMeta` is what merges it with the explicit registration.
286
+ *
287
+ * This is what makes annotation cheap: a 13-field form is 13 short
288
+ * `.register(ui, { section: "…" })` calls, not 13 full metadata objects.
289
+ */
290
+ export function inferFieldUiMeta(name, schema) {
291
+ const { schema: inner, type } = unwrapSchema(schema);
292
+ const meta = { ...fromZodType(type, inner) };
293
+ for (const rule of NAME_RULES) {
294
+ if (rule.test.test(name)) {
295
+ Object.assign(meta, rule.meta);
296
+ break;
297
+ }
298
+ }
299
+ if (SERVER_OWNED.test(name)) {
300
+ meta.derived = true;
301
+ meta.tier = meta.tier ?? "t2-server";
302
+ }
303
+ // >5 options is the standing PaginatedSelect threshold. Recorded as `help`
304
+ // rather than a separate flag so the renderer can decide without a second
305
+ // source of truth about what "many options" means.
306
+ const options = enumOptions(inner);
307
+ if (meta.kind === "select" && options != null && options.length > 5) {
308
+ meta.help = meta.help ?? "paginated";
309
+ }
310
+ return meta;
311
+ }
312
+ /**
313
+ * The field's final metadata: inference first, explicit registration on top.
314
+ *
315
+ * Returns `null` when the field has no registration at all — the caller
316
+ * decides whether that is a hard error (a registered form schema, where
317
+ * `section` is mandatory) or a fallback (an ad-hoc schema being previewed).
318
+ */
319
+ export function resolveFieldUiMeta(name, schema) {
320
+ const explicit = ui.get(schema);
321
+ if (!explicit)
322
+ return null;
323
+ return { ...inferFieldUiMeta(name, schema), ...explicit };
324
+ }
@@ -10,5 +10,9 @@ export type { SectionDef, SectionFormProps, UseSectionFormNavResult, } from "./S
10
10
  export { SectionForm, useSectionFormNav, orderSections, buildFieldToSectionIndex, sectionAnchorId, } from "./SectionForm";
11
11
  export type { EntityFormDefinition } from "./entity-form";
12
12
  export { deriveQuickFields, deriveQuickFieldNames, allDefinitionFields, applyDerive, assertNoDeriveCollision, humaniseFieldName, } from "./entity-form";
13
+ export type { FieldUiMeta, FieldRow, FieldKind, CalculatedTier, } from "./field-ui-meta";
14
+ export { ui, annotate, ROW_COLUMNS, inferFieldUiMeta, resolveFieldUiMeta, unwrapSchema, schemaEnumOptions, schemaIsOptional, objectShape, } from "./field-ui-meta";
15
+ export type { ResolvedField, FieldRenderer, BuildSectionsOptions, } from "./build-sections";
16
+ export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows, } from "./build-sections";
13
17
  export type { TitleDescriptionGroupProps, ImageFieldGroupProps, SeoFieldGroupProps, StatusFieldGroupProps, StatusFieldGroupOption, } from "./field-groups";
14
18
  export { TitleDescriptionGroup, ImageFieldGroup, SeoFieldGroup, StatusFieldGroup, } from "./field-groups";
@@ -4,4 +4,6 @@ export { QuickFormDrawer } from "./QuickFormDrawer";
4
4
  export { StepForm, StepFormActions, StepIndicator } from "./StepForm";
5
5
  export { SectionForm, useSectionFormNav, orderSections, buildFieldToSectionIndex, sectionAnchorId, } from "./SectionForm";
6
6
  export { deriveQuickFields, deriveQuickFieldNames, allDefinitionFields, applyDerive, assertNoDeriveCollision, humaniseFieldName, } from "./entity-form";
7
+ export { ui, annotate, ROW_COLUMNS, inferFieldUiMeta, resolveFieldUiMeta, unwrapSchema, schemaEnumOptions, schemaIsOptional, objectShape, } from "./field-ui-meta";
8
+ export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows, } from "./build-sections";
7
9
  export { TitleDescriptionGroup, ImageFieldGroup, SeoFieldGroup, StatusFieldGroup, } from "./field-groups";