@ramathibodi/nuxt-commons 4.0.10 → 4.0.12

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 (44) hide show
  1. package/README.md +8 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/document/TemplateBuilder.d.vue.ts +8 -3
  4. package/dist/runtime/components/document/TemplateBuilder.vue +22 -42
  5. package/dist/runtime/components/document/TemplateBuilder.vue.d.ts +8 -3
  6. package/dist/runtime/components/form/ActionPad.vue +1 -0
  7. package/dist/runtime/components/form/Birthdate.d.vue.ts +3 -3
  8. package/dist/runtime/components/form/Birthdate.vue.d.ts +3 -3
  9. package/dist/runtime/components/form/Date.vue +11 -6
  10. package/dist/runtime/components/form/Dialog.d.vue.ts +1 -5
  11. package/dist/runtime/components/form/Dialog.vue +1 -0
  12. package/dist/runtime/components/form/Dialog.vue.d.ts +1 -5
  13. package/dist/runtime/components/form/EditPad.vue +1 -0
  14. package/dist/runtime/components/form/Pad.d.vue.ts +24 -0
  15. package/dist/runtime/components/form/Pad.vue +12 -7
  16. package/dist/runtime/components/form/Pad.vue.d.ts +24 -0
  17. package/dist/runtime/components/form/Time.vue +10 -5
  18. package/dist/runtime/components/form/images/Edit.d.vue.ts +1 -3
  19. package/dist/runtime/components/form/images/Edit.vue.d.ts +1 -3
  20. package/dist/runtime/components/model/AutoRefreshChip.d.vue.ts +16 -0
  21. package/dist/runtime/components/model/AutoRefreshChip.vue +34 -0
  22. package/dist/runtime/components/model/AutoRefreshChip.vue.d.ts +16 -0
  23. package/dist/runtime/components/model/Table.d.vue.ts +91 -61
  24. package/dist/runtime/components/model/Table.vue +24 -5
  25. package/dist/runtime/components/model/Table.vue.d.ts +91 -61
  26. package/dist/runtime/components/model/iterator.d.vue.ts +103 -71
  27. package/dist/runtime/components/model/iterator.vue +24 -5
  28. package/dist/runtime/components/model/iterator.vue.d.ts +103 -71
  29. package/dist/runtime/composables/apiModel.d.ts +2 -2
  30. package/dist/runtime/composables/apiModel.js +3 -3
  31. package/dist/runtime/composables/autoRefresh.d.ts +42 -0
  32. package/dist/runtime/composables/autoRefresh.js +57 -0
  33. package/dist/runtime/composables/document/template.js +10 -1
  34. package/dist/runtime/composables/document/templateInputTypes.d.ts +228 -0
  35. package/dist/runtime/composables/document/templateInputTypes.js +128 -0
  36. package/dist/runtime/composables/graphqlModel.d.ts +2 -2
  37. package/dist/runtime/composables/graphqlModel.js +3 -3
  38. package/dist/runtime/composables/modelAutoRefresh.d.ts +29 -0
  39. package/dist/runtime/composables/modelAutoRefresh.js +16 -0
  40. package/dist/runtime/composables/utils/validation.d.ts +4 -0
  41. package/dist/runtime/composables/utils/validation.js +2 -0
  42. package/package.json +4 -2
  43. package/scripts/generate-ai-summary.mjs +88 -0
  44. package/scripts/scaffold-playground-pages.mjs +207 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Registry of input types available in `<DocumentTemplateBuilder>` (the
3
+ * structured editor for document templates) AND consumable by the runtime
4
+ * renderer (`useDocumentTemplate` in `./template.ts`).
5
+ *
6
+ * **Single registration entry point:** `registerDocumentTemplateInputType`.
7
+ *
8
+ * Use it from a Nuxt plugin to add an entry app-wide. The same `entry` object
9
+ * carries everything — dropdown label, builder-UI flags, and an optional
10
+ * `render` function consumed by the runtime renderer in `template.ts`:
11
+ *
12
+ * ```ts
13
+ * // app/plugins/document-template-inputs.ts
14
+ * export default defineNuxtPlugin(() => {
15
+ * registerDocumentTemplateInputType({
16
+ * label: 'From Staff',
17
+ * value: 'FromStaff',
18
+ * render(item, _parents, dataVariable) {
19
+ * return `<FromStaff :staff-id="${dataVariable}.${item.variableName}" />`
20
+ * },
21
+ * })
22
+ * })
23
+ * ```
24
+ *
25
+ * For rare page-scoped extensions, call `useDocumentTemplateInputType()` in
26
+ * `<script setup>` — it wraps `register` + `onScopeDispose` so the entry is
27
+ * removed automatically when the component unmounts.
28
+ *
29
+ * Merge order (later wins on `value` collision):
30
+ * built-in ⊕ runtime-registry
31
+ *
32
+ * The runtime renderer dispatches as follows inside `templateItemToString`:
33
+ * 1. If the registered entry for `inputType` has a `render` function → call it.
34
+ * 2. Else if `inputType` is one of the built-in special cases (Header,
35
+ * Separator, FormTable, …) → use the hand-written switch arm.
36
+ * 3. Else → `processDefaultTemplate` (the v-model/label fallback).
37
+ *
38
+ * The `computedValue` post-pass (appending `<FormHidden>` to write a derived
39
+ * value back to `data[variableName]`) still runs unconditionally, so custom
40
+ * renderers don't need to handle it.
41
+ */
42
+ import type { DocumentTemplateItem } from './template.js';
43
+ /**
44
+ * Signature for a custom renderer registered against a specific `inputType`.
45
+ *
46
+ * Must return the Vue template fragment string to splice into the document
47
+ * template. Receives:
48
+ *
49
+ * - `item` — a `cloneDeep`'d copy of the template item, with two
50
+ * transformations already applied to `item.inputAttributes`:
51
+ * 1. `migrateInputAttributes()` — Vuetify v3→v4 attribute migration
52
+ * (e.g. bare `dense` → `density="compact"`).
53
+ * 2. If `item.conditionalDisplay` was set, `v-if="<expr>"` has already
54
+ * been appended. **Renderers don't need to handle
55
+ * `conditionalDisplay` separately** — just include
56
+ * `item.inputAttributes` on the emitted tag.
57
+ * - `parentTemplates` — chain of ancestor template codes (used by nested
58
+ * types like `DocumentForm` / `FormTable`); pass-through to nested
59
+ * `useDocumentTemplate()` calls when relevant.
60
+ * - `dataVariable` — the form `data` variable name. `"data"` at the top
61
+ * level; nested tables pass their row alias (e.g. `"row"`).
62
+ *
63
+ * **Helpers safe to import from `./template`:**
64
+ *
65
+ * - `buildValidationRules(item.validationRules)` — produces the
66
+ * `:rules="[rules.x()]"` attribute string (or `''` when no rules).
67
+ * The same helper used by the built-in switch arms.
68
+ * - `optionStringToChoiceObject(item.inputOptions)` — parses
69
+ * `"a|1,b|2"` / array forms into `ChoiceItem[]`.
70
+ * - `escapeObjectForInlineBinding(obj)` — `JSON.stringify` + `'` escaping
71
+ * for inline `v-bind` payloads.
72
+ * - `processDefaultTemplate(item, insideTemplate?, optionString?, validationRules?, dataVariable?)`
73
+ * — the same fallback used for unknown types. Call it from a renderer
74
+ * when you want "the default plus a wrapper".
75
+ *
76
+ * **Handled for you AFTER `render` returns:**
77
+ *
78
+ * - The `computedValue` post-pass that appends
79
+ * `<FormHidden v-model="data.x" :hook="()=>computedValue"/>`
80
+ * when both `item.computedValue` and `item.variableName` are set.
81
+ * - Column wrapping (`<v-col cols=…>`), `columnAttributes`, `customClass`,
82
+ * `customStyle` are applied by the caller (`columnWrapTemplateItemString`).
83
+ *
84
+ * **What `optionString` is and why it isn't passed:** the built-in switch
85
+ * arms precompute an `optionString` for the specific built-in option-bearing
86
+ * types (`MasterAutocomplete`, `VSelect`, `VAutocomplete`, `VCombobox`,
87
+ * `FormCheckboxGroup`, `VRadio`, `VRadioInline`, `Header`). For any
88
+ * custom-registered `inputType` that string is empty, so passing it would
89
+ * be dead weight. Compose your own option markup with the two helpers above
90
+ * when needed.
91
+ *
92
+ * **Minimal "field-shaped" renderer template:**
93
+ *
94
+ * ```ts
95
+ * import { buildValidationRules } from '@ramathibodi/nuxt-commons'
96
+ *
97
+ * render(item, _parents, dataVariable) {
98
+ * const variable = (item.variableName ?? '').replace(/[^\w$]/g, '')
99
+ * const attrs = item.inputAttributes ? ` ${item.inputAttributes.trim()}` : ''
100
+ * const rules = item.validationRules ? buildValidationRules(item.validationRules) : ''
101
+ * return `<MyComponent `
102
+ * + `:value="${dataVariable}.${variable}" `
103
+ * + `@update:value="v => ${dataVariable}.${variable} = v"`
104
+ * + `${attrs}${rules ? ' ' + rules : ''}/>`
105
+ * }
106
+ * ```
107
+ */
108
+ export type DocumentTemplateInputTypeRenderer = (item: DocumentTemplateItem, parentTemplates: string | string[], dataVariable: string) => string;
109
+ export interface DocumentTemplateInputType {
110
+ /** Label shown in the input-type dropdown. */
111
+ label: string;
112
+ /**
113
+ * `inputType` value stored in the template JSON. For app-specific types,
114
+ * this must be a globally-registered Vue component tag — the runtime
115
+ * renderer emits it as `<value v-model=… label=… …/>`.
116
+ */
117
+ value: string;
118
+ /** Default `true`. Set `false` for decoration types (e.g. `Header`, `Separator`). */
119
+ needsVariableName?: boolean;
120
+ /** Default `true`. Set `false` for types that have no label (e.g. `Separator`, `CustomCode`). */
121
+ needsLabel?: boolean;
122
+ /** Default `true`. Set `false` for types laid out outside the grid (e.g. `Separator`, `FormHidden`). */
123
+ needsWidth?: boolean;
124
+ /**
125
+ * Whether the generic `inputOptions` editor (textarea / specific editor)
126
+ * is shown. Default `true`. Set `false` only when the type has no
127
+ * meaningful inputOptions (e.g. `Separator`, `CustomCode`, `FormFile`).
128
+ */
129
+ showsOptions?: boolean;
130
+ /** Default `false`. Set `true` to make `inputOptions` required in the editor. */
131
+ requiresOptions?: boolean;
132
+ /** Override the textarea label (`'Input Options'` by default). */
133
+ optionsLabel?: string;
134
+ /**
135
+ * Render the choice-array editor instead of a plain textarea. Applies to
136
+ * the choice-style inputs (select, radio, checkbox-group, etc.).
137
+ */
138
+ optionsAsChoice?: boolean;
139
+ /**
140
+ * Marks the type as having a custom inputOptions sub-editor handled inline
141
+ * by `TemplateBuilder.vue` (FormHidden / FormTable / FormTableData). Apps
142
+ * cannot extend this; new built-ins must update the builder UI as well.
143
+ */
144
+ hasSpecificOptionEditor?: boolean;
145
+ /** Default `false`. Hides the Validations expansion panel. */
146
+ hidesValidationRules?: boolean;
147
+ /** Default `false`. Hides the `Input Attributes` field inside Advanced settings. */
148
+ hidesInputAttributes?: boolean;
149
+ /** Default `false`. Hides the `Column Attributes` field inside Advanced settings. */
150
+ hidesColumnAttributes?: boolean;
151
+ /** Default `false`. Hides the entire Advanced settings expansion panel. */
152
+ hidesAdvancedSettings?: boolean;
153
+ /** Default `false`. Hides the Class and Styles expansion panel. */
154
+ hidesClassAndStyle?: boolean;
155
+ /**
156
+ * Optional renderer that overrides the default `<Tag v-model="…" label="…"/>`
157
+ * emission for this `inputType`. Use when an app component does not follow
158
+ * the standard contract (different value prop, slot content, etc.).
159
+ */
160
+ render?: DocumentTemplateInputTypeRenderer;
161
+ }
162
+ /**
163
+ * Built-in registry. One entry per inputType that `TemplateBuilder.vue`
164
+ * historically hard-coded. Flag values mirror the previous arrays
165
+ * (`requireOption`, `notRequireVariable`, `notRequireLabel`, `notRequireWidth`,
166
+ * `notRequireOptions`, `notRequireRules`, `notRequireInputAttributes`,
167
+ * `notRequireColumnAttributes`, `notRequireAdvancedSetting`,
168
+ * `notRequireClassAndStyle`, `choiceOption`, `inputOptionsLabel`,
169
+ * `hasSpecificOption`).
170
+ */
171
+ export declare const BUILT_IN_DOCUMENT_TEMPLATE_INPUT_TYPES: readonly DocumentTemplateInputType[];
172
+ /**
173
+ * Merge multiple lists of input-type definitions. Later entries win on
174
+ * `value` collision, allowing a consumer to override a built-in entry's
175
+ * label or flags (e.g. relabel "Text Field" to "Free Text" in one app).
176
+ *
177
+ * Render functions follow the same precedence: a later entry without
178
+ * `render` does NOT clear an earlier-registered render (the shallow merge
179
+ * keeps the prior value). To explicitly remove a render, set
180
+ * `render: undefined` on the later entry, or call
181
+ * `unregisterDocumentTemplateInputType`.
182
+ */
183
+ export declare function mergeDocumentTemplateInputTypes(...lists: Array<readonly DocumentTemplateInputType[] | DocumentTemplateInputType[] | undefined | null>): DocumentTemplateInputType[];
184
+ /**
185
+ * Register (or upsert) an input-type entry. Returns an `unregister()`
186
+ * function so callers — especially HMR-aware Nuxt plugins — can revert the
187
+ * registration. Calling `register` twice with the same `value` performs a
188
+ * shallow merge, matching the wider `mergeDocumentTemplateInputTypes`
189
+ * semantics. No-op (and returns a no-op unregister) when `entry` lacks a
190
+ * string `value`.
191
+ *
192
+ * **Where to call from:**
193
+ * - Nuxt plugin (`app/plugins/*.ts`) — the canonical place for app-wide
194
+ * registration. Entries persist for the lifetime of the app.
195
+ * - Inside a Vue component setup — prefer `useDocumentTemplateInputType`
196
+ * instead; it wraps this function with auto-cleanup on unmount.
197
+ */
198
+ export declare function registerDocumentTemplateInputType(entry: DocumentTemplateInputType): () => void;
199
+ /** Explicit removal — handy in unit tests and for one-off cleanup. */
200
+ export declare function unregisterDocumentTemplateInputType(value: string): void;
201
+ /**
202
+ * Page-scoped registration. Calls `registerDocumentTemplateInputType` and
203
+ * schedules the returned `unregister()` on `onScopeDispose`, so the entry
204
+ * is removed automatically when the surrounding effect scope (typically a
205
+ * Vue component's setup) is disposed.
206
+ *
207
+ * If called outside an effect scope, the registration persists for the
208
+ * lifetime of the runtime — at which point you should use
209
+ * `registerDocumentTemplateInputType` directly so the lifecycle is explicit.
210
+ */
211
+ export declare function useDocumentTemplateInputType(entry: DocumentTemplateInputType): void;
212
+ /** Snapshot of the runtime registry. Internal — exported for tests. */
213
+ export declare function _getDocumentTemplateRuntimeRegistry(): DocumentTemplateInputType[];
214
+ /** Test-only reset. Does not affect built-ins. */
215
+ export declare function _resetDocumentTemplateRuntimeRegistry(): void;
216
+ /**
217
+ * Look up a single entry from the merged registry (built-in ⊕ runtime).
218
+ * Used by `templateItemToString` to decide whether a custom `render` is in
219
+ * play.
220
+ */
221
+ export declare function getDocumentTemplateInputTypeEntry(value: string): DocumentTemplateInputType | undefined;
222
+ /**
223
+ * Read the merged input-type registry: built-in ⊕ runtime registry.
224
+ * Stable across calls (re-reads on every call so HMR-driven re-registration
225
+ * picks up). Returns a fresh array — callers may mutate without affecting
226
+ * the underlying state.
227
+ */
228
+ export declare function useDocumentTemplateInputTypes(): DocumentTemplateInputType[];
@@ -0,0 +1,128 @@
1
+ import { getCurrentScope, onScopeDispose } from "vue";
2
+ export const BUILT_IN_DOCUMENT_TEMPLATE_INPUT_TYPES = Object.freeze([
3
+ { label: "Text Field", value: "VTextField" },
4
+ { label: "Text Area", value: "VTextarea" },
5
+ { label: "Text Date Picker", value: "FormDate" },
6
+ { label: "Text Time Picker", value: "FormTime" },
7
+ { label: "Text Date & Time Picker", value: "FormDateTime" },
8
+ { label: "Select Dropdown", value: "VSelect", requiresOptions: true, optionsAsChoice: true },
9
+ { label: "Select Combobox", value: "VCombobox", requiresOptions: true, optionsAsChoice: true },
10
+ { label: "Autocomplete", value: "VAutocomplete", requiresOptions: true, optionsAsChoice: true },
11
+ { label: "Autocomplete from Master", value: "MasterAutocomplete", requiresOptions: true, optionsLabel: "Group Key" },
12
+ { label: "Radio Buttons", value: "VRadio", requiresOptions: true, optionsAsChoice: true },
13
+ { label: "Radio Buttons Inline", value: "VRadioInline", requiresOptions: true, optionsAsChoice: true },
14
+ { label: "Checkbox", value: "VCheckbox" },
15
+ { label: "Checkbox Group", value: "FormCheckboxGroup", requiresOptions: true, optionsAsChoice: true },
16
+ { label: "Switch", value: "VSwitch" },
17
+ { label: "File Upload", value: "FormFile", needsLabel: false, showsOptions: false },
18
+ { label: "Signature Pad", value: "FormSignPad" },
19
+ { label: "Table", value: "FormTable", requiresOptions: true, hasSpecificOptionEditor: true },
20
+ { label: "Table Data", value: "FormTableData", requiresOptions: true, hasSpecificOptionEditor: true },
21
+ {
22
+ label: "[Decoration] Header",
23
+ value: "Header",
24
+ needsVariableName: false,
25
+ needsWidth: false,
26
+ optionsLabel: "Class",
27
+ hidesValidationRules: true,
28
+ hidesInputAttributes: true
29
+ },
30
+ {
31
+ label: "[Decoration] Separator",
32
+ value: "Separator",
33
+ needsVariableName: false,
34
+ needsLabel: false,
35
+ needsWidth: false,
36
+ showsOptions: false,
37
+ hidesValidationRules: true,
38
+ hidesInputAttributes: true,
39
+ hidesColumnAttributes: true,
40
+ hidesAdvancedSettings: true
41
+ },
42
+ {
43
+ label: "[Advanced] Hidden Field",
44
+ value: "FormHidden",
45
+ needsLabel: false,
46
+ needsWidth: false,
47
+ hasSpecificOptionEditor: true,
48
+ hidesValidationRules: true,
49
+ hidesColumnAttributes: true,
50
+ hidesClassAndStyle: true
51
+ },
52
+ {
53
+ label: "[Advanced] Inherit Form",
54
+ value: "DocumentForm",
55
+ needsVariableName: false,
56
+ needsLabel: false,
57
+ needsWidth: false,
58
+ requiresOptions: true,
59
+ optionsLabel: "Template Code",
60
+ hidesValidationRules: true,
61
+ hidesInputAttributes: true,
62
+ hidesColumnAttributes: true
63
+ },
64
+ {
65
+ label: "[Advanced] Custom Code",
66
+ value: "CustomCode",
67
+ needsVariableName: false,
68
+ needsLabel: false,
69
+ showsOptions: false,
70
+ hidesValidationRules: true,
71
+ hidesInputAttributes: true,
72
+ hidesAdvancedSettings: true
73
+ }
74
+ ]);
75
+ export function mergeDocumentTemplateInputTypes(...lists) {
76
+ const byValue = /* @__PURE__ */ new Map();
77
+ for (const list of lists) {
78
+ if (!list) continue;
79
+ for (const entry of list) {
80
+ if (!entry || typeof entry.value !== "string" || entry.value.length === 0) continue;
81
+ byValue.set(entry.value, { ...byValue.get(entry.value), ...entry });
82
+ }
83
+ }
84
+ return [...byValue.values()];
85
+ }
86
+ const runtimeRegistry = /* @__PURE__ */ new Map();
87
+ export function registerDocumentTemplateInputType(entry) {
88
+ if (!entry || typeof entry.value !== "string" || entry.value.length === 0) {
89
+ return () => {
90
+ };
91
+ }
92
+ const previous = runtimeRegistry.get(entry.value);
93
+ const merged = { ...previous, ...entry };
94
+ runtimeRegistry.set(entry.value, merged);
95
+ return () => {
96
+ if (runtimeRegistry.get(entry.value) === merged) {
97
+ runtimeRegistry.delete(entry.value);
98
+ }
99
+ };
100
+ }
101
+ export function unregisterDocumentTemplateInputType(value) {
102
+ runtimeRegistry.delete(value);
103
+ }
104
+ export function useDocumentTemplateInputType(entry) {
105
+ const unregister = registerDocumentTemplateInputType(entry);
106
+ if (getCurrentScope()) {
107
+ onScopeDispose(unregister);
108
+ }
109
+ }
110
+ export function _getDocumentTemplateRuntimeRegistry() {
111
+ return [...runtimeRegistry.values()];
112
+ }
113
+ export function _resetDocumentTemplateRuntimeRegistry() {
114
+ runtimeRegistry.clear();
115
+ }
116
+ export function getDocumentTemplateInputTypeEntry(value) {
117
+ if (!value) return void 0;
118
+ const fromRuntime = runtimeRegistry.get(value);
119
+ const builtIn = BUILT_IN_DOCUMENT_TEMPLATE_INPUT_TYPES.find((t) => t.value === value);
120
+ if (fromRuntime) return { ...builtIn, ...fromRuntime };
121
+ return builtIn;
122
+ }
123
+ export function useDocumentTemplateInputTypes() {
124
+ return mergeDocumentTemplateInputTypes(
125
+ BUILT_IN_DOCUMENT_TEMPLATE_INPUT_TYPES,
126
+ _getDocumentTemplateRuntimeRegistry()
127
+ );
128
+ }
@@ -29,7 +29,7 @@ export declare function useGraphqlModel<T extends GraphqlModelProps>(props: T):
29
29
  importItems: (importItems: Record<string, any>[], callback?: FormDialogCallback) => void;
