@dragonmastery/zinia-forms-core 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -139,6 +139,87 @@ interface CheckboxFieldProps<FormType> {
139
139
  variant?: string;
140
140
  }
141
141
 
142
+ /**
143
+ * Option item for combobox fields - can be a string or label/value object
144
+ */
145
+ type ComboboxOption = string | {
146
+ label: string;
147
+ value: string;
148
+ };
149
+ /**
150
+ * Props for the ComboboxField component
151
+ */
152
+ interface ComboboxFieldProps<FormType, P extends Path<FormType> = Path<FormType>> {
153
+ /**
154
+ * Field name (path in the form state)
155
+ * Accepts both static paths and dynamic paths for use in array fields
156
+ */
157
+ name: FlexiblePath<FormType>;
158
+ /**
159
+ * Field label
160
+ */
161
+ label?: string;
162
+ /**
163
+ * Whether to hide the label
164
+ */
165
+ hideLabel?: boolean;
166
+ /**
167
+ * Field description
168
+ */
169
+ description?: string;
170
+ /**
171
+ * Whether the field is required
172
+ */
173
+ required?: boolean;
174
+ /**
175
+ * Placeholder text
176
+ */
177
+ placeholder?: string;
178
+ /**
179
+ * Whether the field is disabled
180
+ */
181
+ disabled?: boolean;
182
+ /**
183
+ * Whether the field is readonly
184
+ */
185
+ readonly?: boolean;
186
+ /**
187
+ * CSS classes to apply to the field
188
+ */
189
+ class?: string | string[];
190
+ /**
191
+ * Field size
192
+ */
193
+ size?: string;
194
+ /**
195
+ * Field variant
196
+ */
197
+ variant?: string;
198
+ /**
199
+ * Combobox options (array of strings or label/value objects)
200
+ * If not provided, will use options from schema metadata
201
+ *
202
+ * For async loading, use form-level dataLoaders and pass options via extraData:
203
+ * ```ts
204
+ * useForm(schema, {
205
+ * dataLoaders: { products: loadProducts },
206
+ * });
207
+ * // Then: :options="form.extraData.products || []"
208
+ * ```
209
+ */
210
+ options?: ComboboxOption[];
211
+ /**
212
+ * Whether to allow creating new values that don't exist in the options
213
+ * Default: true
214
+ */
215
+ allowCreate?: boolean;
216
+ /**
217
+ * Custom filter function for options
218
+ * Default: filters by case-insensitive substring match
219
+ */
220
+ filterFn?: (query: string, option: ComboboxOption) => boolean;
221
+ }
222
+
142
223
  interface CurrencyFieldProps<FormType> {
143
224
  name: FlexiblePath<FormType>;
144
225
  label?: string;
@@ -811,7 +892,7 @@ interface FieldMetadata {
811
892
  /** The data type of the field */
812
893
  type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'enum' | 'date' | 'union';
813
894
  /** The input type for rendering the field */
814
- inputType?: 'text' | 'email' | 'url' | 'tel' | 'number' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'password' | 'array' | 'currency' | 'date' | 'time' | 'datetime-local' | 'file' | 'range' | 'search';
895
+ inputType?: 'text' | 'email' | 'url' | 'tel' | 'number' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'password' | 'array' | 'currency' | 'date' | 'time' | 'datetime-local' | 'file' | 'range' | 'search' | 'combobox';
815
896
  /** The display label for the field */
816
897
  label?: string;
817
898
  /** Placeholder text for the field */
@@ -870,7 +951,7 @@ interface FieldMetadata {
870
951
  */
871
952
  interface SchemaFieldMetadata {
872
953
  /** The input type for rendering the field */
873
- inputType?: 'text' | 'email' | 'tel' | 'number' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'password' | 'currency' | 'date' | 'time' | 'datetime-local';
954
+ inputType?: 'text' | 'email' | 'tel' | 'number' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'password' | 'currency' | 'date' | 'time' | 'datetime-local' | 'combobox';
874
955
  /** Placeholder text for the field */
875
956
  placeholder?: string;
876
957
  /** The display label for the field */
@@ -928,6 +1009,7 @@ interface StyleCreators {
928
1009
  createSearchField: <FormType>() => FunctionalComponent<SearchFieldProps<FormType>, {}, any, {}>;
929
1010
  createTimeField: <FormType>() => FunctionalComponent<TimeFieldProps<FormType>, {}, any, {}>;
930
1011
  createDateTimeLocalField: <FormType>() => FunctionalComponent<DateTimeLocalFieldProps<FormType>, {}, any, {}>;
1012
+ createComboboxField: <FormType>() => FunctionalComponent<ComboboxFieldProps<FormType>, {}, any, {}>;
931
1013
  createArrayField: <FormType, ItemType = any>() => FunctionalComponent<ArrayFieldProps<FormType, ItemType>, {}, {
932
1014
  itemRenderer: (props: {
933
1015
  item: ItemType;
@@ -1107,6 +1189,12 @@ declare function useForm<T extends z.ZodObject<any>, CalcType = (values: z.infer
1107
1189
  resetField: (path: string) => void;
1108
1190
  hasError: (path: string) => boolean;
1109
1191
  getError: (path: string) => string;
1192
+ setFocus: (path: string, value: boolean) => void;
1193
+ isFocused: (path: string) => boolean;
1194
+ setDisplayText: (path: string, text: string) => void;
1195
+ getDisplayText: (path: string) => string;
1196
+ setSelectedIndex: (path: string, index: number) => void;
1197
+ getSelectedIndex: (path: string) => number;
1110
1198
  };
1111
1199
  ZiniaForm: vue.FunctionalComponent<FormProps<z.TypeOf<T>>, {}, any, {}>;
1112
1200
  ZiniaSubmitButton: vue.FunctionalComponent<SubmitButtonProps<z.TypeOf<T>>, {}, any, {}>;
package/dist/index.js CHANGED
@@ -151,6 +151,7 @@ function generateFieldComponents(schema, fieldsMetadata, styleName) {
151
151
  const SearchField = styleCreators.createSearchField();
152
152
  const TimeField = styleCreators.createTimeField();
153
153
  const DateTimeLocalField = styleCreators.createDateTimeLocalField();
154
+ const ComboboxField = styleCreators.createComboboxField();
154
155
  const ArrayField = styleCreators.createArrayField();
155
156
  const TransferListField = styleCreators.createTransferListField();
156
157
  const ToppingsField = styleCreators.createToppingsField();
@@ -241,6 +242,15 @@ function generateFieldComponents(schema, fieldsMetadata, styleName) {
241
242
  typedSelectFields[typedPath] = selectFieldComponent;
242
243
  return;
243
244
  // Skip the type-based switch below
245
+ case "combobox":
246
+ const comboboxFieldComponent = (props, context) => {
247
+ const metadataProps = createMetadataProps(fieldMeta);
248
+ return ComboboxField({ ...metadataProps, ...props, name: typedPath }, context || { attrs: {} });
249
+ };
250
+ fields[typedPath] = comboboxFieldComponent;
251
+ components[componentName] = comboboxFieldComponent;
252
+ return;
253
+ // Skip the type-based switch below
244
254
  case "text":
245
255
  const textFieldComponent = (props, context) => {
246
256
  const metadataProps = createMetadataProps(fieldMeta);
@@ -665,6 +675,36 @@ function hasFieldError(formState, path) {
665
675
  function getFieldError(formState, path) {
666
676
  return formState.errors[path] || "";
667
677
  }
678
+ function setFieldFocus(formState, path, value) {
679
+ if (value) {
680
+ formState.focused[path] = true;
681
+ } else {
682
+ delete formState.focused[path];
683
+ }
684
+ }
685
+ function isFieldFocused(formState, path) {
686
+ return !!formState.focused[path];
687
+ }
688
+ function setFieldDisplayText(formState, path, text) {
689
+ if (text) {
690
+ formState.displayText[path] = text;
691
+ } else {
692
+ delete formState.displayText[path];
693
+ }
694
+ }
695
+ function getFieldDisplayText(formState, path) {
696
+ return formState.displayText[path] || "";
697
+ }
698
+ function setFieldSelectedIndex(formState, path, index) {
699
+ if (index >= 0) {
700
+ formState.selectedIndex[path] = index;
701
+ } else {
702
+ delete formState.selectedIndex[path];
703
+ }
704
+ }
705
+ function getFieldSelectedIndex(formState, path) {
706
+ return formState.selectedIndex[path] ?? -1;
707
+ }
668
708
 
669
709
  // src/registry/metadataRegistry.ts
670
710
  var globalRegistry = {};
@@ -1335,6 +1375,9 @@ function useFormState(initialData, options) {
1335
1375
  touched: {},
1336
1376
  dirty: {},
1337
1377
  errors: {},
1378
+ focused: {},
1379
+ displayText: {},
1380
+ selectedIndex: {},
1338
1381
  // Form status
1339
1382
  isSubmitting: false,
1340
1383
  hasAttemptedSubmit: false,
@@ -1405,6 +1448,9 @@ function useFormState(initialData, options) {
1405
1448
  state.touched = {};
1406
1449
  state.dirty = {};
1407
1450
  state.errors = {};
1451
+ state.focused = {};
1452
+ state.displayText = {};
1453
+ state.selectedIndex = {};
1408
1454
  }
1409
1455
  function setSubmitting(value) {
1410
1456
  state.isSubmitting = value;
@@ -1797,7 +1843,13 @@ function useForm(schema, options) {
1797
1843
  isDirtyField: (path) => isDirtyField(formState.state, path),
1798
1844
  resetField: (path) => resetField(formState.state, initialFormData, path, schema, schemaCache),
1799
1845
  hasError: (path) => hasFieldError(formState.state, path),
1800
- getError: (path) => getFieldError(formState.state, path)
1846
+ getError: (path) => getFieldError(formState.state, path),
1847
+ setFocus: (path, value) => setFieldFocus(formState.state, path, value),
1848
+ isFocused: (path) => isFieldFocused(formState.state, path),
1849
+ setDisplayText: (path, text) => setFieldDisplayText(formState.state, path, text),
1850
+ getDisplayText: (path) => getFieldDisplayText(formState.state, path),
1851
+ setSelectedIndex: (path, index) => setFieldSelectedIndex(formState.state, path, index),
1852
+ getSelectedIndex: (path) => getFieldSelectedIndex(formState.state, path)
1801
1853
  };
1802
1854
  if (options.autoProvide !== false) {
1803
1855
  provide(ZINIA_FORM_SCHEMA_KEY, schema);
@@ -3818,6 +3870,396 @@ function createDaisyUICheckboxField() {
3818
3870
  ];
3819
3871
  return DaisyUICheckboxField;
3820
3872
  }
3873
+
3874
+ // src/fields/types/ComboboxFieldProps.ts
3875
+ var COMBOBOX_FIELD_PROP_NAMES = [
3876
+ "name",
3877
+ "options",
3878
+ "placeholder",
3879
+ "disabled",
3880
+ "readonly",
3881
+ "class",
3882
+ "label",
3883
+ "hideLabel",
3884
+ "description",
3885
+ "required",
3886
+ "size",
3887
+ "variant",
3888
+ "allowCreate",
3889
+ "filterFn"
3890
+ ];
3891
+
3892
+ // src/utils/propNormalization.ts
3893
+ function camelToKebab(str) {
3894
+ return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();
3895
+ }
3896
+ function getPropVariants(propsDefinition) {
3897
+ const variants = /* @__PURE__ */ new Set();
3898
+ if (Array.isArray(propsDefinition)) {
3899
+ propsDefinition.forEach((propName) => {
3900
+ variants.add(propName);
3901
+ variants.add(camelToKebab(propName));
3902
+ });
3903
+ } else {
3904
+ Object.keys(propsDefinition).forEach((propName) => {
3905
+ variants.add(propName);
3906
+ variants.add(camelToKebab(propName));
3907
+ });
3908
+ }
3909
+ return variants;
3910
+ }
3911
+ function filterComponentPropsFromAttrs(propsDefinition, attrs) {
3912
+ const propVariants = getPropVariants(propsDefinition);
3913
+ const filtered = {};
3914
+ for (const key in attrs) {
3915
+ if (!propVariants.has(key)) {
3916
+ filtered[key] = attrs[key];
3917
+ }
3918
+ }
3919
+ return filtered;
3920
+ }
3921
+ function normalizePropsFromAttrs(props, propsDefinition, attrs) {
3922
+ const propVariants = getPropVariants(propsDefinition);
3923
+ const normalized = { ...props };
3924
+ for (const key in attrs) {
3925
+ if (propVariants.has(key)) {
3926
+ const camelCaseKey = Array.isArray(propsDefinition) ? propsDefinition.find((p) => camelToKebab(p) === key) || key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : Object.keys(propsDefinition).find((p) => camelToKebab(p) === key) || key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
3927
+ if (!(camelCaseKey in normalized) || normalized[camelCaseKey] === void 0) {
3928
+ normalized[camelCaseKey] = attrs[key];
3929
+ }
3930
+ }
3931
+ }
3932
+ return normalized;
3933
+ }
3934
+ function createDaisyUIComboboxField() {
3935
+ const propsDefinition = [...COMBOBOX_FIELD_PROP_NAMES];
3936
+ const DaisyUIComboboxField = (props, { attrs }) => {
3937
+ const formState = inject(ZINIA_FORM_KEY);
3938
+ if (!formState) {
3939
+ console.error("ComboboxField: formState is required but was not injected");
3940
+ return null;
3941
+ }
3942
+ const fieldMetadata = formState.fieldsMetadata[props.name] || {};
3943
+ const normalizedProps = normalizePropsFromAttrs(props, propsDefinition, attrs);
3944
+ const normalizeOption = (option) => {
3945
+ if (typeof option === "string") {
3946
+ return { label: option, value: option };
3947
+ }
3948
+ return option;
3949
+ };
3950
+ const getOptions = computed(() => {
3951
+ if (normalizedProps.options) {
3952
+ return normalizedProps.options.map(normalizeOption);
3953
+ }
3954
+ const schemaOptions = fieldMetadata?.options || [];
3955
+ return schemaOptions.map((opt) => ({
3956
+ label: opt.label || String(opt.value),
3957
+ value: String(opt.value)
3958
+ }));
3959
+ });
3960
+ const filteredOptions = computed(() => {
3961
+ const query = String(displayText.value || "").toLowerCase();
3962
+ if (!query) {
3963
+ return getOptions.value;
3964
+ }
3965
+ const filterFn = normalizedProps.filterFn || ((q, opt) => {
3966
+ const normalized = normalizeOption(opt);
3967
+ return normalized.label.toLowerCase().includes(q.toLowerCase()) || normalized.value.toLowerCase().includes(q.toLowerCase());
3968
+ });
3969
+ return getOptions.value.filter((opt) => {
3970
+ const originalOpt = opt.label === opt.value ? opt.label : opt;
3971
+ return filterFn(query, originalOpt);
3972
+ });
3973
+ });
3974
+ const isNewValue = computed(() => {
3975
+ if (!normalizedProps.allowCreate) return false;
3976
+ const currentText = String(displayText.value || "").trim().toLowerCase();
3977
+ if (!currentText) return false;
3978
+ return !getOptions.value.some(
3979
+ (opt) => opt.value.toLowerCase() === currentText || opt.label.toLowerCase() === currentText
3980
+ );
3981
+ });
3982
+ const hasErrors = formState.hasError(props.name);
3983
+ const isTouched2 = formState.isTouched(props.name);
3984
+ const comboboxRoot = ref(null);
3985
+ const inputElement = ref(null);
3986
+ const selectedIndex = computed({
3987
+ get: () => formState.getSelectedIndex(props.name),
3988
+ set: (value) => formState.setSelectedIndex(props.name, value)
3989
+ });
3990
+ const isFocused = computed(() => formState.isFocused(props.name));
3991
+ const displayText = computed({
3992
+ get: () => formState.getDisplayText(props.name),
3993
+ set: (value) => formState.setDisplayText(props.name, value)
3994
+ });
3995
+ const currentFormValue = computed(() => formState.getValue(props.name));
3996
+ const getLabelForValue = (value) => {
3997
+ if (!value) return "";
3998
+ const valueStr = String(value);
3999
+ const option = getOptions.value.find((opt) => opt.value === valueStr);
4000
+ return option ? option.label : valueStr;
4001
+ };
4002
+ const displayLabelForValue = computed(() => getLabelForValue(currentFormValue.value));
4003
+ if (!isFocused.value && displayText.value !== displayLabelForValue.value) {
4004
+ displayText.value = displayLabelForValue.value;
4005
+ }
4006
+ const showDropdown = computed(() => {
4007
+ if (!isFocused.value) return false;
4008
+ if (getOptions.value.length > 0) return true;
4009
+ if (normalizedProps.allowCreate) {
4010
+ return String(formState.getValue(props.name) || "").trim().length > 0;
4011
+ }
4012
+ return false;
4013
+ });
4014
+ const inputClass = [
4015
+ "input",
4016
+ "pr-10",
4017
+ props.size ? `input-${props.size}` : "",
4018
+ props.variant ? `input-${props.variant}` : "",
4019
+ isTouched2 && !hasErrors ? "input-success" : "",
4020
+ isTouched2 && hasErrors ? "input-error" : "",
4021
+ props.class
4022
+ ].filter(Boolean).join(" ");
4023
+ const htmlAttrs = filterComponentPropsFromAttrs(propsDefinition, attrs);
4024
+ const inputProps = {
4025
+ type: "text",
4026
+ value: displayText.value,
4027
+ onInput: (event) => {
4028
+ const target = event.target;
4029
+ const inputValue = target.value;
4030
+ displayText.value = inputValue;
4031
+ formState.setSelectedIndex(props.name, -1);
4032
+ if (!isFocused.value) {
4033
+ formState.setFocus(props.name, true);
4034
+ }
4035
+ formState.setValue(props.name, inputValue);
4036
+ },
4037
+ onBlur: (event) => {
4038
+ const relatedTarget = event.relatedTarget;
4039
+ if (comboboxRoot.value?.contains(relatedTarget)) {
4040
+ return;
4041
+ }
4042
+ const displayValue = String(displayText.value || "").trim();
4043
+ const matchingOption = getOptions.value.find(
4044
+ (opt) => opt.label.toLowerCase() === displayValue.toLowerCase() || opt.value.toLowerCase() === displayValue.toLowerCase()
4045
+ );
4046
+ if (matchingOption) {
4047
+ formState.setValue(props.name, matchingOption.value);
4048
+ displayText.value = matchingOption.label;
4049
+ } else if (normalizedProps.allowCreate && displayValue) {
4050
+ formState.setValue(props.name, displayValue);
4051
+ displayText.value = displayValue;
4052
+ } else if (!displayValue) {
4053
+ formState.setValue(props.name, "");
4054
+ displayText.value = "";
4055
+ } else {
4056
+ formState.setValue(props.name, displayValue);
4057
+ }
4058
+ formState.touchField(props.name);
4059
+ formState.validateField(props.name);
4060
+ formState.setFocus(props.name, false);
4061
+ },
4062
+ onFocus: () => {
4063
+ formState.setFocus(props.name, true);
4064
+ const labelForValue = displayLabelForValue.value;
4065
+ if (labelForValue && displayText.value !== labelForValue) {
4066
+ displayText.value = labelForValue;
4067
+ }
4068
+ },
4069
+ onKeydown: (event) => {
4070
+ const { key } = event;
4071
+ switch (key) {
4072
+ case "ArrowDown":
4073
+ event.preventDefault();
4074
+ event.stopPropagation();
4075
+ formState.setFocus(props.name, true);
4076
+ const totalOptions = filteredOptions.value.length + (isNewValue.value ? 1 : 0);
4077
+ if (totalOptions > 0) {
4078
+ const currentIndex = selectedIndex.value;
4079
+ const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % totalOptions;
4080
+ selectedIndex.value = newIndex;
4081
+ nextTick(() => {
4082
+ const selectedElement = comboboxRoot.value?.querySelector(
4083
+ newIndex === filteredOptions.value.length ? "#option-new" : `#option-${newIndex}`
4084
+ );
4085
+ if (selectedElement) {
4086
+ selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" });
4087
+ }
4088
+ });
4089
+ }
4090
+ break;
4091
+ case "ArrowUp":
4092
+ event.preventDefault();
4093
+ event.stopPropagation();
4094
+ formState.setFocus(props.name, true);
4095
+ const totalOptionsUp = filteredOptions.value.length + (isNewValue.value ? 1 : 0);
4096
+ if (totalOptionsUp > 0) {
4097
+ const currentIndex = selectedIndex.value;
4098
+ const newIndex = currentIndex < 0 ? totalOptionsUp - 1 : (currentIndex - 1 + totalOptionsUp) % totalOptionsUp;
4099
+ selectedIndex.value = newIndex;
4100
+ nextTick(() => {
4101
+ const selectedElement = comboboxRoot.value?.querySelector(
4102
+ newIndex === filteredOptions.value.length ? "#option-new" : `#option-${newIndex}`
4103
+ );
4104
+ if (selectedElement) {
4105
+ selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" });
4106
+ }
4107
+ });
4108
+ }
4109
+ break;
4110
+ case "Enter":
4111
+ event.preventDefault();
4112
+ if (selectedIndex.value >= 0 && selectedIndex.value < filteredOptions.value.length) {
4113
+ const option = filteredOptions.value[selectedIndex.value];
4114
+ formState.setValue(props.name, option.value);
4115
+ displayText.value = option.label;
4116
+ formState.touchField(props.name);
4117
+ formState.validateField(props.name);
4118
+ formState.setSelectedIndex(props.name, -1);
4119
+ formState.setFocus(props.name, false);
4120
+ } else if (selectedIndex.value === filteredOptions.value.length && isNewValue.value) {
4121
+ const displayValue = String(displayText.value || "").trim();
4122
+ formState.setValue(props.name, displayValue);
4123
+ formState.touchField(props.name);
4124
+ formState.validateField(props.name);
4125
+ formState.setSelectedIndex(props.name, -1);
4126
+ formState.setFocus(props.name, false);
4127
+ } else if (isNewValue.value && selectedIndex.value < 0) {
4128
+ const displayValue = String(displayText.value || "").trim();
4129
+ formState.setValue(props.name, displayValue);
4130
+ formState.touchField(props.name);
4131
+ formState.validateField(props.name);
4132
+ formState.setSelectedIndex(props.name, -1);
4133
+ formState.setFocus(props.name, false);
4134
+ }
4135
+ break;
4136
+ case "Escape":
4137
+ inputElement.value?.blur();
4138
+ break;
4139
+ }
4140
+ },
4141
+ name: props.name,
4142
+ ...htmlAttrs
4143
+ };
4144
+ return /* @__PURE__ */ jsxs("label", { ref: comboboxRoot, class: "floating-label", children: [
4145
+ !props.hideLabel && /* @__PURE__ */ jsxs("span", { children: [
4146
+ props.label || fieldMetadata.label,
4147
+ fieldMetadata.isRequired && /* @__PURE__ */ jsx("span", { class: "text-error", children: " *" })
4148
+ ] }),
4149
+ /* @__PURE__ */ jsxs("div", { class: "relative inline-block max-w-full", style: "min-width: clamp(3rem, 20rem, 100%);", role: "combobox", "aria-expanded": showDropdown.value, "aria-haspopup": "listbox", children: [
4150
+ /* @__PURE__ */ jsx(
4151
+ "input",
4152
+ {
4153
+ ref: inputElement,
4154
+ class: inputClass,
4155
+ placeholder: props.placeholder || "Search or type new value",
4156
+ disabled: props.disabled,
4157
+ autocomplete: "off",
4158
+ "data-testid": `${formState.storeName}-combobox-field-${String(props.name)}`,
4159
+ ...inputProps
4160
+ }
4161
+ ),
4162
+ /* @__PURE__ */ jsx("div", { class: "absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none", children: /* @__PURE__ */ jsx(
4163
+ "svg",
4164
+ {
4165
+ xmlns: "http://www.w3.org/2000/svg",
4166
+ class: "h-5 w-5 text-base-content/50",
4167
+ fill: "none",
4168
+ viewBox: "0 0 24 24",
4169
+ stroke: "currentColor",
4170
+ "stroke-width": "2",
4171
+ children: /* @__PURE__ */ jsx("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M8 9l4-4 4 4m0 6l-4 4-4-4" })
4172
+ }
4173
+ ) }),
4174
+ showDropdown.value && /* @__PURE__ */ jsx("div", { class: "absolute z-50 mt-1 bg-base-100 border border-base-300 rounded-box shadow-lg max-h-60 overflow-auto", style: "min-width: 100%;", children: filteredOptions.value.length > 0 || isNewValue.value ? /* @__PURE__ */ jsxs("ul", { id: "combobox-listbox", class: "menu w-full", role: "listbox", children: [
4175
+ filteredOptions.value.map((option, index) => /* @__PURE__ */ jsx(
4176
+ "li",
4177
+ {
4178
+ id: `option-${index}`,
4179
+ role: "option",
4180
+ "aria-selected": index === selectedIndex.value,
4181
+ children: /* @__PURE__ */ jsx(
4182
+ "a",
4183
+ {
4184
+ href: "#",
4185
+ class: `w-full ${index === selectedIndex.value ? "menu-focus" : ""}`,
4186
+ onClick: (e) => {
4187
+ e.preventDefault();
4188
+ e.stopPropagation();
4189
+ formState.setValue(props.name, option.value);
4190
+ displayText.value = option.label;
4191
+ formState.touchField(props.name);
4192
+ formState.validateField(props.name);
4193
+ formState.setSelectedIndex(props.name, -1);
4194
+ formState.setFocus(props.name, false);
4195
+ },
4196
+ children: option.label
4197
+ }
4198
+ )
4199
+ },
4200
+ option.value
4201
+ )),
4202
+ isNewValue.value && /* @__PURE__ */ jsx(
4203
+ "li",
4204
+ {
4205
+ id: `option-new`,
4206
+ role: "option",
4207
+ "aria-selected": selectedIndex.value === filteredOptions.value.length,
4208
+ children: /* @__PURE__ */ jsx(
4209
+ "a",
4210
+ {
4211
+ href: "#",
4212
+ class: `w-full ${selectedIndex.value === filteredOptions.value.length ? "menu-focus" : ""}`,
4213
+ onClick: (e) => {
4214
+ e.preventDefault();
4215
+ e.stopPropagation();
4216
+ const displayValue = String(displayText.value || "").trim();
4217
+ formState.setValue(props.name, displayValue);
4218
+ formState.touchField(props.name);
4219
+ formState.validateField(props.name);
4220
+ formState.setSelectedIndex(props.name, -1);
4221
+ formState.setFocus(props.name, false);
4222
+ },
4223
+ children: /* @__PURE__ */ jsxs("div", { class: "flex items-start gap-2", children: [
4224
+ /* @__PURE__ */ jsx(
4225
+ "svg",
4226
+ {
4227
+ xmlns: "http://www.w3.org/2000/svg",
4228
+ class: "h-5 w-5 text-success mt-0.5 flex-shrink-0",
4229
+ fill: "none",
4230
+ viewBox: "0 0 24 24",
4231
+ stroke: "currentColor",
4232
+ "stroke-width": "2",
4233
+ children: /* @__PURE__ */ jsx("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M12 4v16m8-8H4" })
4234
+ }
4235
+ ),
4236
+ /* @__PURE__ */ jsxs("div", { class: "text-sm", children: [
4237
+ /* @__PURE__ */ jsx("p", { class: "font-medium text-success", children: "New value" }),
4238
+ /* @__PURE__ */ jsxs("p", { class: "text-base-content/70", children: [
4239
+ '"',
4240
+ displayText.value,
4241
+ '" will be saved.'
4242
+ ] })
4243
+ ] })
4244
+ ] })
4245
+ }
4246
+ )
4247
+ }
4248
+ )
4249
+ ] }) : null })
4250
+ ] }),
4251
+ props.description && /* @__PURE__ */ jsx("p", { class: "text-sm mt-1", children: props.description }),
4252
+ isTouched2 && hasErrors && /* @__PURE__ */ jsx("p", { class: "text-error text-xs", children: formState.getError(props.name) }),
4253
+ isNewValue.value && displayText.value && !hasErrors && /* @__PURE__ */ jsxs("p", { class: "text-success text-xs", children: [
4254
+ '\u2713 New value "',
4255
+ displayText.value,
4256
+ '" will be saved.'
4257
+ ] })
4258
+ ] });
4259
+ };
4260
+ DaisyUIComboboxField.props = propsDefinition;
4261
+ return DaisyUIComboboxField;
4262
+ }
3821
4263
  function createDaisyUICurrencyField() {
3822
4264
  const DaisyUICurrencyField = (props, { attrs }) => {
3823
4265
  const formState = inject(ZINIA_FORM_KEY);
@@ -4774,49 +5216,6 @@ var SELECT_FIELD_PROP_NAMES = [
4774
5216
  "valueToLabel",
4775
5217
  "useSchemaOptions"
4776
5218
  ];
4777
-
4778
- // src/utils/propNormalization.ts
4779
- function camelToKebab(str) {
4780
- return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();
4781
- }
4782
- function getPropVariants(propsDefinition) {
4783
- const variants = /* @__PURE__ */ new Set();
4784
- if (Array.isArray(propsDefinition)) {
4785
- propsDefinition.forEach((propName) => {
4786
- variants.add(propName);
4787
- variants.add(camelToKebab(propName));
4788
- });
4789
- } else {
4790
- Object.keys(propsDefinition).forEach((propName) => {
4791
- variants.add(propName);
4792
- variants.add(camelToKebab(propName));
4793
- });
4794
- }
4795
- return variants;
4796
- }
4797
- function filterComponentPropsFromAttrs(propsDefinition, attrs) {
4798
- const propVariants = getPropVariants(propsDefinition);
4799
- const filtered = {};
4800
- for (const key in attrs) {
4801
- if (!propVariants.has(key)) {
4802
- filtered[key] = attrs[key];
4803
- }
4804
- }
4805
- return filtered;
4806
- }
4807
- function normalizePropsFromAttrs(props, propsDefinition, attrs) {
4808
- const propVariants = getPropVariants(propsDefinition);
4809
- const normalized = { ...props };
4810
- for (const key in attrs) {
4811
- if (propVariants.has(key)) {
4812
- const camelCaseKey = Array.isArray(propsDefinition) ? propsDefinition.find((p) => camelToKebab(p) === key) || key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : Object.keys(propsDefinition).find((p) => camelToKebab(p) === key) || key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
4813
- if (!(camelCaseKey in normalized) || normalized[camelCaseKey] === void 0) {
4814
- normalized[camelCaseKey] = attrs[key];
4815
- }
4816
- }
4817
- }
4818
- return normalized;
4819
- }
4820
5219
  function createDaisyUISelectField() {
4821
5220
  const propsDefinition = [...SELECT_FIELD_PROP_NAMES];
4822
5221
  const DaisyUISelectField = (props, { attrs }) => {
@@ -7375,6 +7774,7 @@ var daisyUIStyle = {
7375
7774
  createSearchField: createDaisyUISearchField,
7376
7775
  createTimeField: createDaisyUITimeField,
7377
7776
  createDateTimeLocalField: createDaisyUIDateTimeLocalField,
7777
+ createComboboxField: createDaisyUIComboboxField,
7378
7778
  createArrayField: createDaisyUIArrayField,
7379
7779
  createTransferListField: createDaisyUITransferListField,
7380
7780
  createToppingsField: createDaisyUIToppingsField,