@mapomodule/form 2.0.0-beta.1 → 2.0.0-beta.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.
Files changed (144) hide show
  1. package/dist/__tests__/__mocks__/nuxt-imports.d.ts +4 -0
  2. package/dist/__tests__/__mocks__/nuxt-imports.d.ts.map +1 -0
  3. package/dist/__tests__/registry.test.d.ts +2 -0
  4. package/dist/__tests__/registry.test.d.ts.map +1 -0
  5. package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
  6. package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
  7. package/dist/__tests__/useMapoForm.test.d.ts +2 -0
  8. package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
  9. package/dist/index.d.mts +9 -1
  10. package/dist/index.d.ts +15 -1
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.mjs +7 -34
  13. package/dist/module.d.mts +20 -2
  14. package/dist/module.d.ts +12 -2
  15. package/dist/module.d.ts.map +1 -0
  16. package/dist/module.json +1 -1
  17. package/dist/module.mjs +167 -13
  18. package/dist/runtime/components/MapoFocusPortal.vue +113 -0
  19. package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
  20. package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
  21. package/dist/runtime/components/MapoForm.vue +168 -0
  22. package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
  23. package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
  24. package/dist/runtime/components/MapoFormField.vue +269 -0
  25. package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
  26. package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
  27. package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
  28. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
  29. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
  30. package/dist/runtime/components/MapoFormGroup.vue +247 -0
  31. package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
  32. package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
  33. package/dist/runtime/components/MapoFormTabs.vue +108 -0
  34. package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
  35. package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
  36. package/dist/runtime/components/MapoUnknownField.vue +62 -0
  37. package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
  38. package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
  39. package/dist/runtime/components/fields/MapoDateField.vue +86 -0
  40. package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
  41. package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
  42. package/dist/runtime/components/fields/MapoDateTimeField.vue +157 -0
  43. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
  44. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
  45. package/dist/runtime/components/fields/MapoFileField.vue +97 -0
  46. package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
  47. package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
  48. package/dist/runtime/components/fields/MapoFksField.vue +118 -0
  49. package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
  50. package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
  51. package/dist/runtime/components/fields/MapoMapField.vue +97 -0
  52. package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
  53. package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
  54. package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
  55. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
  56. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
  57. package/dist/runtime/components/fields/MapoRepeater.vue +449 -0
  58. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
  59. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
  60. package/dist/runtime/components/fields/MapoRepeaterItem.vue +341 -0
  61. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
  62. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
  63. package/dist/runtime/components/fields/MapoSeoPreview.vue +118 -0
  64. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
  65. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
  66. package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
  67. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
  68. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
  69. package/dist/runtime/components/fields/MapoWygEditor.vue +227 -0
  70. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
  71. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
  72. package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
  73. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
  74. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
  75. package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
  76. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
  77. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
  78. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
  79. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
  80. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
  81. package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
  82. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
  83. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
  84. package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
  85. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
  86. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
  87. package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
  88. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
  89. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
  90. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
  91. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
  92. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
  93. package/dist/runtime/composables/defineFormField.d.ts +25 -0
  94. package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
  95. package/dist/runtime/composables/defineFormField.js +27 -0
  96. package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
  97. package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
  98. package/dist/runtime/composables/useCurrentLang.js +9 -0
  99. package/dist/runtime/composables/useFocusMode.d.ts +40 -0
  100. package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
  101. package/dist/runtime/composables/useFocusMode.js +31 -0
  102. package/dist/runtime/composables/useFormDraft.d.ts +60 -0
  103. package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
  104. package/dist/runtime/composables/useFormDraft.js +52 -0
  105. package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
  106. package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
  107. package/dist/runtime/composables/useFormFromSchema.js +142 -0
  108. package/dist/runtime/composables/useMapoForm.d.ts +96 -0
  109. package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
  110. package/dist/runtime/composables/useMapoForm.js +323 -0
  111. package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
  112. package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
  113. package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
  114. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
  115. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
  116. package/dist/runtime/composables/useUnsavedChangesGuard.js +27 -0
  117. package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
  118. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
  119. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
  120. package/dist/runtime/plugins/form-registry.d.ts +18 -0
  121. package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
  122. package/dist/runtime/plugins/form-registry.js +19 -0
  123. package/dist/runtime/registry/defaults.d.ts +3 -0
  124. package/dist/runtime/registry/defaults.d.ts.map +1 -0
  125. package/dist/runtime/registry/defaults.js +87 -0
  126. package/dist/runtime/types/descriptor.d.ts +359 -0
  127. package/dist/runtime/types/descriptor.d.ts.map +1 -0
  128. package/dist/runtime/types/descriptor.js +26 -0
  129. package/dist/runtime/types/index.d.ts +11 -0
  130. package/dist/runtime/types/index.d.ts.map +1 -0
  131. package/dist/runtime/types/index.js +6 -0
  132. package/dist/runtime/types/layout.d.ts +21 -0
  133. package/dist/runtime/types/layout.d.ts.map +1 -0
  134. package/dist/runtime/types/layout.js +0 -0
  135. package/dist/runtime/types/registry.d.ts +62 -0
  136. package/dist/runtime/types/registry.d.ts.map +1 -0
  137. package/dist/runtime/types/registry.js +19 -0
  138. package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
  139. package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
  140. package/dist/runtime/utils/flattenFieldGroups.js +24 -0
  141. package/dist/types.d.mts +6 -10
  142. package/package.json +3 -3
  143. package/dist/index.d.cts +0 -1
  144. package/dist/module.d.cts +0 -2