30
30
  updateItem: (item: Record<string, any>, callback?: FormDialogCallback) => Promise<void>;
31
31
  deleteItem: (item: Record<string, any>, callback?: FormDialogCallback) => Promise<any>;
32
- loadItems: (options: any) => void;
33
- reload: () => void;
32
+ loadItems: (options: any) => Promise<void> | undefined;
33
+ reload: () => Promise<void> | undefined;
34
34
  isLoading: import("vue").Ref<boolean, boolean>;
35
35
  };
@@ -127,7 +127,7 @@ export function useGraphqlModel(props) {
127
127
  sortBy: options.sortBy
128
128
  };
129
129
  isLoading.value = true;
130
- operationReadPageable.value?.call([{ data: fields.value }, "meta"], Object.assign({}, props.modelBy, { pageable: pageableVariable })).then((result) => {
130
+ return operationReadPageable.value?.call([{ data: fields.value }, "meta"], Object.assign({}, props.modelBy, { pageable: pageableVariable })).then((result) => {
131
131
  items.value = result.data;
132
132
  itemsLength.value = result.meta.totalItems;
133
133
  }).catch((error) => {
@@ -141,10 +141,10 @@ export function useGraphqlModel(props) {
141
141
  }
142
142
  function reload() {
143
143
  if (canServerPageable.value) {
144
- if (currentOptions.value) loadItems(currentOptions.value);
144
+ if (currentOptions.value) return loadItems(currentOptions.value);
145
145
  } else {
146
146
  isLoading.value = true;
147
- operationRead.value?.call(fields.value, props.modelBy).then((result) => {
147
+ return operationRead.value?.call(fields.value, props.modelBy).then((result) => {
148
148
  items.value = arrayWrap(result);
149
149
  }).catch((error) => {
150
150
  items.value = [];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared auto-refresh wiring for `<ModelTable>` and `<ModelIterator>`.
3
+ *
4
+ * Both components expose the same `autoRefresh` / `autoRefreshDefault` / `autoRefreshControl`
5
+ * props and gate polling on the same signals (dialog open + in-flight loading). This helper
6
+ * keeps that contract in one place: pass the model's `reload`/`isLoading` and the dialog-open
7
+ * ref, get back the `useAutoRefresh` handle plus a `manualReload` that reloads and restarts
8
+ * the countdown (used by the toolbar refresh icon).
9
+ */
10
+ import { type Ref } from 'vue';
11
+ import { type UseAutoRefreshHandle } from './autoRefresh.js';
12
+ export interface ModelAutoRefreshProps {
13
+ autoRefresh?: number | boolean;
14
+ autoRefreshDefault?: number;
15
+ }
16
+ export interface UseModelAutoRefreshDeps {
17
+ /** The model composable's reload(). */
18
+ reload: () => void | Promise<void>;
19
+ /** The model composable's loading flag. */
20
+ isLoading: Ref<boolean>;
21
+ /** Component ref that is true while a create/edit dialog is open. */
22
+ isDialogOpen: Ref<boolean>;
23
+ }
24
+ export interface UseModelAutoRefreshHandle {
25
+ autoRefresh: UseAutoRefreshHandle;
26
+ /** Reload now and restart the countdown — wired to the toolbar refresh icon. */
27
+ manualReload: () => void;
28
+ }
29
+ export declare function useModelAutoRefresh(props: ModelAutoRefreshProps, deps: UseModelAutoRefreshDeps): UseModelAutoRefreshHandle;
@@ -0,0 +1,16 @@
1
+ import { computed } from "vue";
2
+ import { useAutoRefresh } from "./autoRefresh.js";
3
+ export function useModelAutoRefresh(props, deps) {
4
+ const autoRefresh = useAutoRefresh({
5
+ interval: () => props.autoRefresh,
6
+ defaultSeconds: props.autoRefreshDefault,
7
+ reload: deps.reload,
8
+ isLoading: deps.isLoading,
9
+ paused: computed(() => deps.isDialogOpen.value)
10
+ });
11
+ function manualReload() {
12
+ deps.reload();
13
+ autoRefresh.reset();
14
+ }
15
+ return { autoRefresh, manualReload };
16
+ }
@@ -3,6 +3,7 @@ export declare function useRules(): {
3
3
  requireIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
4
4
  requireTrue: (customError?: string) => (value: any) => string | true;
5
5
  requireTrueIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
6
+ requireNotEmpty: (customError?: string) => (value: any) => string | true;
6
7
  numeric: (customError?: string) => (value: any) => string | true;
7
8
  range: (minValue: number, maxValue: number, customError?: string) => (value: any) => string | true;
8
9
  integer: (customError?: string) => (value: any) => string | true;
@@ -31,6 +32,7 @@ export declare function useRules(): {
31
32
  requireIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
32
33
  requireTrue: (customError?: string) => (value: any) => string | true;
33
34
  requireTrueIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
35
+ requireNotEmpty: (customError?: string) => (value: any) => string | true;
34
36
  numeric: (customError?: string) => (value: any) => string | true;
35
37
  range: (minValue: number, maxValue: number, customError?: string) => (value: any) => string | true;
36
38
  integer: (customError?: string) => (value: any) => string | true;
@@ -59,6 +61,7 @@ export declare function useRules(): {
59
61
  requireIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
60
62
  requireTrue: (customError?: string) => (value: any) => string | true;
61
63
  requireTrueIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
64
+ requireNotEmpty: (customError?: string) => (value: any) => string | true;
62
65
  numeric: (customError?: string) => (value: any) => string | true;
63
66
  range: (minValue: number, maxValue: number, customError?: string) => (value: any) => string | true;
64
67
  integer: (customError?: string) => (value: any) => string | true;
@@ -87,6 +90,7 @@ export declare function useRules(): {
87
90
  requireIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
88
91
  requireTrue: (customError?: string) => (value: any) => string | true;
89
92
  requireTrueIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
93
+ requireNotEmpty: (customError?: string) => (value: any) => string | true;
90
94
  numeric: (customError?: string) => (value: any) => string | true;
91
95
  range: (minValue: number, maxValue: number, customError?: string) => (value: any) => string | true;
92
96
  integer: (customError?: string) => (value: any) => string | true;
@@ -7,6 +7,7 @@ export function useRules() {
7
7
  const requireIf = (conditionIf, customError = "This field is required") => (value) => condition(!!value || value === false || value === 0 || !conditionIf, customError);
8
8
  const requireTrue = (customError = "This field must be true") => (value) => condition(!!value, customError);
9
9
  const requireTrueIf = (conditionIf, customError = "This field must be true") => (value) => condition(!!value || !conditionIf, customError);
10
+ const requireNotEmpty = (customError = "This field is required") => (value) => condition(Array.isArray(value) ? value.length > 0 : !!value || value === false || value === 0, customError);
10
11
  const numeric = (customError = "This field must be a number") => (value) => condition(!value || !isNaN(Number(value)), customError);
11
12
  const range = (minValue, maxValue, customError = `Value is out of range (${minValue}-${maxValue})`) => (value) => condition(!value || value >= minValue && value <= maxValue, customError);
12
13
  const integer = (customError = "This field must be an integer") => (value) => condition(!value || isInteger(value) || /^\+?-?\d+$/.test(value), customError);
@@ -35,6 +36,7 @@ export function useRules() {
35
36
  requireIf,
36
37
  requireTrue,
37
38
  requireTrueIf,
39
+ requireNotEmpty,
38
40
  numeric,
39
41
  range,
40
42
  integer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "4.0.10",
3
+ "version": "4.0.12",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,6 +47,8 @@
47
47
  "prepack": "nuxt-module-build build",
48
48
  "dev": "npm run dev:prepare && nuxt dev playground",
49
49
  "dev:build": "nuxt build playground",
50
+ "dev:generate": "nuxt generate playground",
51
+ "playground:scaffold": "node scripts/scaffold-playground-pages.mjs",
50
52
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
51
53
  "docs:api:components": "vue-docgen -c docs/vue-docgen.config.cjs && npm run docs:ai:summary && node scripts/enrich-vue-docs-from-ai.mjs",
52
54
  "docs:api:composables": "typedoc --options docs/typedoc.json",
@@ -86,7 +88,7 @@
86
88
  "@techstark/opencv-js": "4.11.0-release.1",
87
89
  "@thumbmarkjs/thumbmarkjs": "^1.7.4",
88
90
  "@vue/apollo-composable": "^4.2.2",
89
- "@vuepic/vue-datepicker": "^7.4.1",
91
+ "@vuepic/vue-datepicker": "^12.1.0",
90
92
  "@vueuse/integrations": "^14.2.1",
91
93
  "@zxing/browser": "^0.1.5",
92
94
  "cropperjs": "^1.6.2",
@@ -159,6 +159,73 @@ function extractEmits(content) {
159
159
  return emits
160
160
  }
161
161
 
162
+ function extractTemplate(content) {
163
+ const m = content.match(/<template[^>]*>([\s\S]*)<\/template>/i)
164
+ return m ? m[1] : ''
165
+ }
166
+
167
+ function extractSlots(content) {
168
+ const slots = []
169
+ const seen = new Set()
170
+ const template = extractTemplate(content)
171
+ if (!template) return slots
172
+
173
+ const slotRe = /<slot\b([^>]*?)(?:\/?>)/g
174
+ let match
175
+ while ((match = slotRe.exec(template)) !== null) {
176
+ const attrs = match[1] || ''
177
+ const nameMatch = attrs.match(/(?:^|\s)name\s*=\s*"([^"]+)"/)
178
+ || attrs.match(/(?:^|\s)#([A-Za-z_][A-Za-z0-9_-]*)/)
179
+ const name = nameMatch ? nameMatch[1] : 'default'
180
+ if (seen.has(name)) continue
181
+ seen.add(name)
182
+
183
+ const bindings = []
184
+ const bindRe = /:([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*"([^"]*)"/g
185
+ let bm
186
+ while ((bm = bindRe.exec(attrs)) !== null) {
187
+ if (bm[1] === 'name') continue
188
+ bindings.push({ name: bm[1], expression: compact(bm[2]) })
189
+ }
190
+ slots.push({ name, bindings })
191
+ }
192
+ return slots
193
+ }
194
+
195
+ function extractExposed(content) {
196
+ const exposed = []
197
+ const m = content.match(/defineExpose\s*\(\s*\{([\s\S]*?)\}\s*\)/)
198
+ if (!m) return exposed
199
+ const body = m[1]
200
+ // Match top-level identifiers; ignore values, nested braces handled by depth.
201
+ let depth = 0
202
+ let token = ''
203
+ const tokens = []
204
+ for (let i = 0; i < body.length; i++) {
205
+ const ch = body[i]
206
+ if (ch === '{' || ch === '(' || ch === '[') depth++
207
+ else if (ch === '}' || ch === ')' || ch === ']') depth--
208
+ else if (ch === ',' && depth === 0) {
209
+ tokens.push(token); token = ''
210
+ continue
211
+ }
212
+ token += ch
213
+ }
214
+ if (token.trim()) tokens.push(token)
215
+
216
+ for (const raw of tokens) {
217
+ const piece = raw.trim().replace(/^\s*\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '').trim()
218
+ if (!piece) continue
219
+ // `name`, `name: value`, `name(...) {}` — take the first identifier.
220
+ const idMatch = piece.match(/^([A-Za-z_$][A-Za-z0-9_$]*)/)
221
+ if (!idMatch) continue
222
+ const name = idMatch[1]
223
+ if (exposed.find(e => e.name === name)) continue
224
+ exposed.push({ name })
225
+ }
226
+ return exposed
227
+ }
228
+
162
229
  function extractExports(content) {
163
230
  const functions = []
164
231
  const constants = []
@@ -251,11 +318,15 @@ for (const file of componentFiles) {
251
318
  const summary = extractTopDescription(content)
252
319
  const props = extractProps(content)
253
320
  const emits = extractEmits(content)
321
+ const slots = extractSlots(content)
322
+ const exposed = extractExposed(content)
254
323
  const componentEntry = {
255
324
  path: rel,
256
325
  summary,
257
326
  props,
258
327
  emits,
328
+ slots,
329
+ exposed,
259
330
  }
260
331
  docModel.components.push(componentEntry)
261
332
 
@@ -283,6 +354,23 @@ for (const file of componentFiles) {
283
354
  } else {
284
355
  lines.push('- Emits: None detected.')
285
356
  }
357
+ if (slots.length) {
358
+ lines.push(`- Slots (${slots.length}):`)
359
+ for (const s of slots) {
360
+ const bindings = s.bindings.length ? ` bindings: ${s.bindings.map(b => `${b.name}=${b.expression}`).join(', ')}` : ''
361
+ lines.push(` - \`${s.name}\`${bindings}`)
362
+ }
363
+ } else {
364
+ lines.push('- Slots: None detected.')
365
+ }
366
+ if (exposed.length) {
367
+ lines.push(`- Exposed (${exposed.length}):`)
368
+ for (const x of exposed) {
369
+ lines.push(` - \`${x.name}\``)
370
+ }
371
+ } else {
372
+ lines.push('- Exposed: None detected.')
373
+ }
286
374
  lines.push('')
287
375
  }
288
376