@narrative.io/jsonforms-provider-protocols 2.11.0 → 2.12.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 (147) hide show
  1. package/README.md +193 -33
  2. package/dist/core/initFormData.d.ts +17 -0
  3. package/dist/core/initFormData.d.ts.map +1 -0
  4. package/dist/core/initFormData.js +99 -0
  5. package/dist/core/initFormData.js.map +1 -0
  6. package/dist/core/projection.d.ts +36 -0
  7. package/dist/core/projection.d.ts.map +1 -0
  8. package/dist/core/projection.js +77 -0
  9. package/dist/core/projection.js.map +1 -0
  10. package/dist/core/refs.d.ts +58 -0
  11. package/dist/core/refs.d.ts.map +1 -0
  12. package/dist/core/refs.js +70 -0
  13. package/dist/core/refs.js.map +1 -0
  14. package/dist/core/resolveScope.d.ts +17 -0
  15. package/dist/core/resolveScope.d.ts.map +1 -0
  16. package/dist/core/resolveScope.js +28 -0
  17. package/dist/core/resolveScope.js.map +1 -0
  18. package/dist/core/seedProjectionTargets.d.ts +60 -0
  19. package/dist/core/seedProjectionTargets.d.ts.map +1 -0
  20. package/dist/core/seedProjectionTargets.js +52 -0
  21. package/dist/core/seedProjectionTargets.js.map +1 -0
  22. package/dist/core/transforms.d.ts +8 -10
  23. package/dist/core/transforms.d.ts.map +1 -1
  24. package/dist/core/transforms.js +58 -13
  25. package/dist/core/transforms.js.map +1 -1
  26. package/dist/core/types.d.ts +8 -0
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/index.d.ts +9 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +21 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/jsonforms-provider-protocols.css +6 -2
  33. package/dist/no-eval-ajv.d.ts +70 -0
  34. package/dist/no-eval-ajv.d.ts.map +1 -0
  35. package/dist/no-eval-ajv.js +247 -0
  36. package/dist/no-eval-ajv.js.map +1 -0
  37. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  38. package/dist/vue/components/ProviderAutocomplete.vue.js +12 -6
  39. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  40. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  41. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  42. package/dist/vue/components/ProviderMultiSelect.vue2.js +21 -11
  43. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  44. package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts +9 -0
  45. package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts.map +1 -0
  46. package/dist/vue/components/ProviderObjectMultiSelect.vue.js +8 -0
  47. package/dist/vue/components/ProviderObjectMultiSelect.vue.js.map +1 -0
  48. package/dist/vue/components/ProviderObjectMultiSelect.vue2.js +142 -0
  49. package/dist/vue/components/ProviderObjectMultiSelect.vue2.js.map +1 -0
  50. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  51. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  52. package/dist/vue/components/ProviderSelect.vue2.js +22 -10
  53. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  54. package/dist/vue/composables/useDataLayer.d.ts +10 -0
  55. package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
  56. package/dist/vue/composables/useDataLayer.js +26 -0
  57. package/dist/vue/composables/useDataLayer.js.map +1 -0
  58. package/dist/vue/composables/useDerive.d.ts +5 -2
  59. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  60. package/dist/vue/composables/useDerive.js +29 -12
  61. package/dist/vue/composables/useDerive.js.map +1 -1
  62. package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
  63. package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
  64. package/dist/vue/composables/useDeriveInitialValue.js +125 -0
  65. package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
  66. package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
  67. package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
  68. package/dist/vue/composables/useDirtyValidation.js +15 -0
  69. package/dist/vue/composables/useDirtyValidation.js.map +1 -0
  70. package/dist/vue/composables/useProjection.d.ts +42 -0
  71. package/dist/vue/composables/useProjection.d.ts.map +1 -0
  72. package/dist/vue/composables/useProjection.js +116 -0
  73. package/dist/vue/composables/useProjection.js.map +1 -0
  74. package/dist/vue/composables/useProvider.d.ts +2 -2
  75. package/dist/vue/composables/useProvider.d.ts.map +1 -1
  76. package/dist/vue/composables/useProvider.js +14 -10
  77. package/dist/vue/composables/useProvider.js.map +1 -1
  78. package/dist/vue/index.d.ts +9 -1
  79. package/dist/vue/index.d.ts.map +1 -1
  80. package/dist/vue/index.js +72 -34
  81. package/dist/vue/index.js.map +1 -1
  82. package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
  83. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  84. package/dist/vue/primevue/JfBoolean.vue.js +44 -17
  85. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  86. package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
  87. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  88. package/dist/vue/primevue/JfEnum.vue.js +38 -24
  89. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  90. package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
  91. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  92. package/dist/vue/primevue/JfEnumArray.vue.js +40 -20
  93. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  94. package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
  95. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  96. package/dist/vue/primevue/JfNumber.vue.js +33 -23
  97. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  98. package/dist/vue/primevue/JfText.vue.d.ts +9 -0
  99. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  100. package/dist/vue/primevue/JfText.vue.js +51 -35
  101. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  102. package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
  103. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  104. package/dist/vue/primevue/JfTextArea.vue.js +34 -19
  105. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  106. package/dist/vue/primevue/index.d.ts.map +1 -1
  107. package/dist/vue/primevue/index.js +100 -8
  108. package/dist/vue/primevue/index.js.map +1 -1
  109. package/dist/vue/utils/objectMultiSelect.d.ts +68 -0
  110. package/dist/vue/utils/objectMultiSelect.d.ts.map +1 -0
  111. package/dist/vue/utils/objectMultiSelect.js +72 -0
  112. package/dist/vue/utils/objectMultiSelect.js.map +1 -0
  113. package/dist/vue/utils/placeholder.d.ts +17 -0
  114. package/dist/vue/utils/placeholder.d.ts.map +1 -0
  115. package/dist/vue/utils/placeholder.js +17 -0
  116. package/dist/vue/utils/placeholder.js.map +1 -0
  117. package/package.json +10 -2
  118. package/src/core/initFormData.ts +208 -0
  119. package/src/core/projection.ts +147 -0
  120. package/src/core/refs.ts +166 -0
  121. package/src/core/resolveScope.ts +54 -0
  122. package/src/core/seedProjectionTargets.ts +144 -0
  123. package/src/core/transforms.ts +118 -26
  124. package/src/core/types.ts +9 -0
  125. package/src/index.ts +22 -2
  126. package/src/no-eval-ajv.ts +381 -0
  127. package/src/vue/components/ProviderAutocomplete.vue +11 -7
  128. package/src/vue/components/ProviderMultiSelect.vue +22 -15
  129. package/src/vue/components/ProviderObjectMultiSelect.vue +169 -0
  130. package/src/vue/components/ProviderSelect.vue +23 -14
  131. package/src/vue/composables/useDataLayer.ts +43 -0
  132. package/src/vue/composables/useDerive.ts +62 -16
  133. package/src/vue/composables/useDeriveInitialValue.ts +195 -0
  134. package/src/vue/composables/useDirtyValidation.ts +20 -0
  135. package/src/vue/composables/useProjection.ts +245 -0
  136. package/src/vue/composables/useProvider.ts +28 -11
  137. package/src/vue/index.ts +83 -47
  138. package/src/vue/primevue/JfBoolean.vue +35 -12
  139. package/src/vue/primevue/JfEnum.vue +35 -26
  140. package/src/vue/primevue/JfEnumArray.vue +37 -20
  141. package/src/vue/primevue/JfNumber.vue +32 -24
  142. package/src/vue/primevue/JfText.vue +48 -33
  143. package/src/vue/primevue/JfTextArea.vue +32 -21
  144. package/src/vue/primevue/index.ts +114 -8
  145. package/src/vue/styles.css +26 -1
  146. package/src/vue/utils/objectMultiSelect.ts +171 -0
  147. package/src/vue/utils/placeholder.ts +42 -0