@@ -0,0 +1,52 @@
1
+ import { watch, onMounted, onUnmounted } from "vue";
2
+ import { debounce } from "@mapomodule/utils";
3
+ export function useFormDraft(options) {
4
+ const storageKey = options.userId != null ? `mapo:draft:${options.userId}:${options.key}` : `mapo:draft:${options.key}`;
5
+ const ttl = options.ttl ?? 864e5;
6
+ const canUseStorage = typeof localStorage !== "undefined";
7
+ const debouncedSave = debounce(() => {
8
+ if (!canUseStorage) return;
9
+ if (!options.isDirty.value) return;
10
+ const entry = {
11
+ model: options.model.value,
12
+ savedAt: Date.now()
13
+ };
14
+ try {
15
+ localStorage.setItem(storageKey, JSON.stringify(entry));
16
+ options.onSave?.(new Date(entry.savedAt));
17
+ } catch {
18
+ }
19
+ }, options.debounce ?? 2e3);
20
+ const stopWatch = watch(options.model, debouncedSave, { deep: true });
21
+ onMounted(() => {
22
+ const draft = getDraft();
23
+ if (!draft) return;
24
+ options.onRestore?.(draft.model, new Date(draft.savedAt));
25
+ });
26
+ onUnmounted(() => {
27
+ stopWatch();
28
+ });
29
+ function getDraft() {
30
+ if (!canUseStorage) return null;
31
+ try {
32
+ const raw = localStorage.getItem(storageKey);
33
+ if (!raw) return null;
34
+ const entry = JSON.parse(raw);
35
+ if (Date.now() - entry.savedAt > ttl) {
36
+ localStorage.removeItem(storageKey);
37
+ return null;
38
+ }
39
+ return entry;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ function clearDraft() {
45
+ if (!canUseStorage) return;
46
+ localStorage.removeItem(storageKey);
47
+ }
48
+ function hasDraft() {
49
+ return getDraft() !== null;
50
+ }
51
+ return { clearDraft, getDraft, hasDraft };
52
+ }
@@ -0,0 +1,21 @@
1
+ import type { JSONSchema } from "@mapomodule/utils";
2
+ import type { AnyFieldDescriptor } from "../types/index.js";
3
+ /** Options for generating field descriptors from a JSON Schema document. */
4
+ export interface UseFormFromSchemaOptions {
5
+ /** Keys to exclude from generation, for example `id` or `created_at`. */
6
+ exclude?: string[];
7
+ /** Per-field overrides that shallow-merge onto the generated descriptor. */
8
+ overrides?: Record<string, Partial<AnyFieldDescriptor>>;
9
+ }
10
+ /**
11
+ * Builds a `AnyFieldDescriptor[]` array from a JSON Schema.
12
+ * Supports Pydantic v2 (`$defs`, nullable `anyOf`), DRF Spectacular, and `if/then/else` conditionals.
13
+ *
14
+ * @example
15
+ * const fields = useFormFromSchema(myApiSchema, {
16
+ * exclude: ['id', 'created_at'],
17
+ * overrides: { body: { type: 'editor', translatable: true } },
18
+ * })
19
+ */
20
+ export declare function useFormFromSchema(schema: JSONSchema, options?: UseFormFromSchemaOptions): AnyFieldDescriptor[];
21
+ //# sourceMappingURL=useFormFromSchema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFormFromSchema.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useFormFromSchema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAa,MAAM,mBAAmB,CAAC;AAsMvE,4EAA4E;AAC5E,MAAM,WAAW,wBAAwB;IACvC,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE,wBAA6B,GACrC,kBAAkB,EAAE,CA+BtB"}
@@ -0,0 +1,142 @@
1
+ import { resolveSchema, extractDefs, matchesSchema } from "@mapomodule/utils";
2
+ function mapType(schema, key) {
3
+ if (schema.enum) return "select";
4
+ const t = Array.isArray(schema.type) ? schema.type.find((v) => v !== "null") : schema.type;
5
+ const fmt = schema.format;
6
+ if (t === "string") {
7
+ if (fmt === "date") return "date";
8
+ if (fmt === "date-time") return "datetime";
9
+ if (fmt === "time") return "time";
10
+ if (fmt === "color") return "color";
11
+ if (typeof schema.maxLength === "number" && schema.maxLength > 255)
12
+ return "textarea";
13
+ return "text";
14
+ }
15
+ if (t === "integer" || t === "number") return "number";
16
+ if (t === "boolean") return "boolean";
17
+ if (t === "relation") {
18
+ return schema.multiple ? "m2m" : "fks";
19
+ }
20
+ if (t === "array") {
21
+ const items = schema.items;
22
+ if (items?.type === "relation") return "m2m";
23
+ if (items?.type === "object" || items?.properties) return "repeater";
24
+ return "select";
25
+ }
26
+ if (t === "object") {
27
+ const props = Object.keys(schema.properties ?? {});
28
+ if (props.includes("title") && props.includes("description") && (props.includes("url") || props.includes("slug")))
29
+ return "seo";
30
+ return `object:${key}`;
31
+ }
32
+ return "text";
33
+ }
34
+ function buildAttrs(schema, type) {
35
+ const attrs = {};
36
+ if (type === "select") {
37
+ if (schema.enum) {
38
+ attrs.options = schema.enum.map((v) => ({
39
+ text: String(v),
40
+ value: v
41
+ }));
42
+ }
43
+ }
44
+ if (type === "number") {
45
+ if (schema.minimum !== void 0) attrs.min = schema.minimum;
46
+ if (schema.maximum !== void 0) attrs.max = schema.maximum;
47
+ }
48
+ if (type === "text" || type === "textarea") {
49
+ if (schema.maxLength !== void 0) attrs.maxLength = schema.maxLength;
50
+ if (schema.minLength !== void 0) attrs.minLength = schema.minLength;
51
+ }
52
+ if (type === "fks" || type === "m2m") {
53
+ const ext = schema;
54
+ attrs.endpoint = ext.endpoint ?? ext.relation ?? "";
55
+ attrs.returnObject = true;
56
+ if (type === "m2m") attrs.multiple = true;
57
+ }
58
+ if (type === "date" || type === "datetime") {
59
+ if (schema.minimum) attrs.min = schema.minimum;
60
+ if (schema.maximum) attrs.max = schema.maximum;
61
+ }
62
+ return attrs;
63
+ }
64
+ function collectRules(schema) {
65
+ const rules = [];
66
+ function addRule(ifSchema, then, elseBranch) {
67
+ rules.push({
68
+ ifSchema,
69
+ thenKeys: new Set(Object.keys(then?.properties ?? {})),
70
+ elseKeys: new Set(Object.keys(elseBranch?.properties ?? {}))
71
+ });
72
+ }
73
+ if (schema.if) addRule(schema.if, schema.then, schema.else);
74
+ if (Array.isArray(schema.allOf)) {
75
+ for (const entry of schema.allOf) {
76
+ if (entry?.if) addRule(entry.if, entry.then, entry.else);
77
+ }
78
+ }
79
+ return rules;
80
+ }
81
+ function makeVisible(ifSchema, side) {
82
+ return ({ model }) => {
83
+ const matches = matchesSchema(model, ifSchema);
84
+ return side === "then" ? matches : !matches;
85
+ };
86
+ }
87
+ function propertyToDescriptor(key, rawSchema, defs, requiredKeys, rules) {
88
+ const schema = resolveSchema(rawSchema, defs);
89
+ const type = mapType(schema, key);
90
+ const attrs = buildAttrs(schema, type);
91
+ let visible;
92
+ for (const rule of rules) {
93
+ if (rule.thenKeys.has(key) && !rule.elseKeys.has(key)) {
94
+ visible = makeVisible(rule.ifSchema, "then");
95
+ break;
96
+ }
97
+ if (rule.elseKeys.has(key) && !rule.thenKeys.has(key)) {
98
+ visible = makeVisible(rule.ifSchema, "else");
99
+ break;
100
+ }
101
+ }
102
+ const base = {
103
+ key,
104
+ type,
105
+ label: schema.title ?? void 0,
106
+ required: requiredKeys.has(key),
107
+ ...visible ? { visible } : {},
108
+ ...Object.keys(attrs).length ? { attrs } : {}
109
+ };
110
+ if (type === "repeater" && schema.items) {
111
+ const itemSchema = resolveSchema(schema.items, defs);
112
+ const childRequired = new Set(itemSchema.required ?? []);
113
+ const childProps = itemSchema.properties ?? {};
114
+ const childRules = collectRules(itemSchema);
115
+ base.fields = Object.entries(childProps).map(
116
+ ([k, v]) => propertyToDescriptor(k, v, defs, childRequired, childRules)
117
+ );
118
+ }
119
+ return base;
120
+ }
121
+ export function useFormFromSchema(schema, options = {}) {
122
+ const { exclude = [], overrides = {} } = options;
123
+ const defs = extractDefs(schema);
124
+ const properties = schema.properties ?? {};
125
+ const requiredKeys = new Set(schema.required ?? []);
126
+ const rules = collectRules(schema);
127
+ const descriptors = [];
128
+ for (const [key, rawProp] of Object.entries(properties)) {
129
+ if (exclude.includes(key)) continue;
130
+ const descriptor = propertyToDescriptor(
131
+ key,
132
+ rawProp,
133
+ defs,
134
+ requiredKeys,
135
+ rules
136
+ );
137
+ const override = overrides[key];
138
+ const final = override ? { ...descriptor, ...override } : descriptor;
139
+ descriptors.push(final);
140
+ }
141
+ return descriptors;
142
+ }
@@ -0,0 +1,96 @@
1
+ import { type Ref, type MaybeRef } from "vue";
2
+ import type { AnyFieldDescriptor, FieldRegistry } from "../types/index.js";
3
+ /** Options accepted by `useMapoForm()`. */
4
+ export interface UseMapoFormOptions<T extends object> {
5
+ model: Ref<T>;
6
+ fields: MaybeRef<AnyFieldDescriptor<T>[]>;
7
+ errors?: Ref<Record<string, string[]>>;
8
+ languages?: string[];
9
+ currentLang?: Ref<string>;
10
+ immediate?: boolean;
11
+ /** Global debounce in milliseconds. Override per field with `descriptor.debounce`. Default: `300`. */
12
+ debounce?: number;
13
+ /**
14
+ * When `false`, suppresses the toast that lists server-side field errors.
15
+ * Set this for sub-forms (e.g. each repeater item) so they don't duplicate the
16
+ * root form's error toast. Default: `true`.
17
+ */
18
+ notifyErrors?: boolean;
19
+ registry: FieldRegistry;
20
+ /**
21
+ * When set, enables automatic draft persistence to localStorage.
22
+ * Key is prefixed with `mapo:draft:`. Drafts expire after 24 h.
23
+ * Call the returned `checkDraft()` after the model is populated to offer restore.
24
+ * Example: `"article:42"` → `mapo:draft:article:42`.
25
+ */
26
+ draftKey?: MaybeRef<string | undefined>;
27
+ }
28
+ /** State exposed when a pending draft is found in localStorage. */
29
+ export interface MapoDraftBanner {
30
+ savedAt: Date;
31
+ restore: () => void;
32
+ discard: () => void;
33
+ }
34
+ /** Reactive context shared with child form fields via provide/inject. */
35
+ export interface MapoFormContext<T extends object> {
36
+ model: Ref<T>;
37
+ fields: MaybeRef<AnyFieldDescriptor<T>[]>;
38
+ errors: Ref<Record<string, string[]>>;
39
+ languages: string[];
40
+ currentLang: Ref<string>;
41
+ readonly: Ref<boolean>;
42
+ immediate: boolean;
43
+ debounce: number;
44
+ registry: FieldRegistry;
45
+ setFieldValue: (key: string, val: unknown) => void;
46
+ getClientError: (key: string) => string | null;
47
+ markTouched: (key: string) => void;
48
+ isTouched: (key: string) => boolean;
49
+ submitted: Ref<boolean>;
50
+ }
51
+ /** Injection key for the current Mapo form context. */
52
+ export declare const FORM_KEY: unique symbol;
53
+ /** Injects the nearest form context, if present. */
54
+ export declare function injectMapoForm<T extends object>(): MapoFormContext<T> | undefined;
55
+ /**
56
+ * Creates the core reactive state and helpers for a Mapo form.
57
+ * Handles dirty tracking, field accessors, sync validation, and submit flows.
58
+ */
59
+ export declare function useMapoForm<T extends object>(options: UseMapoFormOptions<T>): {
60
+ state: import("vue").ComputedRef<{
61
+ model: T;
62
+ errors: Record<string, string[]>;
63
+ isDirty: boolean;
64
+ currentLang: string;
65
+ isLoading: boolean;
66
+ }>;
67
+ isDirty: import("vue").ComputedRef<boolean>;
68
+ isLoading: Ref<boolean, boolean>;
69
+ readonly: Ref<boolean, boolean>;
70
+ currentLang: Ref<string, string>;
71
+ getFieldValue: (descriptor: AnyFieldDescriptor<T>) => unknown;
72
+ setFieldValue: (keyOrDescriptor: string | AnyFieldDescriptor<T>, val: unknown) => void;
73
+ getClientError: (keyOrDescriptor: string | AnyFieldDescriptor<T>) => string | null;
74
+ markTouched: (key: string) => void;
75
+ isTouched: (key: string) => boolean;
76
+ validateClient: () => {
77
+ valid: boolean;
78
+ errors: Record<string, string>;
79
+ };
80
+ resetDirty: () => void;
81
+ getPatch: () => Partial<T>;
82
+ submit: (handler: (model: T | Partial<T>, isNew: boolean) => Promise<void>, isNew?: boolean) => Promise<void>;
83
+ ctx: MapoFormContext<T>;
84
+ draftBanner: Ref<{
85
+ savedAt: Date;
86
+ restore: () => void;
87
+ discard: () => void;
88
+ } | null, MapoDraftBanner | {
89
+ savedAt: Date;
90
+ restore: () => void;
91
+ discard: () => void;
92
+ } | null>;
93
+ checkDraft: () => void;
94
+ clearDraft: () => void;
95
+ };
96
+ //# sourceMappingURL=useMapoForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMapoForm.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useMapoForm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,323 @@
1
+ import {
2
+ ref,
3
+ computed,
4
+ toRaw,
5
+ toValue,
6
+ watch,
7
+ provide,
8
+ inject
9
+ } from "vue";
10
+ import {
11
+ getNestedValue,
12
+ setNestedValueMutating,
13
+ objectDiff,
14
+ debounce
15
+ } from "@mapomodule/utils";
16
+ import { resolveFieldAccessor } from "../types/index.js";
17
+ import { useCurrentLang } from "./useCurrentLang.js";
18
+ import { useSnackStore } from "@mapomodule/store/runtime/stores/snack";
19
+ import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
20
+ function safeClone(v) {
21
+ const raw = toRaw(v);
22
+ if (typeof structuredClone === "function") {
23
+ try {
24
+ return structuredClone(raw);
25
+ } catch {
26
+ }
27
+ }
28
+ return JSON.parse(JSON.stringify(raw));
29
+ }
30
+ export const FORM_KEY = Symbol("mapoForm");
31
+ function provideMapoForm(ctx) {
32
+ provide(FORM_KEY, ctx);
33
+ }
34
+ export function injectMapoForm() {
35
+ return inject(FORM_KEY, void 0);
36
+ }
37
+ export function useMapoForm(options) {
38
+ const {
39
+ model,
40
+ fields,
41
+ errors = ref({}),
42
+ languages = [],
43
+ currentLang: currentLangProp,
44
+ immediate = false,
45
+ debounce: debounceMs = 300,
46
+ notifyErrors = true,
47
+ registry
48
+ } = options;
49
+ const currentLang = useCurrentLang(currentLangProp);
50
+ const snack = useSnackStore();
51
+ const authStore = useAuthStore();
52
+ const backup = ref(safeClone(model.value));
53
+ const clientErrors = ref({});
54
+ const isLoading = ref(false);
55
+ const readonly = ref(false);
56
+ const touched = ref(/* @__PURE__ */ new Set());
57
+ const submitted = ref(false);
58
+ if (notifyErrors) {
59
+ watch(
60
+ errors,
61
+ (val) => {
62
+ const entries = Object.entries(val);
63
+ if (!entries.length) return;
64
+ const lines = entries.map(([field, msgs]) => `${field}: ${msgs.join(", ")}`).join("\n");
65
+ snack.show(lines, "error");
66
+ },
67
+ { deep: true }
68
+ );
69
+ }
70
+ const dirtyKeys = ref(/* @__PURE__ */ new Set());
71
+ const isDirty = computed(() => dirtyKeys.value.size > 0);
72
+ const DRAFT_TTL = 864e5;
73
+ const draftBanner = ref(null);
74
+ function _draftStorageKey() {
75
+ const key = toValue(options.draftKey);
76
+ if (!key) return null;
77
+ const userId = authStore.info?.id ?? "anonymous";
78
+ return `mapo:draft:${userId}:${key}`;
79
+ }
80
+ function clearDraft() {
81
+ const key = _draftStorageKey();
82
+ if (key && typeof localStorage !== "undefined")
83
+ localStorage.removeItem(key);
84
+ }
85
+ function checkDraft() {
86
+ const key = _draftStorageKey();
87
+ if (!key || typeof localStorage === "undefined") return;
88
+ try {
89
+ const raw = localStorage.getItem(key);
90
+ if (!raw) return;
91
+ const entry = JSON.parse(raw);
92
+ if (Date.now() - entry.savedAt > DRAFT_TTL) {
93
+ localStorage.removeItem(key);
94
+ return;
95
+ }
96
+ const savedAt = new Date(entry.savedAt);
97
+ draftBanner.value = {
98
+ savedAt,
99
+ restore: () => {
100
+ Object.assign(model.value, entry.model);
101
+ draftBanner.value = null;
102
+ },
103
+ discard: () => {
104
+ localStorage.removeItem(key);
105
+ draftBanner.value = null;
106
+ }
107
+ };
108
+ } catch {
109
+ }
110
+ }
111
+ if (options.draftKey) {
112
+ const saveDraft = debounce(() => {
113
+ const key = _draftStorageKey();
114
+ if (!key || !isDirty.value || typeof localStorage === "undefined") return;
115
+ try {
116
+ const noDraftKeys = new Set(
117
+ toValue(fields).filter((f) => f.noDraft).map((f) => f.key)
118
+ );
119
+ const snapshot = toRaw(model.value);
120
+ const filtered = noDraftKeys.size > 0 ? Object.fromEntries(
121
+ Object.entries(snapshot).filter(([k]) => !noDraftKeys.has(k))
122
+ ) : snapshot;
123
+ localStorage.setItem(
124
+ key,
125
+ JSON.stringify({ model: filtered, savedAt: Date.now() })
126
+ );
127
+ } catch {
128
+ }
129
+ }, 2e3);
130
+ watch(model, saveDraft, { deep: true });
131
+ }
132
+ function markTouched(key) {
133
+ if (touched.value.has(key)) return;
134
+ touched.value.add(key);
135
+ touched.value = new Set(touched.value);
136
+ }
137
+ function isTouched(key) {
138
+ return submitted.value || touched.value.has(key);
139
+ }
140
+ const allErrors = computed(() => {
141
+ const merged = { ...errors.value };
142
+ for (const [k, v] of Object.entries(clientErrors.value)) {
143
+ merged[k] = [v, ...merged[k] ?? []];
144
+ }
145
+ return merged;
146
+ });
147
+ function resolvePath(descriptor) {
148
+ const lang = currentLang.value;
149
+ if (descriptor.translatable && lang) {
150
+ return `translations.${lang}.${descriptor.key}`;
151
+ }
152
+ return descriptor.key;
153
+ }
154
+ function initLang(lang) {
155
+ const m = model.value;
156
+ if (!m.translations) m.translations = {};
157
+ const translations = m.translations;
158
+ if (!translations[lang]) translations[lang] = {};
159
+ }
160
+ function setFieldValue(descriptor, val) {
161
+ const accessor = resolveFieldAccessor(descriptor, registry);
162
+ const setVal = accessor.set ? accessor.set({ model: model.value, val, lang: currentLang.value }) : val;
163
+ if (descriptor.synci18n && languages.length) {
164
+ for (const lang of languages) {
165
+ initLang(lang);
166
+ setNestedValueMutating(
167
+ model.value,
168
+ `translations.${lang}.${descriptor.key}`,
169
+ setVal
170
+ );
171
+ }
172
+ } else {
173
+ if (descriptor.translatable && currentLang.value)
174
+ initLang(currentLang.value);
175
+ setNestedValueMutating(
176
+ model.value,
177
+ resolvePath(descriptor),
178
+ setVal
179
+ );
180
+ }
181
+ descriptor.onChange?.({ model: model.value, val: setVal });
182
+ const key = descriptor.key;
183
+ if (!dirtyKeys.value.has(key)) {
184
+ dirtyKeys.value.add(key);
185
+ dirtyKeys.value = new Set(dirtyKeys.value);
186
+ }
187
+ if (clientErrors.value[key]) {
188
+ const next = { ...clientErrors.value };
189
+ delete next[key];
190
+ clientErrors.value = next;
191
+ }
192
+ }
193
+ function getFieldValue(descriptor) {
194
+ const raw = getNestedValue(model.value, resolvePath(descriptor));
195
+ const accessor = resolveFieldAccessor(descriptor, registry);
196
+ return accessor.get ? accessor.get({ model: model.value, val: raw, lang: currentLang.value }) : raw;
197
+ }
198
+ function checkRequired(descriptor, val) {
199
+ if (!descriptor.required) return null;
200
+ if (val === null || val === void 0 || val === "")
201
+ return "This field is required.";
202
+ return null;
203
+ }
204
+ function getClientError(descriptor) {
205
+ if (!descriptor.required && !descriptor.validate) return null;
206
+ if (!immediate && !isTouched(descriptor.key)) return null;
207
+ const val = getFieldValue(descriptor);
208
+ return checkRequired(descriptor, val) ?? descriptor.validate?.(val, {
209
+ model: model.value
210
+ }) ?? null;
211
+ }
212
+ function validateClient() {
213
+ submitted.value = true;
214
+ const errs = {};
215
+ const fs = toValue(fields);
216
+ for (const descriptor of fs) {
217
+ if (!descriptor.required && !descriptor.validate) continue;
218
+ const val = getFieldValue(descriptor);
219
+ const err = checkRequired(descriptor, val) ?? descriptor.validate?.(val, {
220
+ model: model.value
221
+ }) ?? null;
222
+ if (err) errs[descriptor.key] = err;
223
+ }
224
+ clientErrors.value = errs;
225
+ return { valid: Object.keys(errs).length === 0, errors: errs };
226
+ }
227
+ function resetDirty() {
228
+ backup.value = safeClone(model.value);
229
+ dirtyKeys.value = /* @__PURE__ */ new Set();
230
+ }
231
+ function getPatch() {
232
+ return objectDiff(backup.value, model.value);
233
+ }
234
+ async function submit(handler, isNew = false) {
235
+ if (isLoading.value) return;
236
+ submitted.value = true;
237
+ const { valid } = validateClient();
238
+ if (!valid) {
239
+ snack.show("Please fix the errors before saving.", "error");
240
+ return;
241
+ }
242
+ isLoading.value = true;
243
+ try {
244
+ await handler(isNew ? model.value : getPatch(), isNew);
245
+ resetDirty();
246
+ clearDraft();
247
+ submitted.value = false;
248
+ touched.value = /* @__PURE__ */ new Set();
249
+ } finally {
250
+ isLoading.value = false;
251
+ }
252
+ }
253
+ const fieldIndex = computed(() => {
254
+ const map = /* @__PURE__ */ new Map();
255
+ for (const f of toValue(fields)) map.set(f.key, f);
256
+ return map;
257
+ });
258
+ const findDescriptor = (key) => fieldIndex.value.get(key);
259
+ const ctx = {
260
+ model,
261
+ fields,
262
+ errors: allErrors,
263
+ languages,
264
+ currentLang,
265
+ readonly,
266
+ immediate,
267
+ debounce: debounceMs,
268
+ registry,
269
+ setFieldValue: (key, val) => {
270
+ const descriptor = findDescriptor(key);
271
+ if (descriptor) setFieldValue(descriptor, val);
272
+ },
273
+ getClientError: (key) => {
274
+ const descriptor = findDescriptor(key);
275
+ return descriptor ? getClientError(descriptor) : null;
276
+ },
277
+ markTouched,
278
+ isTouched,
279
+ submitted
280
+ };
281
+ function setFieldValuePublic(keyOrDescriptor, val) {
282
+ if (typeof keyOrDescriptor === "string") {
283
+ const descriptor = findDescriptor(keyOrDescriptor);
284
+ if (descriptor) setFieldValue(descriptor, val);
285
+ return;
286
+ }
287
+ setFieldValue(keyOrDescriptor, val);
288
+ }
289
+ function getClientErrorPublic(keyOrDescriptor) {
290
+ if (typeof keyOrDescriptor === "string") {
291
+ const descriptor = findDescriptor(keyOrDescriptor);
292
+ return descriptor ? getClientError(descriptor) : null;
293
+ }
294
+ return getClientError(keyOrDescriptor);
295
+ }
296
+ provideMapoForm(ctx);
297
+ return {
298
+ state: computed(() => ({
299
+ model: model.value,
300
+ errors: allErrors.value,
301
+ isDirty: isDirty.value,
302
+ currentLang: currentLang.value,
303
+ isLoading: isLoading.value
304
+ })),
305
+ isDirty,
306
+ isLoading,
307
+ readonly,
308
+ currentLang,
309
+ getFieldValue,
310
+ setFieldValue: setFieldValuePublic,
311
+ getClientError: getClientErrorPublic,
312
+ markTouched,
313
+ isTouched,
314
+ validateClient,
315
+ resetDirty,
316
+ getPatch,
317
+ submit,
318
+ ctx,
319
+ draftBanner,
320
+ checkDraft,
321
+ clearDraft
322
+ };
323
+ }
@@ -0,0 +1,44 @@
1
+ type Matcher<T = unknown> = (val: T) => boolean;
2
+ type Condition<TModel extends object> = (ctx: {
3
+ model: TModel;
4
+ }) => boolean;
5
+ /**
6
+ * Combines conditions with AND: all conditions must match.
7
+ *
8
+ * @example
9
+ * visible: when(matchesField('seo_enabled', true), matchesField('status', isOneOf(['published'])))
10
+ */
11
+ export declare function when<TModel extends object>(...conditions: Condition<TModel>[]): Condition<TModel>;
12
+ /**
13
+ * Combines conditions with OR: at least one condition must match.
14
+ */
15
+ export declare function whenAny<TModel extends object>(...conditions: Condition<TModel>[]): Condition<TModel>;
16
+ /**
17
+ * Negates a condition.
18
+ */
19
+ export declare function whenNot<TModel extends object>(condition: Condition<TModel>): Condition<TModel>;
20
+ /**
21
+ * Checks that a field value satisfies a matcher, or equals a literal value.
22
+ * Supports dotted paths such as `meta.status`.
23
+ *
24
+ * @example
25
+ * matchesField('type', 'article')
26
+ * matchesField('count', greaterThan(0))
27
+ */
28
+ export declare function matchesField<TModel extends object>(key: string, matcher: unknown | Matcher): Condition<TModel>;
29
+ /** The value is included in the options array. */
30
+ export declare const isOneOf: <T>(options: T[]) => (val: unknown) => boolean;
31
+ /** The value is NOT included in the options array. */
32
+ export declare const isNoneOf: <T>(options: T[]) => (val: unknown) => boolean;
33
+ /** The value is non-empty (not null, not undefined, not '', not false, array with at least one item). */
34
+ export declare const isNotEmpty: () => (val: unknown) => boolean;
35
+ /** The value is empty. */
36
+ export declare const isEmpty: () => (val: unknown) => boolean;
37
+ /** Returns true when the numeric value is greater than `n`. */
38
+ export declare const greaterThan: (n: number) => (val: unknown) => boolean;
39
+ /** Returns true when the numeric value is less than `n`. */
40
+ export declare const lessThan: (n: number) => (val: unknown) => boolean;
41
+ /** Returns true when the stringified value matches the given regular expression. */
42
+ export declare const matches: (pattern: RegExp) => (val: unknown) => boolean;
43
+ export {};
44
+ //# sourceMappingURL=useProgressiveDisclosure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useProgressiveDisclosure.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useProgressiveDisclosure.ts"],"names":[],"mappings":"AAEA,KAAK,OAAO,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;AAChD,KAAK,SAAS,CAAC,MAAM,SAAS,MAAM,IAAI,CAAC,GAAG,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC;AAE5E;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,MAAM,SAAS,MAAM,EACxC,GAAG,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,GACjC,SAAS,CAAC,MAAM,CAAC,CAEnB;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,MAAM,SAAS,MAAM,EAC3C,GAAG,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,GACjC,SAAS,CAAC,MAAM,CAAC,CAEnB;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,MAAM,SAAS,MAAM,EAC3C,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,GAC3B,SAAS,CAAC,MAAM,CAAC,CAEnB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,SAAS,MAAM,EAChD,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,GAAG,OAAO,GACzB,SAAS,CAAC,MAAM,CAAC,CAOnB;AAID,kDAAkD;AAClD,eAAO,MAAM,OAAO,GACjB,CAAC,EAAE,SAAS,CAAC,EAAE,MACf,KAAK,OAAO,KAAG,OACY,CAAC;AAE/B,sDAAsD;AACtD,eAAO,MAAM,QAAQ,GAClB,CAAC,EAAE,SAAS,CAAC,EAAE,MACf,KAAK,OAAO,KAAG,OACa,CAAC;AAEhC,yGAAyG;AACzG,eAAO,MAAM,UAAU,SAEpB,KAAK,OAAO,KAAG,OAIyB,CAAC;AAE5C,0BAA0B;AAC1B,eAAO,MAAM,OAAO,SAEjB,KAAK,OAAO,KAAG,OACI,CAAC;AAEvB,+DAA+D;AAC/D,eAAO,MAAM,WAAW,GACrB,GAAG,MAAM,MACT,KAAK,OAAO,KAAG,OACC,CAAC;AAEpB,4DAA4D;AAC5D,eAAO,MAAM,QAAQ,GAClB,GAAG,MAAM,MACT,KAAK,OAAO,KAAG,OACC,CAAC;AAEpB,oFAAoF;AACpF,eAAO,MAAM,OAAO,GACjB,SAAS,MAAM,MACf,KAAK,OAAO,KAAG,OACiB,CAAC"}