@focus-reactive/payload-plugin-translator 0.5.0 → 0.5.1

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 (27) hide show
  1. package/dist/client/shared/ui/Button/Button.d.ts +4 -4
  2. package/dist/client/shared/ui/Button/Button.js +18 -18
  3. package/dist/client/shared/ui/Button/styles.module.scss +1 -1
  4. package/dist/client/shared/ui/Popup/Popup.d.ts +10 -4
  5. package/dist/client/shared/ui/Popup/Popup.js +4 -3
  6. package/dist/client/shared/ui/Select/Select.d.ts +9 -7
  7. package/dist/client/shared/ui/Select/Select.js +18 -17
  8. package/dist/client/shared/ui/Select/styles.module.scss +5 -0
  9. package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +31 -0
  10. package/dist/server/features/translate-field/resolveFieldSubtree.js +45 -0
  11. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +3 -9
  12. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +71 -78
  13. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +11 -22
  14. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +85 -106
  15. package/dist/server/shared/field-traversal/findFieldByPath.d.ts +40 -0
  16. package/dist/server/shared/field-traversal/findFieldByPath.js +88 -0
  17. package/dist/server/shared/field-traversal/index.d.ts +5 -0
  18. package/dist/server/shared/field-traversal/index.js +5 -0
  19. package/dist/server/shared/field-traversal/kernel.d.ts +85 -0
  20. package/dist/server/shared/field-traversal/kernel.js +145 -0
  21. package/dist/server/shared/field-traversal/types.d.ts +191 -0
  22. package/dist/server/shared/field-traversal/types.js +41 -0
  23. package/dist/server/shared/field-traversal/walkFields.d.ts +51 -0
  24. package/dist/server/shared/field-traversal/walkFields.js +164 -0
  25. package/dist/server/shared/utils/filterLocalizedFields.d.ts +3 -3
  26. package/dist/server/shared/utils/filterLocalizedFields.js +52 -63
  27. package/package.json +2 -2
