@ramathibodi/nuxt-commons 4.0.9 → 4.0.11
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/README.md +8 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/document/TemplateBuilder.d.vue.ts +6 -1
- package/dist/runtime/components/document/TemplateBuilder.vue +21 -41
- package/dist/runtime/components/document/TemplateBuilder.vue.d.ts +6 -1
- package/dist/runtime/components/form/Pad.vue +1 -1
- package/dist/runtime/composables/document/template.js +9 -0
- package/dist/runtime/composables/document/templateInputTypes.d.ts +228 -0
- package/dist/runtime/composables/document/templateInputTypes.js +128 -0
- package/dist/runtime/types/graphqlOperation.d.ts +3 -1
- package/package.json +3 -1
- package/scripts/generate-ai-summary.mjs +88 -0
- package/scripts/scaffold-playground-pages.mjs +207 -0
- package/templates/.codegen/plugin-schema-object.cjs +13 -5
package/README.md
CHANGED
|
@@ -6,6 +6,14 @@ Nuxt module for Nuxt 3 and Nuxt 4 that provides shared runtime building blocks f
|
|
|
6
6
|
- runtime plugins for permission/dialog/default behavior
|
|
7
7
|
- utility and lab exports
|
|
8
8
|
|
|
9
|
+
## Documentation & playground
|
|
10
|
+
|
|
11
|
+
The live playground is the project's documentation site — every component has a real demo page running the real module against the real Vuetify theme, with a Vuetify-docs-style API panel (Props / Emits / Slots / Exposed) sourced from `docs/ai-summary.json`. Bug-repro fixtures live under *Scenarios*.
|
|
12
|
+
|
|
13
|
+
→ **https://ramacare.gitlab.rama.mahidol.ac.th/frontend/rama-modules/** _(published from `master` by the `pages` CI job; if the project later moves to a unique Pages domain, update `NUXT_APP_BASE_URL` in the CI job accordingly)_
|
|
14
|
+
|
|
15
|
+
Run it locally with `pnpm run dev`. See `CLAUDE.md` > *Playground* for the conventions every new/modified component must follow.
|
|
16
|
+
|
|
9
17
|
## Install
|
|
10
18
|
|
|
11
19
|
```bash
|
package/dist/module.json
CHANGED
|
@@ -5,7 +5,12 @@ interface Props {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Public props accepted by DocumentTemplateBuilder.
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* To extend the Input Type dropdown with app-specific entries, call
|
|
10
|
+
* `registerDocumentTemplateInputType()` from a Nuxt plugin (app-wide) or
|
|
11
|
+
* `useDocumentTemplateInputType()` inside `<script setup>` of a page
|
|
12
|
+
* (page-scoped, auto-cleanup on unmount). Both flow through the same
|
|
13
|
+
* runtime registry that this component reads via `useDocumentTemplateInputTypes()`.
|
|
9
14
|
*/
|
|
10
15
|
type __VLS_Props = Props;
|
|
11
16
|
type __VLS_ModelProps = {
|
|
@@ -3,6 +3,9 @@ import { computed, ref, watch } from "vue";
|
|
|
3
3
|
import * as prettier from "prettier";
|
|
4
4
|
import prettierPluginHtml from "prettier/plugins/html";
|
|
5
5
|
import { useDocumentTemplate, validationRulesRegex, optionStringToChoiceObject } from "../../composables/document/template";
|
|
6
|
+
import {
|
|
7
|
+
useDocumentTemplateInputTypes
|
|
8
|
+
} from "../../composables/document/templateInputTypes";
|
|
6
9
|
import { autoActionHeader, templateToHeader } from "../../composables/document/templateFormTable";
|
|
7
10
|
import { cloneDeep } from "lodash-es";
|
|
8
11
|
import VueJsonPretty from "vue-json-pretty";
|
|
@@ -113,47 +116,24 @@ async function convertToAdvanceMode() {
|
|
|
113
116
|
modelValue.value = await prettier.format(useDocumentTemplate(templateItems.value).replaceAll(">", ">\n"), { parser: "html", plugins: [prettierPluginHtml] });
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
{ label: "Table Data", value: "FormTableData" },
|
|
135
|
-
{ label: "[Decoration] Header", value: "Header" },
|
|
136
|
-
{ label: "[Decoration] Separator", value: "Separator" },
|
|
137
|
-
{ label: "[Advanced] Hidden Field", value: "FormHidden" },
|
|
138
|
-
{ label: "[Advanced] Inherit Form", value: "DocumentForm" },
|
|
139
|
-
{ label: "[Advanced] Custom Code", value: "CustomCode" }
|
|
140
|
-
]);
|
|
141
|
-
const requireOption = ref(["VSelect", "VAutocomplete", "VCombobox", "VRadio", "VRadioInline", "MasterAutocomplete", "FormTable", "FormTableData", "DocumentForm", "FormCheckboxGroup"]);
|
|
142
|
-
const notRequireVariable = ref(["Header", "Separator", "CustomCode", "DocumentForm"]);
|
|
143
|
-
const notRequireLabel = ref(["Separator", "CustomCode", "FormFile", "FormHidden", "DocumentForm"]);
|
|
144
|
-
const notRequireWidth = ref(["Separator", "FormHidden", "DocumentForm"]);
|
|
145
|
-
const notRequireOptions = ref(["Separator", "CustomCode", "FormFile"]);
|
|
146
|
-
const notRequireRules = ref(["Separator", "Header", "CustomCode", "FormHidden", "DocumentForm"]);
|
|
147
|
-
const notRequireInputAttributes = ref(["CustomCode", "Header", "DocumentForm"]);
|
|
148
|
-
const notRequireColumnAttributes = ref(["Separator", "FormHidden", "DocumentForm"]);
|
|
149
|
-
const notRequireAdvancedSetting = ref(["Separator", "CustomCode"]);
|
|
150
|
-
const notRequireClassAndStyle = ref(["FormHidden"]);
|
|
151
|
-
const hasSpecificOption = ref(["FormHidden", "FormTable", "FormTableData"]);
|
|
152
|
-
const choiceOption = ref(["VRadio", "VRadioInline", "VSelect", "VAutocomplete", "VCombobox", "FormCheckboxGroup"]);
|
|
153
|
-
const inputOptionsLabel = ref({
|
|
154
|
-
"MasterAutocomplete": "Group Key",
|
|
155
|
-
"Header": "Class",
|
|
156
|
-
"DocumentForm": "Template Code"
|
|
119
|
+
const inputTypes = computed(() => useDocumentTemplateInputTypes());
|
|
120
|
+
const inputTypeChoice = computed(() => inputTypes.value.map((t) => ({ label: t.label, value: t.value })));
|
|
121
|
+
const requireOption = computed(() => inputTypes.value.filter((t) => t.requiresOptions).map((t) => t.value));
|
|
122
|
+
const notRequireVariable = computed(() => inputTypes.value.filter((t) => t.needsVariableName === false).map((t) => t.value));
|
|
123
|
+
const notRequireLabel = computed(() => inputTypes.value.filter((t) => t.needsLabel === false).map((t) => t.value));
|
|
124
|
+
const notRequireWidth = computed(() => inputTypes.value.filter((t) => t.needsWidth === false).map((t) => t.value));
|
|
125
|
+
const notRequireOptions = computed(() => inputTypes.value.filter((t) => t.showsOptions === false).map((t) => t.value));
|
|
126
|
+
const notRequireRules = computed(() => inputTypes.value.filter((t) => t.hidesValidationRules).map((t) => t.value));
|
|
127
|
+
const notRequireInputAttributes = computed(() => inputTypes.value.filter((t) => t.hidesInputAttributes).map((t) => t.value));
|
|
128
|
+
const notRequireColumnAttributes = computed(() => inputTypes.value.filter((t) => t.hidesColumnAttributes).map((t) => t.value));
|
|
129
|
+
const notRequireAdvancedSetting = computed(() => inputTypes.value.filter((t) => t.hidesAdvancedSettings).map((t) => t.value));
|
|
130
|
+
const notRequireClassAndStyle = computed(() => inputTypes.value.filter((t) => t.hidesClassAndStyle).map((t) => t.value));
|
|
131
|
+
const hasSpecificOption = computed(() => inputTypes.value.filter((t) => t.hasSpecificOptionEditor).map((t) => t.value));
|
|
132
|
+
const choiceOption = computed(() => inputTypes.value.filter((t) => t.optionsAsChoice).map((t) => t.value));
|
|
133
|
+
const inputOptionsLabel = computed(() => {
|
|
134
|
+
const acc = {};
|
|
135
|
+
for (const t of inputTypes.value) if (t.optionsLabel) acc[t.value] = t.optionsLabel;
|
|
136
|
+
return acc;
|
|
157
137
|
});
|
|
158
138
|
const ruleOptions = (inputType) => (value) => {
|
|
159
139
|
if (choiceOption.value.includes(inputType) && !/^[^'",]+(,[^'",]+)*$/.test(value)) return "Invalid options format";
|
|
@@ -5,7 +5,12 @@ interface Props {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Public props accepted by DocumentTemplateBuilder.
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* To extend the Input Type dropdown with app-specific entries, call
|
|
10
|
+
* `registerDocumentTemplateInputType()` from a Nuxt plugin (app-wide) or
|
|
11
|
+
* `useDocumentTemplateInputType()` inside `<script setup>` of a page
|
|
12
|
+
* (page-scoped, auto-cleanup on unmount). Both flow through the same
|
|
13
|
+
* runtime registry that this component reads via `useDocumentTemplateInputTypes()`.
|
|
9
14
|
*/
|
|
10
15
|
type __VLS_Props = Props;
|
|
11
16
|
type __VLS_ModelProps = {
|
|
@@ -308,5 +308,5 @@ defineExpose({
|
|
|
308
308
|
</template>
|
|
309
309
|
|
|
310
310
|
<style>
|
|
311
|
-
.form-data-dirty:not(.v-input--error) :not(.v-chip):not(.v-chip *){color:color-mix(in srgb,currentColor 70%,rgb(var(--v-theme-primary)))!important;text-shadow:0 0 .02em currentColor}
|
|
311
|
+
.form-data-dirty:not(.v-input--error) :not(.v-chip):not(.v-chip *):not(.v-toolbar):not(.v-toolbar *){color:color-mix(in srgb,currentColor 70%,rgb(var(--v-theme-primary)))!important;text-shadow:0 0 .02em currentColor}
|
|
312
312
|
</style>
|
|
@@ -3,6 +3,7 @@ import { processTemplateFormTableData } from "./templateFormTableData.js";
|
|
|
3
3
|
import { processTemplateFormHidden } from "./templateFormHidden.js";
|
|
4
4
|
import { some, includes, cloneDeep } from "lodash-es";
|
|
5
5
|
import { migrateInputAttributes, migrateTemplateString } from "./templateMigrate.js";
|
|
6
|
+
import { getDocumentTemplateInputTypeEntry } from "./templateInputTypes.js";
|
|
6
7
|
export const validationRulesRegex = /^(require(?:\([^)]*\))?|requireIf\([^)]*\)|requireTrue(?:\([^)]*\))?|requireTrueIf\([^)]*\)|numeric(?:\([^)]*\))?|range\([^)]*\)|integer(?:\([^)]*\))?|unique\([^)]*\)|length(?:\([^)]*\))?|lengthGreater\([^)]*\)|lengthLess\([^)]*\)|telephone(?:\([^)]*\))?|email(?:\([^)]*\))?|regex\([^)]*\)|idcard(?:\([^)]*\))?|DateFuture(?:\([^)]*\))?|DatetimeFuture(?:\([^)]*\))?|DateHappen(?:\([^)]*\))?|DatetimeHappen(?:\([^)]*\))?|DateAfter\([^)]*\)|DateBefore\([^)]*\)|DateEqual\([^)]*\))(,(require(?:\([^)]*\))?|requireIf\([^)]*\)|requireTrue(?:\([^)]*\))?|requireTrueIf\([^)]*\)|numeric(?:\([^)]*\))?|range\([^)]*\)|integer(?:\([^)]*\))?|unique\([^)]*\)|length(?:\([^)]*\))?|lengthGreater\([^)]*\)|lengthLess\([^)]*\)|telephone(?:\([^)]*\))?|email(?:\([^)]*\))?|regex\([^)]*\)|idcard(?:\([^)]*\))?|DateFuture(?:\([^)]*\))?|DatetimeFuture(?:\([^)]*\))?|DateHappen(?:\([^)]*\))?|DatetimeHappen(?:\([^)]*\))?|DateAfter\([^)]*\)|DateBefore\([^)]*\)|DateEqual\([^)]*\)))*$/;
|
|
7
8
|
export function useDocumentTemplate(items, parentTemplates) {
|
|
8
9
|
if (!items) return "";
|
|
@@ -52,6 +53,14 @@ export function templateItemToString(inputItem, parentTemplates, dataVariable =
|
|
|
52
53
|
}
|
|
53
54
|
let templateString;
|
|
54
55
|
const validationRules = item.validationRules ? buildValidationRules(item.validationRules) || "" : "";
|
|
56
|
+
const registryEntry = getDocumentTemplateInputTypeEntry(item.inputType);
|
|
57
|
+
if (registryEntry && typeof registryEntry.render === "function") {
|
|
58
|
+
templateString = registryEntry.render(item, parentTemplates, dataVariable);
|
|
59
|
+
if (item.computedValue && item.variableName) {
|
|
60
|
+
templateString = `${templateString || ""}<FormHidden v-model="${dataVariable}.${item.variableName}" :item-value="data" :hook="()=>${item.computedValue}"/>`.trim();
|
|
61
|
+
}
|
|
62
|
+
return templateString || "";
|
|
63
|
+
}
|
|
55
64
|
switch (item.inputType) {
|
|
56
65
|
case "CustomCode":
|
|
57
66
|
templateString = item.inputCustomCode || "";
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface graphqlVariable {
|
|
2
2
|
name: string
|
|
3
|
-
|
|
3
|
+
// `true` for `[T]`, `[true]` for `[T!]` — matches gql-query-builder's
|
|
4
|
+
// VariableOptions shape so element non-null survives into the query string.
|
|
5
|
+
list?: boolean | [boolean]
|
|
4
6
|
required?: boolean
|
|
5
7
|
type?: string
|
|
6
8
|
value?: any
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramathibodi/nuxt-commons",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.11",
|
|
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",
|
|
@@ -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
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scaffold per-component playground pages.
|
|
4
|
+
*
|
|
5
|
+
* Reads docs/ai-summary.json and, for every component under
|
|
6
|
+
* src/runtime/components/, emits:
|
|
7
|
+
* - playground/pages/components/<slug>.vue (stub demo + API panel)
|
|
8
|
+
* - playground/playgrounds.generated.ts (component registry array)
|
|
9
|
+
*
|
|
10
|
+
* Idempotency: pages start with a "SCAFFOLD_MARKER" comment. The script
|
|
11
|
+
* overwrites only files that still carry the marker — hand-edited pages
|
|
12
|
+
* (marker removed) are left alone.
|
|
13
|
+
*
|
|
14
|
+
* Run with: pnpm exec node scripts/scaffold-playground-pages.mjs
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
|
|
19
|
+
const rootDir = process.cwd()
|
|
20
|
+
const aiSummary = JSON.parse(fs.readFileSync(path.join(rootDir, 'docs/ai-summary.json'), 'utf8'))
|
|
21
|
+
const pagesDir = path.join(rootDir, 'playground/pages/components')
|
|
22
|
+
const registryFile = path.join(rootDir, 'playground/playgrounds.generated.ts')
|
|
23
|
+
|
|
24
|
+
const SCAFFOLD_MARKER = '<!-- SCAFFOLD: auto-generated by scripts/scaffold-playground-pages.mjs. Delete this line to opt out of overwrites. -->'
|
|
25
|
+
|
|
26
|
+
function pascal(segment) {
|
|
27
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function deriveNames(componentPath) {
|
|
31
|
+
// componentPath like "src/runtime/components/form/images/Pad.vue"
|
|
32
|
+
const rel = componentPath.replace(/^src\/runtime\/components\//, '').replace(/\.vue$/, '')
|
|
33
|
+
const segments = rel.split('/')
|
|
34
|
+
const componentName = segments.map(pascal).join('')
|
|
35
|
+
const slug = segments.map(s => s.toLowerCase()).join('-')
|
|
36
|
+
const title = componentName
|
|
37
|
+
// First path segment is the group; root-level components fall into "general".
|
|
38
|
+
const group = segments.length > 1 ? segments[0].toLowerCase() : 'general'
|
|
39
|
+
return { componentName, slug, title, group }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderPage({ componentName, title, summary, sourcePath }) {
|
|
43
|
+
const props = inferStubProps(componentName)
|
|
44
|
+
const propsAttr = props ? ' ' + props : ''
|
|
45
|
+
const setup = setupForComponent[componentName] ?? ''
|
|
46
|
+
return `<script setup lang="ts">
|
|
47
|
+
// ${SCAFFOLD_MARKER.replace(/^<!--\s*|\s*-->$/g, '')}
|
|
48
|
+
//
|
|
49
|
+
// To enrich this page (richer demo, multiple variants, controls, etc.),
|
|
50
|
+
// remove the SCAFFOLD comment in the <template> below — the scaffold
|
|
51
|
+
// script will then leave this file alone on future runs.
|
|
52
|
+
${setup}</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
${SCAFFOLD_MARKER}
|
|
56
|
+
<PlaygroundPage
|
|
57
|
+
title="${title}"
|
|
58
|
+
summary="${escapeAttr(summary)}"
|
|
59
|
+
source-path="${sourcePath}"
|
|
60
|
+
>
|
|
61
|
+
<div class="text-caption text-medium-emphasis mb-3">
|
|
62
|
+
Auto-scaffolded demo — mounts the component with minimal props.
|
|
63
|
+
Replace this block (and delete the SCAFFOLD comment) when you wire
|
|
64
|
+
up a realistic interactive demo.
|
|
65
|
+
</div>
|
|
66
|
+
<${componentName}${propsAttr} />
|
|
67
|
+
</PlaygroundPage>
|
|
68
|
+
</template>
|
|
69
|
+
`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function escapeAttr(s) {
|
|
73
|
+
return (s || '').replace(/"/g, '"').replace(/\n/g, ' ')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Hand-tuned minimal props for components that throw without them.
|
|
78
|
+
* Keep tiny — anything richer belongs in a hand-enriched page.
|
|
79
|
+
*/
|
|
80
|
+
const stubProps = {
|
|
81
|
+
FormTable: 'title="Demo table"',
|
|
82
|
+
FormTableData: 'title="Demo table"',
|
|
83
|
+
FormActionPad: 'title="Demo"',
|
|
84
|
+
FormEditPad: 'title="Demo"',
|
|
85
|
+
FormSystem: 'name="demoForm"',
|
|
86
|
+
FormDialog: ':model-value="false" title="Demo"',
|
|
87
|
+
FormImagesCapture: 'title="Demo"',
|
|
88
|
+
FormImagesEdit: 'title="Demo"',
|
|
89
|
+
FormImagesField: 'title="Demo"',
|
|
90
|
+
FormImagesPad: 'title="Demo"',
|
|
91
|
+
DialogConfirm: ':model-value="false"',
|
|
92
|
+
DialogLoading: ':model-value="false"',
|
|
93
|
+
DialogIndex: ':model-value="false" title="Demo"',
|
|
94
|
+
DialogHost: '',
|
|
95
|
+
DialogDefaultConfirm: ':model-value="false"',
|
|
96
|
+
DialogDefaultLoading: ':model-value="false"',
|
|
97
|
+
DialogDefaultNotify: ':model-value="false"',
|
|
98
|
+
DialogDefaultPrinting: ':model-value="false"',
|
|
99
|
+
DialogDefaultVerifyUser: ':model-value="false"',
|
|
100
|
+
DocumentForm: 'documentName="demo"',
|
|
101
|
+
DocumentTemplateBuilder: 'title="Template Builder"',
|
|
102
|
+
ModelTable: 'modelName="MasterItem" :fields="[\'*\']"',
|
|
103
|
+
ModelPad: 'modelName="MasterItem"',
|
|
104
|
+
ModelIterator: 'modelName="MasterItem" :fields="[\'*\']"',
|
|
105
|
+
ModelLabel: 'modelName="MasterItem"',
|
|
106
|
+
ModelSelect: 'modelName="MasterItem"',
|
|
107
|
+
ModelAutocomplete: 'modelName="MasterItem"',
|
|
108
|
+
ModelCombobox: 'modelName="MasterItem"',
|
|
109
|
+
MasterLabel: 'groupKey="DEMO"',
|
|
110
|
+
MasterSelect: 'groupKey="DEMO"',
|
|
111
|
+
MasterAutocomplete: 'groupKey="DEMO"',
|
|
112
|
+
MasterCombobox: 'groupKey="DEMO"',
|
|
113
|
+
MasterRadioGroup: 'groupKey="DEMO"',
|
|
114
|
+
LabelDate: 'model-value="2026-05-18"',
|
|
115
|
+
LabelDateAgo: ':model-value="DateTime.now()"',
|
|
116
|
+
LabelDateCount: ':model-value="DateTime.now()"',
|
|
117
|
+
LabelField: 'label="Demo" model-value="value"',
|
|
118
|
+
LabelFormatMoney: ':model-value="1234.5"',
|
|
119
|
+
LabelMask: 'mask="###-###-####" model-value="1234567890"',
|
|
120
|
+
LabelObject: ':model-value="{ key: \'value\' }"',
|
|
121
|
+
PdfView: '',
|
|
122
|
+
PdfPrint: '',
|
|
123
|
+
SplitterPanel: '',
|
|
124
|
+
TabsGroup: ':items="[{ label: \'Tab 1\' }, { label: \'Tab 2\' }]"',
|
|
125
|
+
ImportCSV: '',
|
|
126
|
+
ExportCSV: ':model-value="[]" file-name="demo"',
|
|
127
|
+
FileBtn: 'label="Upload"',
|
|
128
|
+
BarcodeReader: '',
|
|
129
|
+
MrzReader: '',
|
|
130
|
+
TextBarcode: ':model-value="\'ABC123\'"',
|
|
131
|
+
Alert: '',
|
|
132
|
+
DeviceIdCardButton: '',
|
|
133
|
+
DeviceIdCardWebSocket: '',
|
|
134
|
+
DeviceScanner: '',
|
|
135
|
+
FormLogin: '',
|
|
136
|
+
FormPad: '',
|
|
137
|
+
FormBirthdate: '',
|
|
138
|
+
FormCheckboxGroup: ':items="[{ title: \'One\', value: 1 }, { title: \'Two\', value: 2 }]"',
|
|
139
|
+
FormCodeEditor: 'language="javascript"',
|
|
140
|
+
FormDate: '',
|
|
141
|
+
FormDateTime: '',
|
|
142
|
+
FormTime: '',
|
|
143
|
+
FormFile: '',
|
|
144
|
+
FormHidden: '',
|
|
145
|
+
FormIterator: '',
|
|
146
|
+
FormSignPad: '',
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function inferStubProps(componentName) {
|
|
150
|
+
return stubProps[componentName] ?? ''
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Per-component <script setup> snippets injected after the SCAFFOLD comment. */
|
|
154
|
+
const setupForComponent = {
|
|
155
|
+
LabelDateAgo: `import { DateTime } from 'luxon'\n`,
|
|
156
|
+
LabelDateCount: `import { DateTime } from 'luxon'\n`,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function shouldOverwrite(filePath) {
|
|
160
|
+
if (!fs.existsSync(filePath)) return true
|
|
161
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
162
|
+
return content.includes(SCAFFOLD_MARKER)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const entries = []
|
|
166
|
+
let written = 0
|
|
167
|
+
let skipped = 0
|
|
168
|
+
|
|
169
|
+
for (const component of aiSummary.components) {
|
|
170
|
+
const { componentName, slug, title, group } = deriveNames(component.path)
|
|
171
|
+
const summary = (component.summary || '').replace(/"/g, '\'')
|
|
172
|
+
const sourcePath = component.path
|
|
173
|
+
|
|
174
|
+
const subdir = slug.includes('-') ? '' : '' // flat layout under components/
|
|
175
|
+
const fileName = `${slug}.vue`
|
|
176
|
+
const filePath = path.join(pagesDir, subdir, fileName)
|
|
177
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
178
|
+
|
|
179
|
+
if (shouldOverwrite(filePath)) {
|
|
180
|
+
fs.writeFileSync(filePath, renderPage({ componentName, title, summary, sourcePath }))
|
|
181
|
+
written++
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
skipped++
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
entries.push({
|
|
188
|
+
slug,
|
|
189
|
+
title,
|
|
190
|
+
summary,
|
|
191
|
+
to: `/components/${slug}`,
|
|
192
|
+
group,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
entries.sort((a, b) => a.slug.localeCompare(b.slug))
|
|
197
|
+
|
|
198
|
+
const registrySource = `// Auto-generated by scripts/scaffold-playground-pages.mjs.
|
|
199
|
+
// Do not edit by hand — re-run the script after adding components.
|
|
200
|
+
import type { PlaygroundEntry } from './playgrounds'
|
|
201
|
+
|
|
202
|
+
export const componentEntries: PlaygroundEntry[] = ${JSON.stringify(entries, null, 2)}
|
|
203
|
+
`
|
|
204
|
+
fs.writeFileSync(registryFile, registrySource)
|
|
205
|
+
|
|
206
|
+
console.log(`Pages scaffolded: ${written} written, ${skipped} preserved (hand-edited).`)
|
|
207
|
+
console.log(`Registry: ${path.relative(rootDir, registryFile)} (${entries.length} entries)`)
|
|
@@ -6,14 +6,21 @@ function capitalizeFirstLetter(string) {
|
|
|
6
6
|
return string.charAt(0).toUpperCase() + string.slice(1)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// `inList` flips to true the moment we descend into a ListType, so a NonNullType
|
|
10
|
+
// nested under a List is recognized as element-non-null (`[T!]`) rather than
|
|
11
|
+
// overwriting the outer list's `required` flag. The element-non-null is encoded
|
|
12
|
+
// as `list: [true]` — the shape gql-query-builder reads to emit `[T!]`.
|
|
13
|
+
function inputTypeToObject(inputType, result, inList = false) {
|
|
10
14
|
if (inputType.kind === 'NonNullType') {
|
|
11
|
-
result['
|
|
12
|
-
|
|
15
|
+
if (inList) result['list'] = [true]
|
|
16
|
+
else result['required'] = true
|
|
17
|
+
inputTypeToObject(inputType.type, result, inList)
|
|
18
|
+
return
|
|
13
19
|
}
|
|
14
20
|
if (inputType.kind === 'ListType') {
|
|
15
|
-
result['list'] = true
|
|
16
|
-
inputTypeToObject(inputType.type, result)
|
|
21
|
+
if (!Array.isArray(result['list'])) result['list'] = true
|
|
22
|
+
inputTypeToObject(inputType.type, result, true)
|
|
23
|
+
return
|
|
17
24
|
}
|
|
18
25
|
if (inputType.kind === 'NamedType') {
|
|
19
26
|
result['type'] = inputType.name.value
|
|
@@ -21,6 +28,7 @@ function inputTypeToObject(inputType, result) {
|
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
module.exports = {
|
|
31
|
+
inputTypeToObject,
|
|
24
32
|
plugin(schema, _documents, _config) {
|
|
25
33
|
const astNode = getCachedDocumentNodeFromSchema(schema) // Transforms the GraphQLSchema into ASTNode
|
|
26
34
|
|