@@ -4,6 +4,8 @@ import JfNumber from "./JfNumber.vue";
4
4
  import JfEnum from "./JfEnum.vue";
5
5
  import JfEnumArray from "./JfEnumArray.vue";
6
6
  import JfBoolean from "./JfBoolean.vue";
7
+ import { getProjectedSchema } from "../../core/projection";
8
+ import { resolveScopeSchema } from "../../core/resolveScope";
7
9
 
8
10
  // Auto-inject layout styles
9
11
  const injectLayoutStyles = () => {
@@ -17,13 +19,38 @@ const injectLayoutStyles = () => {
17
19
  display: flex;
18
20
  flex-direction: column;
19
21
  align-items: flex-start;
20
- gap: 1rem;
22
+ gap: 24px;
21
23
  width: 100%;
22
24
  }
23
25
 
24
26
  .vertical-layout-item {
25
27
  width: 100%;
26
28
  }
29
+
30
+ /* Form control wrapper */
31
+ .jf-control {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 12px;
35
+ }
36
+
37
+ /* Form control label typography */
38
+ .jf-label {
39
+ font-weight: 600;
40
+ font-size: 14px;
41
+ line-height: 14px;
42
+ color: #031553;
43
+ text-align: left;
44
+ }
45
+
46
+ /* Form control description typography */
47
+ .jf-description {
48
+ font-weight: 400;
49
+ font-size: 14px;
50
+ line-height: 14px;
51
+ color: #415290;
52
+ text-align: left;
53
+ }
27
54
  `;
28
55
  document.head.appendChild(style);
29
56
  }
@@ -54,6 +81,7 @@ export function registerPrimevueRenderers(jsonformsCore: any): unknown[] {
54
81
  isNumberControl,
55
82
  isIntegerControl,
56
83
  and,
84
+ or,
57
85
  isControl,
58
86
  schemaMatches,
59
87
  isBooleanControl,
@@ -97,27 +125,105 @@ export function registerPrimevueRenderers(jsonformsCore: any): unknown[] {
97
125
  );
98
126
  };
99
127
 
128
+ // Projection-aware schema check: when options.projection is set,
129
+ // resolve the projected schema and test against it instead of the original
130
+
131
+ const projectedSchemaMatches =
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ (check: (schema: any) => boolean) =>
134
+ (uischema: unknown, schema: unknown): boolean => {
135
+ const ui = uischema as {
136
+ type?: string;
137
+ scope?: string;
138
+ options?: { projection?: string };
139
+ };
140
+ const projection = ui?.options?.projection;
141
+ if (!projection || ui?.type !== "Control" || !ui?.scope) return false;
142
+
143
+ const propertySchema = resolveScopeSchema(
144
+ ui.scope,
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ schema as Record<string, any>,
147
+ );
148
+ if (!propertySchema) return false;
149
+
150
+ return check(getProjectedSchema(propertySchema, projection));
151
+ };
152
+
153
+ const isMultilineProjection = (uischema: unknown, schema: unknown) => {
154
+ const ui = uischema as { options?: { multi?: boolean } };
155
+ return (
156
+ ui?.options?.multi === true &&
157
+ projectedSchemaMatches((s) => s?.type === "string")(uischema, schema)
158
+ );
159
+ };
160
+
100
161
  const renderers = [
101
162
  // Multiline text has higher priority than regular text
102
- { tester: rankWith(PRIME + 4, isMultilineString), renderer: JfTextArea },
103
- { tester: rankWith(PRIME + 3, isStringControl), renderer: JfText },
104
163
  {
105
- tester: rankWith(PRIME + 6, isIntegerControl),
164
+ tester: rankWith(PRIME + 4, or(isMultilineString, isMultilineProjection)),
165
+ renderer: JfTextArea,
166
+ },
167
+ {
168
+ tester: rankWith(
169
+ PRIME + 3,
170
+ or(
171
+ isStringControl,
172
+ projectedSchemaMatches((s) => s?.type === "string"),
173
+ ),
174
+ ),
175
+ renderer: JfText,
176
+ },
177
+ {
178
+ tester: rankWith(
179
+ PRIME + 6,
180
+ or(
181
+ isIntegerControl,
182
+ projectedSchemaMatches((s) => s?.type === "integer"),
183
+ ),
184
+ ),
106
185
  renderer: JfNumber,
107
186
  },
108
187
  {
109
- tester: rankWith(PRIME + 4, isNumberControl),
188
+ tester: rankWith(
189
+ PRIME + 4,
190
+ or(
191
+ isNumberControl,
192
+ projectedSchemaMatches((s) => s?.type === "number"),
193
+ ),
194
+ ),
110
195
  renderer: JfNumber,
111
196
  },
112
197
  {
113
- tester: rankWith(PRIME + 7, and(isControl, schemaMatches(isScalarEnum))),
198
+ tester: rankWith(
199
+ PRIME + 7,
200
+ or(
201
+ and(isControl, schemaMatches(isScalarEnum)),
202
+ and(isControl, projectedSchemaMatches(isScalarEnum)),
203
+ ),
204
+ ),
114
205
  renderer: JfEnum,
115
206
  },
116
207
  {
117
- tester: rankWith(PRIME + 8, and(isControl, schemaMatches(isEnumArray))),
208
+ tester: rankWith(
209
+ PRIME + 8,
210
+ or(
211
+ and(isControl, schemaMatches(isEnumArray)),
212
+ and(isControl, projectedSchemaMatches(isEnumArray)),
213
+ ),
214
+ ),
118
215
  renderer: JfEnumArray,
119
216
  },
120
- { tester: rankWith(PRIME + 3, isBooleanControl), renderer: JfBoolean },
217
+ {
218
+ tester: rankWith(
219
+ PRIME + 3,
220
+ or(
221
+ isBooleanControl,
222
+ projectedSchemaMatches((s) => s?.type === "boolean"),
223
+ ),
224
+ ),
225
+ renderer: JfBoolean,
226
+ },
121
227
  ];
122
228
 
123
229
  // Update the exported array
@@ -5,7 +5,7 @@
5
5
  display: flex;
6
6
  flex-direction: column;
7
7
  align-items: flex-start;
8
- gap: 1rem;
8
+ gap: 24px;
9
9
  width: 100%;
10
10
  }
11
11
 
@@ -14,6 +14,31 @@
14
14
  width: 100%;
15
15
  }
16
16
 
17
+ /* Form control wrapper */
18
+ .jf-control {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 12px;
22
+ }
23
+
24
+ /* Form control label typography */
25
+ .jf-label {
26
+ font-weight: 600;
27
+ font-size: 14px;
28
+ line-height: 14px;
29
+ color: #031553;
30
+ text-align: left;
31
+ }
32
+
33
+ /* Form control description typography */
34
+ .jf-description {
35
+ font-weight: 400;
36
+ font-size: 14px;
37
+ line-height: 14px;
38
+ color: #415290;
39
+ text-align: left;
40
+ }
41
+
17
42
  /* PrimeVue dropdown text alignment fix */
18
43
  .p-dropdown-label {
19
44
  text-align: left !important;
@@ -0,0 +1,171 @@
1
+ import { deref } from "../../core/refs";
2
+
3
+ /**
4
+ * Helpers for `ProviderObjectMultiSelect`: bidirectional translation between
5
+ * the form-data shape (paired objects with consumer-named keys) and the
6
+ * MultiSelect model shape (`{ value, label }` matching the provider's `map`
7
+ * config), plus `objectKeys` inference when the consumer doesn't specify
8
+ * them on the uischema.
9
+ *
10
+ * Pure functions — no Vue, no rendering — so the renderer can compose them
11
+ * and tests can exercise the logic in isolation.
12
+ */
13
+
14
+ export interface ObjectKeys {
15
+ /** Property name on the form-data object that holds the identifier. */
16
+ value: string;
17
+ /** Property name on the form-data object that holds the display string. */
18
+ label: string;
19
+ }
20
+
21
+ export interface MultiSelectOption {
22
+ value: unknown;
23
+ label: string;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ /**
28
+ * Translate form-data items (`[{ [valueKey]: v, [labelKey]: l }]`) into the
29
+ * shape PrimeVue's `<MultiSelect>` expects (`[{ value, label }]`). Items
30
+ * already in `{ value, label }` shape pass through; missing keys yield
31
+ * `undefined` / `""` so the renderer doesn't blow up on partial data.
32
+ */
33
+ export function toMultiSelectShape(
34
+ formData: unknown,
35
+ keys: ObjectKeys,
36
+ ): MultiSelectOption[] {
37
+ if (!Array.isArray(formData)) return [];
38
+ return formData
39
+ .filter((item) => item !== null && typeof item === "object")
40
+ .map((item) => {
41
+ const obj = item as Record<string, unknown>;
42
+ // Tolerate already-translated items: if both `value` and `label` are
43
+ // present and the form-data keys aren't, assume MultiSelect shape.
44
+ if (
45
+ !(keys.value in obj) &&
46
+ !(keys.label in obj) &&
47
+ "value" in obj &&
48
+ "label" in obj
49
+ ) {
50
+ return obj as MultiSelectOption;
51
+ }
52
+ return {
53
+ value: obj[keys.value],
54
+ label: typeof obj[keys.label] === "string" ? (obj[keys.label] as string) : "",
55
+ };
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Inverse of `toMultiSelectShape`. Translates `<MultiSelect>` model items
61
+ * back into form-data shape using the consumer-specified property names.
62
+ */
63
+ export function fromMultiSelectShape(
64
+ modelData: unknown,
65
+ keys: ObjectKeys,
66
+ ): Record<string, unknown>[] {
67
+ if (!Array.isArray(modelData)) return [];
68
+ return modelData
69
+ .filter((item) => item !== null && typeof item === "object")
70
+ .map((item) => {
71
+ const obj = item as Record<string, unknown>;
72
+ return {
73
+ [keys.value]: obj.value,
74
+ [keys.label]: obj.label,
75
+ };
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Order-insensitive equality of two object-shape selections by the
81
+ * identifier property. Used to short-circuit redundant `handleChange`
82
+ * calls when the model emits the same selection under reference inequality.
83
+ */
84
+ export function sameObjectSet(
85
+ a: unknown,
86
+ b: unknown,
87
+ identifierKey: string,
88
+ ): boolean {
89
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
90
+ return false;
91
+ }
92
+ const ids = new Set(
93
+ b
94
+ .filter((x) => x !== null && typeof x === "object")
95
+ .map((x) => (x as Record<string, unknown>)[identifierKey]),
96
+ );
97
+ return a.every((x) => {
98
+ if (x === null || typeof x !== "object") return false;
99
+ return ids.has((x as Record<string, unknown>)[identifierKey]);
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Resolve the items schema of an array control, dereferencing `items.$ref`
105
+ * against the root if present. Returns `undefined` if no items schema exists.
106
+ */
107
+ export function resolveItemsSchema(
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ arraySchema: Record<string, any> | undefined,
110
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
+ rootSchema: Record<string, any>,
112
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
+ ): Record<string, any> | undefined {
114
+ if (!arraySchema || typeof arraySchema !== "object") return undefined;
115
+ const items = arraySchema.items;
116
+ if (!items || typeof items !== "object" || Array.isArray(items)) {
117
+ return undefined;
118
+ }
119
+ return deref(items, rootSchema);
120
+ }
121
+
122
+ /**
123
+ * Infer `objectKeys` from a resolved items schema when the consumer hasn't
124
+ * specified them on the uischema.
125
+ *
126
+ * Strategy: look at `items.required`. If it has exactly two entries, the
127
+ * one whose property has `format: 'uuid'` becomes `value`; the other
128
+ * becomes `label`. If neither has a uuid format, the first entry is
129
+ * `value`, the second is `label`. Returns `undefined` (and the renderer
130
+ * throws at mount) for any other shape — explicit `objectKeys` is required.
131
+ */
132
+ export function inferObjectKeys(
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
+ itemsSchema: Record<string, any> | undefined,
135
+ ): ObjectKeys | undefined {
136
+ if (!itemsSchema || itemsSchema.type !== "object") return undefined;
137
+ const required = itemsSchema.required;
138
+ if (!Array.isArray(required) || required.length !== 2) return undefined;
139
+ const [a, b] = required as [string, string];
140
+ const props = (itemsSchema.properties ?? {}) as Record<
141
+ string,
142
+ { format?: string }
143
+ >;
144
+ const aIsUuid = props[a]?.format === "uuid";
145
+ const bIsUuid = props[b]?.format === "uuid";
146
+ if (aIsUuid && !bIsUuid) return { value: a, label: b };
147
+ if (bIsUuid && !aIsUuid) return { value: b, label: a };
148
+ return { value: a, label: b };
149
+ }
150
+
151
+ /**
152
+ * Resolve the active `objectKeys` for a control: prefer the explicit
153
+ * `uischema.options.objectKeys`, fall back to schema-driven inference.
154
+ * Returns `undefined` when neither is available; the renderer surfaces a
155
+ * runtime error in that case so the consumer knows to be explicit.
156
+ */
157
+ export function resolveObjectKeys(
158
+ uischemaOptions: { objectKeys?: { value?: unknown; label?: unknown } } | undefined,
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
+ itemsSchema: Record<string, any> | undefined,
161
+ ): ObjectKeys | undefined {
162
+ const explicit = uischemaOptions?.objectKeys;
163
+ if (
164
+ explicit &&
165
+ typeof explicit.value === "string" &&
166
+ typeof explicit.label === "string"
167
+ ) {
168
+ return { value: explicit.value, label: explicit.label };
169
+ }
170
+ return inferObjectKeys(itemsSchema);
171
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Placeholder resolution for form controls.
3
+ *
4
+ * Precedence:
5
+ * 1. `uischema.options.placeholder` (explicit author intent — always wins)
6
+ * 2. `Select ${label}` or `Enter ${label}` when a label is resolvable
7
+ * 3. Kind-appropriate bare fallback
8
+ *
9
+ * Never falls back to `schema.description` — descriptions are rendered as
10
+ * prose above the field by our renderers, so re-using them as placeholder
11
+ * produces a duplicated, truncated string inside the input.
12
+ */
13
+
14
+ export type PlaceholderKind = "select" | "input";
15
+
16
+ /**
17
+ * Strip a trailing required-indicator asterisk (" *" or "*") that `resolveLabel`
18
+ * appends for required fields, so composed placeholders read naturally.
19
+ */
20
+ function stripRequiredMarker(label: string): string {
21
+ return label.replace(/\s*\*\s*$/, "").trim();
22
+ }
23
+
24
+ export function resolvePlaceholder(
25
+ uischema: { options?: unknown } | undefined,
26
+ resolvedLabel: string | undefined,
27
+ kind: PlaceholderKind,
28
+ ): string | undefined {
29
+ const options = uischema?.options;
30
+ const explicit =
31
+ options && typeof options === "object"
32
+ ? (options as Record<string, unknown>).placeholder
33
+ : undefined;
34
+ if (typeof explicit === "string" && explicit.length > 0) return explicit;
35
+
36
+ const label = resolvedLabel ? stripRequiredMarker(resolvedLabel) : "";
37
+ if (label) {
38
+ return kind === "select" ? `Select ${label}` : `Enter ${label}`;
39
+ }
40
+
41
+ return kind === "select" ? "Select…" : undefined;
42
+ }