@@ -1,128 +1,107 @@
1
- import { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from 'payload/shared';
2
- import { hasFields, isBlockItem, isLocalizedField, isObject, isTabsField, isTranslatableField, isFieldExcludedFromTranslation } from '../../../../shared';
1
+ import { isFieldExcludedFromTranslation, isLocalizedField, isObject, isTranslatableField } from "../../../../shared";
2
+ import { resolveBlockFields, walkFields } from "../../../../shared/field-traversal";
3
+ const asObject = (value)=>isObject(value) ? value : {};
3
4
  /**
4
- * Collects FieldChunks by traversing schema and data in parallel.
5
- * Each chunk contains a reference to the parent data object for later mutation.
5
+ * Collects FieldChunks by walking schema + data with the shared {@link walkFields} engine.
6
+ * Iteration is driven by `filteredData`; each translatable, localized, non-excluded leaf that
7
+ * the strategy approves is written with its source value and pushed as a chunk carrying a
8
+ * mutable reference to its parent object (for later write-back). Collect-only — `combine` is a
9
+ * no-op; results accumulate in a closure.
6
10
  *
7
- * Applies all filtering logic:
8
- * - isTranslatableField
9
- * - isLocalizedField
10
- * - strategy.shouldTranslate(sourceValue, targetValue)
11
- * - !isFieldExcludedFromTranslation
12
- *
13
- * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization).
14
- * Original schemas preserve `localized: true` on nested fields.
11
+ * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization). Original
12
+ * schemas preserve `localized: true` on nested fields.
15
13
  */ export class FieldChunkCollector {
16
14
  schema;
17
15
  filteredData;
18
16
  sourceData;
19
17
  targetData;
20
18
  strategy;
21
- chunks;
22
19
  constructor(schema, filteredData, sourceData, targetData, strategy){
23
20
  this.schema = schema;
24
21
  this.filteredData = filteredData;
25
22
  this.sourceData = sourceData;
26
23
  this.targetData = targetData;
27
24
  this.strategy = strategy;
28
- this.chunks = [];
29
- }
30
- /**
31
- * Collects translatable field chunks that need translation.
32
- */ collect() {
33
- this.chunks = [];
34
- this.traverseFields(this.schema, this.filteredData, this.sourceData, this.targetData, []);
35
- return this.chunks;
36
25
  }
37
- /**
38
- * Traverses fields recursively, collecting translatable fields that need translation.
39
- * Uses strategy.shouldTranslate() to determine if field needs translation.
40
- */ traverseFields(fields, data, source, target, path) {
41
- for (const field of fields){
42
- if (isTabsField(field)) {
43
- for (const tab of field.tabs){
44
- if (hasFields(tab)) {
45
- if (tabHasName(tab)) {
46
- const tabData = data[tab.name];
47
- const tabSource = source[tab.name];
48
- const tabTarget = target[tab.name];
49
- if (isObject(tabData)) {
50
- this.traverseFields(tab.fields, tabData, isObject(tabSource) ? tabSource : {}, isObject(tabTarget) ? tabTarget : {}, [
51
- ...path,
52
- tab.name
53
- ]);
54
- }
55
- } else {
56
- this.traverseFields(tab.fields, data, source, target, path);
57
- }
58
- }
59
- }
60
- continue;
61
- }
62
- if (!fieldAffectsData(field)) {
63
- if (hasFields(field)) this.traverseFields(field.fields, data, source, target, path);
64
- continue;
65
- }
66
- const value = data[field.name];
67
- const sourceValue = source[field.name];
68
- const targetValue = target[field.name];
69
- if (value === undefined || value === null) continue;
70
- const currentPath = [
71
- ...path,
72
- field.name
73
- ];
74
- if (fieldIsGroupType(field) && isObject(value)) {
75
- this.traverseFields(field.fields, value, isObject(sourceValue) ? sourceValue : {}, isObject(targetValue) ? targetValue : {}, currentPath);
76
- continue;
77
- }
78
- if (fieldIsArrayType(field) && Array.isArray(value)) {
79
- const sourceArray = Array.isArray(sourceValue) ? sourceValue : [];
80
- const targetArray = Array.isArray(targetValue) ? targetValue : [];
26
+ /** Collects translatable field chunks that need translation. */ collect() {
27
+ const chunks = [];
28
+ const { strategy } = this;
29
+ const walker = {
30
+ enterObject (field, cursor) {
31
+ const value = cursor.data[field.name];
32
+ if (!isObject(value)) return "skip";
33
+ return {
34
+ data: value,
35
+ source: asObject(cursor.source[field.name]),
36
+ target: asObject(cursor.target[field.name]),
37
+ path: [
38
+ ...cursor.path,
39
+ field.name
40
+ ]
41
+ };
42
+ },
43
+ enterList (field, cursor) {
44
+ const value = cursor.data[field.name];
45
+ if (!Array.isArray(value)) return "skip";
46
+ const sourceValue = cursor.source[field.name];
47
+ const targetValue = cursor.target[field.name];
48
+ const sourceArr = Array.isArray(sourceValue) ? sourceValue : [];
49
+ const targetArr = Array.isArray(targetValue) ? targetValue : [];
50
+ const children = [];
81
51
  value.forEach((item, index)=>{
82
- if (isObject(item)) {
83
- const sourceItem = isObject(sourceArray[index]) ? sourceArray[index] : {};
84
- const targetItem = isObject(targetArray[index]) ? targetArray[index] : {};
85
- this.traverseFields(field.fields, item, sourceItem, targetItem, [
86
- ...currentPath,
87
- String(index)
88
- ]);
89
- }
90
- });
91
- continue;
92
- }
93
- if (fieldIsBlockType(field) && Array.isArray(value)) {
94
- const sourceArray = Array.isArray(sourceValue) ? sourceValue : [];
95
- const targetArray = Array.isArray(targetValue) ? targetValue : [];
96
- value.forEach((item, index)=>{
97
- if (isBlockItem(item)) {
98
- const block = field.blocks.find((b)=>b.slug === item.blockType);
99
- if (block) {
100
- const sourceItem = isObject(sourceArray[index]) ? sourceArray[index] : {};
101
- const targetItem = isObject(targetArray[index]) ? targetArray[index] : {};
102
- this.traverseFields(block.fields, item, sourceItem, targetItem, [
103
- ...currentPath,
52
+ if (!isObject(item)) return;
53
+ const fields = field.type === "blocks" ? resolveBlockFields(field, item) : field.fields;
54
+ if (!fields) return; // unknown blockType → skip element
55
+ children.push({
56
+ cursor: {
57
+ data: item,
58
+ source: asObject(sourceArr[index]),
59
+ target: asObject(targetArr[index]),
60
+ path: [
61
+ ...cursor.path,
62
+ field.name,
104
63
  String(index)
105
- ]);
106
- }
107
- }
108
- });
109
- continue;
110
- }
111
- // Collect if: translatable, localized, not excluded, and strategy says to translate
112
- if (isTranslatableField(field) && isLocalizedField(field) && !isFieldExcludedFromTranslation(field) && this.strategy.shouldTranslate({
113
- sourceValue,
114
- targetValue
115
- })) {
116
- // Write sourceValue to filteredData — this is what will be translated
117
- data[field.name] = sourceValue;
118
- this.chunks.push({
119
- schema: field,
120
- dataRef: data,
121
- key: field.name,
122
- path: currentPath
64
+ ]
65
+ },
66
+ fields,
67
+ key: index
68
+ });
123
69
  });
70
+ return children;
71
+ },
72
+ leaf (field, cursor) {
73
+ const value = cursor.data[field.name];
74
+ if (value === undefined || value === null) return undefined;
75
+ const sourceValue = cursor.source[field.name];
76
+ const targetValue = cursor.target[field.name];
77
+ if (isTranslatableField(field) && isLocalizedField(field) && !isFieldExcludedFromTranslation(field) && strategy.shouldTranslate({
78
+ sourceValue,
79
+ targetValue
80
+ })) {
81
+ cursor.data[field.name] = sourceValue; // write source value into filteredData — this is what gets translated
82
+ chunks.push({
83
+ schema: field,
84
+ dataRef: cursor.data,
85
+ key: field.name,
86
+ path: [
87
+ ...cursor.path,
88
+ field.name
89
+ ]
90
+ });
91
+ }
92
+ return undefined;
93
+ },
94
+ combine () {
95
+ return undefined; // collect-only — nothing to assemble
124
96
  }
125
- }
97
+ };
98
+ walkFields(this.schema, {
99
+ data: this.filteredData,
100
+ source: this.sourceData,
101
+ target: this.targetData,
102
+ path: []
103
+ }, walker);
104
+ return chunks;
126
105
  }
127
106
  }
128
107
 
@@ -0,0 +1,40 @@
1
+ import type { Field } from "payload";
2
+ import type { LeafField } from "./types";
3
+ /**
4
+ * Outcome of navigating a field schema by a path of segment names (see {@link findFieldByPath}).
5
+ *
6
+ * - `leaf` — the path lands on a data-affecting leaf field (carries it).
7
+ * - `container` — the path lands on a `group`/`array`/`blocks` field or a named tab (a container,
8
+ * not a leaf).
9
+ * - `inside-blocks` — the path descends THROUGH a `blocks` field, which can't be resolved from the
10
+ * schema alone (the block's `blockType` lives in the data, not the schema).
11
+ * - `not-found` — no field matches a segment, or the path continues past a leaf.
12
+ *
13
+ * @public
14
+ */
15
+ export type FieldPathResult = {
16
+ status: "leaf";
17
+ field: LeafField;
18
+ } | {
19
+ status: "container";
20
+ } | {
21
+ status: "inside-blocks";
22
+ } | {
23
+ status: "not-found";
24
+ };
25
+ /**
26
+ * Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
27
+ * with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
28
+ * (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
29
+ * path scope. Built on {@link classifyField} / {@link tabScopes} so the structural dispatch lives
30
+ * in one place.
31
+ *
32
+ * The caller supplies already-prepared name segments (split, trimmed, with array indices dropped —
33
+ * a field's config is shared across array items, so indices never appear in the schema).
34
+ *
35
+ * @param fields - The schema level to search.
36
+ * @param segments - Remaining path segments (names) to match, head-first.
37
+ * @returns A {@link FieldPathResult}.
38
+ * @public
39
+ */
40
+ export declare function findFieldByPath(fields: Field[], segments: string[]): FieldPathResult;
@@ -0,0 +1,88 @@
1
+ import { classifyField, tabScopes } from "./kernel";
2
+ /**
3
+ * Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
4
+ * with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
5
+ * (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
6
+ * path scope. Built on {@link classifyField} / {@link tabScopes} so the structural dispatch lives
7
+ * in one place.
8
+ *
9
+ * The caller supplies already-prepared name segments (split, trimmed, with array indices dropped —
10
+ * a field's config is shared across array items, so indices never appear in the schema).
11
+ *
12
+ * @param fields - The schema level to search.
13
+ * @param segments - Remaining path segments (names) to match, head-first.
14
+ * @returns A {@link FieldPathResult}.
15
+ * @public
16
+ */ export function findFieldByPath(fields, segments) {
17
+ const [head, ...rest] = segments;
18
+ if (head === undefined) return {
19
+ status: "not-found"
20
+ };
21
+ for (const field of fields){
22
+ const structure = classifyField(field);
23
+ switch(structure.kind){
24
+ case "tabs":
25
+ {
26
+ for (const scope of tabScopes(structure.field)){
27
+ if (scope.named) {
28
+ if (scope.tab.name === head) {
29
+ return rest.length === 0 ? {
30
+ status: "container"
31
+ } : findFieldByPath(scope.tab.fields, rest);
32
+ }
33
+ } else {
34
+ const found = findFieldByPath(scope.fields, segments); // unnamed tab → same path scope
35
+ if (found.status !== "not-found") return found;
36
+ }
37
+ }
38
+ break;
39
+ }
40
+ case "transparent":
41
+ {
42
+ const found = findFieldByPath(structure.fields, segments); // row/collapsible/unnamed group → same scope
43
+ if (found.status !== "not-found") return found;
44
+ break;
45
+ }
46
+ case "presentational":
47
+ break;
48
+ case "group":
49
+ case "array":
50
+ {
51
+ if (structure.name !== head) break;
52
+ return rest.length === 0 ? {
53
+ status: "container"
54
+ } : findFieldByPath(structure.fields, rest);
55
+ }
56
+ case "blocks":
57
+ {
58
+ if (structure.name !== head) break;
59
+ return rest.length === 0 ? {
60
+ status: "container"
61
+ } : {
62
+ status: "inside-blocks"
63
+ };
64
+ }
65
+ case "leaf":
66
+ {
67
+ if (structure.name !== head) break;
68
+ return rest.length === 0 ? {
69
+ status: "leaf",
70
+ field: structure.field
71
+ } : {
72
+ status: "not-found"
73
+ };
74
+ }
75
+ default:
76
+ {
77
+ // Exhaustiveness guard: a new FieldStructure kind would error here at compile time.
78
+ const exhaustive = structure;
79
+ throw new Error(`unhandled field structure: ${String(exhaustive)}`);
80
+ }
81
+ }
82
+ }
83
+ return {
84
+ status: "not-found"
85
+ };
86
+ }
87
+
88
+ //# sourceMappingURL=findFieldByPath.js.map
@@ -0,0 +1,5 @@
1
+ export { findFieldByPath } from "./findFieldByPath";
2
+ export type { FieldPathResult } from "./findFieldByPath";
3
+ export { classifyField, resolveBlockFields, tabScopes } from "./kernel";
4
+ export type { ChildCursor, ChildOutput, ContainerInfo, FieldStructure, FieldWalker, LeafField, TabScope, WalkSignal } from "./types";
5
+ export { walkFields } from "./walkFields";
@@ -0,0 +1,5 @@
1
+ export { findFieldByPath } from "./findFieldByPath";
2
+ export { classifyField, resolveBlockFields, tabScopes } from "./kernel";
3
+ export { walkFields } from "./walkFields";
4
+
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,85 @@
1
+ import type { BlocksField, Field, TabsField } from "payload";
2
+ import type { FieldStructure, TabScope } from "./types";
3
+ /**
4
+ * Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
5
+ * one place that encodes how Payload field types map onto data boundaries.
6
+ *
7
+ * Traversals switch on the result instead of re-deriving the predicate chain
8
+ * (`fieldAffectsData` / `fieldIsGroupType` / …), so the dispatch — and any future Payload
9
+ * field type — lives in exactly one place.
10
+ *
11
+ * @param field - Any field from a collection/global `fields` array (original, un-sanitized
12
+ * config).
13
+ * @returns The field's {@link FieldStructure}:
14
+ * - `tabs` — a `tabs` field (expand with {@link tabScopes}).
15
+ * - `transparent` — a presentational container that opens **no** data boundary (`row`,
16
+ * `collapsible`, unnamed `group`); its `fields` live in the parent's data scope.
17
+ * - `presentational` — a `ui` field: no data, no subfields.
18
+ * - `group` / `array` / `blocks` — named data boundaries (carry `name` + child `fields`,
19
+ * except `blocks` whose child fields are per-element via {@link resolveBlockFields}).
20
+ * - `leaf` — a data-affecting field with no subfields.
21
+ *
22
+ * Dispatch order is significant: some fields satisfy more than one predicate and only the
23
+ * order disambiguates them.
24
+ * - `tabs` is checked first — it carries `.tabs` (not `.fields`) and is non-data-affecting,
25
+ * so a later check would mislabel it `presentational`.
26
+ * - `fieldAffectsData` is resolved before the `group` check, so an unnamed group resolves to
27
+ * `transparent`, never a `group` with `name: undefined`.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const structure = classifyField(field);
32
+ * switch (structure.kind) {
33
+ * case "group": descend(structure.fields, data[structure.name]); break;
34
+ * case "leaf": translate(structure.field, structure.name); break;
35
+ * // ...handle the remaining kinds
36
+ * }
37
+ * ```
38
+ *
39
+ * @see {@link walkFields} — the recursive engine built on this classification.
40
+ * @public
41
+ */
42
+ export declare function classifyField(field: Field): FieldStructure;
43
+ /**
44
+ * Flatten a `tabs` field's tabs into {@link TabScope}s, absorbing the named/unnamed +
45
+ * `hasFields` handling that field traversals would otherwise each repeat.
46
+ *
47
+ * @param field - A Payload `tabs` field.
48
+ * @returns One {@link TabScope} per tab that has fields, in declaration order:
49
+ * - a **named** tab → `{ named: true, tab }` — it opens a data boundary at `tab.name`, and
50
+ * the `NamedTab` is surfaced so callers can read its `name`/`fields`/config;
51
+ * - an **unnamed** tab → `{ named: false, fields }` — its fields live in the parent's data
52
+ * scope, so only the `fields` are needed.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * for (const scope of tabScopes(tabsField)) {
57
+ * if (scope.named) descendInto(data[scope.tab.name], scope.tab.fields);
58
+ * else descendInto(data, scope.fields); // unnamed tab — same data scope as the parent
59
+ * }
60
+ * ```
61
+ * @public
62
+ */
63
+ export declare function tabScopes(field: TabsField): TabScope[];
64
+ /**
65
+ * Resolve the child `fields` for a single `blocks` element by matching its `blockType`
66
+ * against the field's block definitions.
67
+ *
68
+ * @param field - A Payload `blocks` field carrying inline block definitions.
69
+ * @param item - The data for one block element (expected to carry a `blockType` string).
70
+ * @returns The matched block's `fields`, or `null` when `item` is not a block object or its
71
+ * `blockType` matches no defined block.
72
+ *
73
+ * Assumes the **original, un-sanitized** schema, where blocks are inline `Block` objects on
74
+ * `field.blocks`. Payload's sanitized config may instead carry `blockReferences` resolved
75
+ * against the top-level config — not handled here by design, since this package traverses
76
+ * the original schema to preserve nested `localized` flags.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const fields = resolveBlockFields(blocksField, item);
81
+ * if (fields) descendInto(item, fields); // null → unknown blockType / not a block → skip
82
+ * ```
83
+ * @public
84
+ */
85
+ export declare function resolveBlockFields(field: BlocksField, item: unknown): Field[] | null;
@@ -0,0 +1,145 @@
1
+ import { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from "payload/shared";
2
+ import { hasFields, isBlockItem, isTabsField } from "../guards";
3
+ /**
4
+ * Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
5
+ * one place that encodes how Payload field types map onto data boundaries.
6
+ *
7
+ * Traversals switch on the result instead of re-deriving the predicate chain
8
+ * (`fieldAffectsData` / `fieldIsGroupType` / …), so the dispatch — and any future Payload
9
+ * field type — lives in exactly one place.
10
+ *
11
+ * @param field - Any field from a collection/global `fields` array (original, un-sanitized
12
+ * config).
13
+ * @returns The field's {@link FieldStructure}:
14
+ * - `tabs` — a `tabs` field (expand with {@link tabScopes}).
15
+ * - `transparent` — a presentational container that opens **no** data boundary (`row`,
16
+ * `collapsible`, unnamed `group`); its `fields` live in the parent's data scope.
17
+ * - `presentational` — a `ui` field: no data, no subfields.
18
+ * - `group` / `array` / `blocks` — named data boundaries (carry `name` + child `fields`,
19
+ * except `blocks` whose child fields are per-element via {@link resolveBlockFields}).
20
+ * - `leaf` — a data-affecting field with no subfields.
21
+ *
22
+ * Dispatch order is significant: some fields satisfy more than one predicate and only the
23
+ * order disambiguates them.
24
+ * - `tabs` is checked first — it carries `.tabs` (not `.fields`) and is non-data-affecting,
25
+ * so a later check would mislabel it `presentational`.
26
+ * - `fieldAffectsData` is resolved before the `group` check, so an unnamed group resolves to
27
+ * `transparent`, never a `group` with `name: undefined`.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const structure = classifyField(field);
32
+ * switch (structure.kind) {
33
+ * case "group": descend(structure.fields, data[structure.name]); break;
34
+ * case "leaf": translate(structure.field, structure.name); break;
35
+ * // ...handle the remaining kinds
36
+ * }
37
+ * ```
38
+ *
39
+ * @see {@link walkFields} — the recursive engine built on this classification.
40
+ * @public
41
+ */ export function classifyField(field) {
42
+ if (isTabsField(field)) {
43
+ return {
44
+ kind: "tabs",
45
+ field
46
+ };
47
+ }
48
+ if (!fieldAffectsData(field)) {
49
+ return hasFields(field) ? {
50
+ kind: "transparent",
51
+ fields: field.fields
52
+ } : {
53
+ kind: "presentational"
54
+ };
55
+ }
56
+ if (fieldIsGroupType(field)) {
57
+ return {
58
+ kind: "group",
59
+ name: field.name,
60
+ fields: field.fields,
61
+ field
62
+ };
63
+ }
64
+ if (fieldIsArrayType(field)) {
65
+ return {
66
+ kind: "array",
67
+ name: field.name,
68
+ fields: field.fields,
69
+ field
70
+ };
71
+ }
72
+ if (fieldIsBlockType(field)) {
73
+ return {
74
+ kind: "blocks",
75
+ name: field.name,
76
+ field
77
+ };
78
+ }
79
+ const leaf = field;
80
+ return {
81
+ kind: "leaf",
82
+ name: leaf.name,
83
+ field: leaf
84
+ };
85
+ }
86
+ /**
87
+ * Flatten a `tabs` field's tabs into {@link TabScope}s, absorbing the named/unnamed +
88
+ * `hasFields` handling that field traversals would otherwise each repeat.
89
+ *
90
+ * @param field - A Payload `tabs` field.
91
+ * @returns One {@link TabScope} per tab that has fields, in declaration order:
92
+ * - a **named** tab → `{ named: true, tab }` — it opens a data boundary at `tab.name`, and
93
+ * the `NamedTab` is surfaced so callers can read its `name`/`fields`/config;
94
+ * - an **unnamed** tab → `{ named: false, fields }` — its fields live in the parent's data
95
+ * scope, so only the `fields` are needed.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * for (const scope of tabScopes(tabsField)) {
100
+ * if (scope.named) descendInto(data[scope.tab.name], scope.tab.fields);
101
+ * else descendInto(data, scope.fields); // unnamed tab — same data scope as the parent
102
+ * }
103
+ * ```
104
+ * @public
105
+ */ export function tabScopes(field) {
106
+ const scopes = [];
107
+ for (const tab of field.tabs){
108
+ if (tabHasName(tab)) scopes.push({
109
+ named: true,
110
+ tab
111
+ });
112
+ else if (hasFields(tab)) scopes.push({
113
+ named: false,
114
+ fields: tab.fields
115
+ });
116
+ }
117
+ return scopes;
118
+ }
119
+ /**
120
+ * Resolve the child `fields` for a single `blocks` element by matching its `blockType`
121
+ * against the field's block definitions.
122
+ *
123
+ * @param field - A Payload `blocks` field carrying inline block definitions.
124
+ * @param item - The data for one block element (expected to carry a `blockType` string).
125
+ * @returns The matched block's `fields`, or `null` when `item` is not a block object or its
126
+ * `blockType` matches no defined block.
127
+ *
128
+ * Assumes the **original, un-sanitized** schema, where blocks are inline `Block` objects on
129
+ * `field.blocks`. Payload's sanitized config may instead carry `blockReferences` resolved
130
+ * against the top-level config — not handled here by design, since this package traverses
131
+ * the original schema to preserve nested `localized` flags.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * const fields = resolveBlockFields(blocksField, item);
136
+ * if (fields) descendInto(item, fields); // null → unknown blockType / not a block → skip
137
+ * ```
138
+ * @public
139
+ */ export function resolveBlockFields(field, item) {
140
+ if (!isBlockItem(item)) return null;
141
+ const block = field.blocks.find((candidate)=>candidate.slug === item.blockType);
142
+ return block ? block.fields : null;
143
+ }
144
+
145
+ //# sourceMappingURL=kernel.js.map