@vc-shell/vc-app-skill 2.0.0-alpha.23 → 2.0.0-alpha.24
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/CHANGELOG.md +8 -0
- package/bin/knowledge-stats.cjs +135 -0
- package/bin/sync-docs.cjs +62 -0
- package/package.json +5 -1
- package/runtime/VERSION +1 -1
- package/runtime/agents/details-blade-generator.md +75 -14
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/api/platform.docs.md +14 -7
- package/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useApiClient/useApiClient.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useAssetsManager/useAssetsManager.docs.md +149 -0
- package/runtime/knowledge/docs/core/composables/useAsync/useAsync.docs.md +7 -0
- package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +7 -0
- package/runtime/knowledge/docs/core/composables/useDashboard/useDashboard.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useMenuService/useMenuService.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/usePermissions/usePermissions.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/global-error-handler/global-error-handler.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/i18n/i18n.docs.md +74 -0
- package/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/validation/validation.docs.md +6 -0
- package/runtime/knowledge/docs/modules/assets-manager/assets-manager.docs.md +29 -33
- package/runtime/knowledge/docs/shell/components/logout-button/logout-button.docs.md +3 -3
- package/runtime/knowledge/docs/ui/components/atoms/vc-button/vc-button.docs.md +11 -0
- package/runtime/knowledge/docs/ui/components/atoms/vc-card/vc-card.docs.md +11 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +11 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-table/vc-data-table.docs.md +12 -0
- package/runtime/knowledge/index.md +60 -0
- package/runtime/knowledge/patterns/assets-management.md +213 -0
- package/runtime/knowledge/patterns/child-blade-flow.md +277 -0
- package/runtime/knowledge/patterns/details-blade-pattern.md +350 -3
- package/runtime/knowledge/patterns/extension-points-usage.md +308 -0
- package/runtime/knowledge/patterns/form-validation.md +377 -0
- package/runtime/knowledge/patterns/multilanguage-fields.md +239 -0
- package/runtime/knowledge/patterns/signalr-notifications.md +237 -0
- package/runtime/vc-app.md +44 -5
- package/runtime/knowledge/docs/shell/components/app-switcher/app-switcher.docs.md +0 -104
|
@@ -115,7 +115,7 @@ A details blade is a form-based panel opened from a list blade (or navigation) t
|
|
|
115
115
|
:label="$t('MODULE.FIELDS.IS_ACTIVE.LABEL')"
|
|
116
116
|
/>
|
|
117
117
|
|
|
118
|
-
<!-- Date/DateTime field: VcInput type="datetime-local" -->
|
|
118
|
+
<!-- Date/DateTime field: VcDatePicker (preferred over VcInput type="datetime-local") -->
|
|
119
119
|
<Field
|
|
120
120
|
v-slot="{ errorMessage, handleChange, errors }"
|
|
121
121
|
:label="$t('MODULE.FIELDS.CREATED_DATE.LABEL')"
|
|
@@ -123,10 +123,9 @@ A details blade is a form-based panel opened from a list blade (or navigation) t
|
|
|
123
123
|
name="createdDate"
|
|
124
124
|
rules="required"
|
|
125
125
|
>
|
|
126
|
-
<
|
|
126
|
+
<VcDatePicker
|
|
127
127
|
v-model="entity.createdDate"
|
|
128
128
|
class="tw-p-3"
|
|
129
|
-
type="datetime-local"
|
|
130
129
|
:label="$t('MODULE.FIELDS.CREATED_DATE.LABEL')"
|
|
131
130
|
required
|
|
132
131
|
:error="!!errors.length"
|
|
@@ -134,6 +133,143 @@ A details blade is a form-based panel opened from a list blade (or navigation) t
|
|
|
134
133
|
@update:model-value="handleChange"
|
|
135
134
|
/>
|
|
136
135
|
</Field>
|
|
136
|
+
|
|
137
|
+
<!-- Textarea field: for long text (description, notes, comments) -->
|
|
138
|
+
<Field
|
|
139
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
140
|
+
:label="$t('MODULE.FIELDS.DESCRIPTION.LABEL')"
|
|
141
|
+
:model-value="entity.description"
|
|
142
|
+
name="description"
|
|
143
|
+
>
|
|
144
|
+
<VcTextarea
|
|
145
|
+
v-model="entity.description"
|
|
146
|
+
class="tw-p-3"
|
|
147
|
+
:label="$t('MODULE.FIELDS.DESCRIPTION.LABEL')"
|
|
148
|
+
:placeholder="$t('MODULE.FIELDS.DESCRIPTION.PLACEHOLDER')"
|
|
149
|
+
:error="!!errors.length"
|
|
150
|
+
:error-message="errorMessage"
|
|
151
|
+
@update:model-value="handleChange"
|
|
152
|
+
/>
|
|
153
|
+
</Field>
|
|
154
|
+
|
|
155
|
+
<!-- Rich text / HTML field: VcEditor (WYSIWYG) -->
|
|
156
|
+
<Field
|
|
157
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
158
|
+
:label="$t('MODULE.FIELDS.BODY.LABEL')"
|
|
159
|
+
:model-value="entity.body"
|
|
160
|
+
name="body"
|
|
161
|
+
>
|
|
162
|
+
<VcEditor
|
|
163
|
+
v-model="entity.body"
|
|
164
|
+
class="tw-p-3"
|
|
165
|
+
:label="$t('MODULE.FIELDS.BODY.LABEL')"
|
|
166
|
+
:error="!!errors.length"
|
|
167
|
+
:error-message="errorMessage"
|
|
168
|
+
@update:model-value="handleChange"
|
|
169
|
+
/>
|
|
170
|
+
</Field>
|
|
171
|
+
|
|
172
|
+
<!-- Currency field: VcInputCurrency (formatted monetary input) -->
|
|
173
|
+
<Field
|
|
174
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
175
|
+
:label="$t('MODULE.FIELDS.PRICE.LABEL')"
|
|
176
|
+
:model-value="entity.price"
|
|
177
|
+
name="price"
|
|
178
|
+
rules="required|min_value:0"
|
|
179
|
+
>
|
|
180
|
+
<VcInputCurrency
|
|
181
|
+
v-model="entity.price"
|
|
182
|
+
class="tw-p-3"
|
|
183
|
+
:label="$t('MODULE.FIELDS.PRICE.LABEL')"
|
|
184
|
+
currency="USD"
|
|
185
|
+
required
|
|
186
|
+
:error="!!errors.length"
|
|
187
|
+
:error-message="errorMessage"
|
|
188
|
+
@update:model-value="handleChange"
|
|
189
|
+
/>
|
|
190
|
+
</Field>
|
|
191
|
+
|
|
192
|
+
<!-- Multi-select / Tags field: VcMultivalue -->
|
|
193
|
+
<VcMultivalue
|
|
194
|
+
v-model="entity.tags"
|
|
195
|
+
class="tw-p-3"
|
|
196
|
+
:label="$t('MODULE.FIELDS.TAGS.LABEL')"
|
|
197
|
+
:placeholder="$t('MODULE.FIELDS.TAGS.PLACEHOLDER')"
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<!-- Checkbox field: alternative to VcSwitch for accept/agree semantics -->
|
|
201
|
+
<VcCheckbox
|
|
202
|
+
v-model="entity.agreeTerms"
|
|
203
|
+
class="tw-p-3"
|
|
204
|
+
>
|
|
205
|
+
{{ $t('MODULE.FIELDS.AGREE_TERMS.LABEL') }}
|
|
206
|
+
</VcCheckbox>
|
|
207
|
+
|
|
208
|
+
<!-- Radio group: for enum fields with 2-5 options -->
|
|
209
|
+
<VcRadioGroup
|
|
210
|
+
v-model="entity.priority"
|
|
211
|
+
class="tw-p-3"
|
|
212
|
+
:label="$t('MODULE.FIELDS.PRIORITY.LABEL')"
|
|
213
|
+
:options="priorityOptions"
|
|
214
|
+
option-value="id"
|
|
215
|
+
option-label="name"
|
|
216
|
+
/>
|
|
217
|
+
|
|
218
|
+
<!-- Rating field: VcRating -->
|
|
219
|
+
<VcRating
|
|
220
|
+
v-model="entity.rating"
|
|
221
|
+
class="tw-p-3"
|
|
222
|
+
:label="$t('MODULE.FIELDS.RATING.LABEL')"
|
|
223
|
+
/>
|
|
224
|
+
|
|
225
|
+
<!-- Slider field: VcSlider (numeric range) -->
|
|
226
|
+
<VcSlider
|
|
227
|
+
v-model="entity.discount"
|
|
228
|
+
class="tw-p-3"
|
|
229
|
+
:label="$t('MODULE.FIELDS.DISCOUNT.LABEL')"
|
|
230
|
+
:min="0"
|
|
231
|
+
:max="100"
|
|
232
|
+
/>
|
|
233
|
+
|
|
234
|
+
<!-- Color field: VcColorInput -->
|
|
235
|
+
<VcColorInput
|
|
236
|
+
v-model="entity.brandColor"
|
|
237
|
+
class="tw-p-3"
|
|
238
|
+
:label="$t('MODULE.FIELDS.BRAND_COLOR.LABEL')"
|
|
239
|
+
/>
|
|
240
|
+
|
|
241
|
+
<!-- Image upload field: VcImageUpload -->
|
|
242
|
+
<VcImageUpload
|
|
243
|
+
v-model="entity.avatar"
|
|
244
|
+
class="tw-p-3"
|
|
245
|
+
:label="$t('MODULE.FIELDS.AVATAR.LABEL')"
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
<!-- File upload field: VcFileUpload -->
|
|
249
|
+
<VcFileUpload
|
|
250
|
+
v-model="entity.attachment"
|
|
251
|
+
class="tw-p-3"
|
|
252
|
+
:label="$t('MODULE.FIELDS.ATTACHMENT.LABEL')"
|
|
253
|
+
:accept="'.pdf,.doc,.docx'"
|
|
254
|
+
/>
|
|
255
|
+
</VcCol>
|
|
256
|
+
</VcRow>
|
|
257
|
+
|
|
258
|
+
<!-- Form sections with VcCard for visual grouping -->
|
|
259
|
+
<VcRow>
|
|
260
|
+
<VcCol>
|
|
261
|
+
<VcCard :header="$t('MODULE.SECTIONS.ADVANCED.TITLE')">
|
|
262
|
+
<!-- Group related fields inside a card -->
|
|
263
|
+
</VcCard>
|
|
264
|
+
</VcCol>
|
|
265
|
+
</VcRow>
|
|
266
|
+
|
|
267
|
+
<!-- Collapsible sections with VcAccordion -->
|
|
268
|
+
<VcRow>
|
|
269
|
+
<VcCol>
|
|
270
|
+
<VcAccordion :header="$t('MODULE.SECTIONS.SETTINGS.TITLE')">
|
|
271
|
+
<!-- Fields that are less frequently edited -->
|
|
272
|
+
</VcAccordion>
|
|
137
273
|
</VcCol>
|
|
138
274
|
</VcRow>
|
|
139
275
|
|
|
@@ -367,6 +503,163 @@ The `VcDataTable` here is read-only — no toolbar, no selection, no pagination
|
|
|
367
503
|
|
|
368
504
|
---
|
|
369
505
|
|
|
506
|
+
## Read-Only Details Pattern (VcField)
|
|
507
|
+
|
|
508
|
+
For details blades that display data without editing (e.g., order details, transaction history), use `VcField` instead of `VcInput`. This is a common pattern for view-only pages.
|
|
509
|
+
|
|
510
|
+
Source: `apps/vendor-portal/src/modules/orders/pages/order-details.vue`
|
|
511
|
+
|
|
512
|
+
```vue
|
|
513
|
+
<VcCard :header="$t('MODULE.SECTIONS.INFO.TITLE')">
|
|
514
|
+
<div class="tw-p-4 tw-space-y-4">
|
|
515
|
+
<VcField
|
|
516
|
+
:label="$t('MODULE.FIELDS.ORDER_REF.LABEL')"
|
|
517
|
+
:model-value="entity.number || entity.id"
|
|
518
|
+
orientation="horizontal"
|
|
519
|
+
:aspect-ratio="[1, 2]"
|
|
520
|
+
copyable
|
|
521
|
+
type="text"
|
|
522
|
+
/>
|
|
523
|
+
<VcField
|
|
524
|
+
:label="$t('MODULE.FIELDS.STATUS.LABEL')"
|
|
525
|
+
:model-value="entity.status"
|
|
526
|
+
orientation="horizontal"
|
|
527
|
+
:aspect-ratio="[1, 2]"
|
|
528
|
+
type="text"
|
|
529
|
+
:tooltip="$t('MODULE.FIELDS.STATUS.TOOLTIP')"
|
|
530
|
+
/>
|
|
531
|
+
<VcField
|
|
532
|
+
:label="$t('MODULE.FIELDS.TOTAL.LABEL')"
|
|
533
|
+
:model-value="withCurrency(entity.total)"
|
|
534
|
+
orientation="horizontal"
|
|
535
|
+
:aspect-ratio="[1, 2]"
|
|
536
|
+
type="text"
|
|
537
|
+
class="tw-font-semibold"
|
|
538
|
+
/>
|
|
539
|
+
</div>
|
|
540
|
+
</VcCard>
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Key `VcField` props:
|
|
544
|
+
- `orientation="horizontal"` — label left, value right (default is vertical)
|
|
545
|
+
- `:aspect-ratio="[1, 2]"` — label takes 1/3, value takes 2/3
|
|
546
|
+
- `copyable` — adds a copy-to-clipboard button
|
|
547
|
+
- `:tooltip` — adds an info icon with hover text
|
|
548
|
+
|
|
549
|
+
Use `VcField` when:
|
|
550
|
+
- The blade is read-only (order details, audit log, transaction view)
|
|
551
|
+
- Showing summary/computed data that users don't edit
|
|
552
|
+
- Displaying key-value pairs in a structured layout
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## Contextual Banners Pattern (VcBanner)
|
|
557
|
+
|
|
558
|
+
Use `VcBanner` at the top of a form to show contextual alerts, warnings, or informational messages based on entity state.
|
|
559
|
+
|
|
560
|
+
Source: `apps/vendor-portal/src/modules/offers/pages/offers-details.vue`, `seller-details-edit.vue`
|
|
561
|
+
|
|
562
|
+
```vue
|
|
563
|
+
<!-- Error banner (shown conditionally) -->
|
|
564
|
+
<VcBanner
|
|
565
|
+
v-if="errorMessage"
|
|
566
|
+
variant="danger"
|
|
567
|
+
icon="lucide-alert-circle"
|
|
568
|
+
icon-size="l"
|
|
569
|
+
icon-variant="danger"
|
|
570
|
+
>
|
|
571
|
+
<template #title>
|
|
572
|
+
{{ $t("MODULE.ALERTS.ERROR") }}
|
|
573
|
+
</template>
|
|
574
|
+
<template #default>
|
|
575
|
+
{{ errorMessage }}
|
|
576
|
+
</template>
|
|
577
|
+
</VcBanner>
|
|
578
|
+
|
|
579
|
+
<!-- Info banner with action button -->
|
|
580
|
+
<VcBanner
|
|
581
|
+
v-if="!entity.id"
|
|
582
|
+
variant="info"
|
|
583
|
+
icon="lucide-lightbulb"
|
|
584
|
+
icon-size="l"
|
|
585
|
+
>
|
|
586
|
+
{{ $t("MODULE.ALERTS.CREATE_NEW_HINT") }}
|
|
587
|
+
</VcBanner>
|
|
588
|
+
|
|
589
|
+
<!-- Warning banner with inline action -->
|
|
590
|
+
<VcBanner
|
|
591
|
+
v-if="entity.id && !entity.priceLists?.length"
|
|
592
|
+
variant="info"
|
|
593
|
+
icon="lucide-lightbulb"
|
|
594
|
+
icon-size="l"
|
|
595
|
+
>
|
|
596
|
+
<div class="tw-flex tw-flex-row tw-justify-between tw-items-center">
|
|
597
|
+
<span>{{ $t("MODULE.ALERTS.MISSING_PRICES") }}</span>
|
|
598
|
+
<VcButton
|
|
599
|
+
icon="lucide-plus"
|
|
600
|
+
variant="link"
|
|
601
|
+
size="sm"
|
|
602
|
+
class="tw-shrink-0"
|
|
603
|
+
@click="openBlade({ name: 'PricesList' })"
|
|
604
|
+
>{{ $t("MODULE.ACTIONS.ADD_PRICE") }}</VcButton>
|
|
605
|
+
</div>
|
|
606
|
+
</VcBanner>
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Banner variants: `"info"`, `"warning"`, `"danger"`, `"success"`.
|
|
610
|
+
|
|
611
|
+
Use banners when:
|
|
612
|
+
- Entity is in a special state (new, error, incomplete)
|
|
613
|
+
- User needs to take action (missing required data)
|
|
614
|
+
- Showing validation errors at form level
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## Blade Widgets Pattern (useBladeWidgets)
|
|
619
|
+
|
|
620
|
+
Blade widgets appear as icon buttons in the blade sidebar, showing related entities with badge counts. Clicking a widget opens a child blade.
|
|
621
|
+
|
|
622
|
+
Source: `apps/vendor-portal/src/modules/products/widgets/useProductWidgets.ts`
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
import { useBladeWidgets, type UseBladeWidgetsReturn } from "@vc-shell/framework";
|
|
626
|
+
|
|
627
|
+
export function useProductWidgets(options: { item: Ref<Product | undefined> }): UseBladeWidgetsReturn {
|
|
628
|
+
const { item } = options;
|
|
629
|
+
const { openBlade } = useBlade();
|
|
630
|
+
|
|
631
|
+
const { count: offersCount, loading: offersLoading, refresh: refreshOffers } = useOfferCount(item);
|
|
632
|
+
|
|
633
|
+
const widgets = useBladeWidgets([
|
|
634
|
+
{
|
|
635
|
+
id: "OffersWidget",
|
|
636
|
+
icon: "lucide-tag",
|
|
637
|
+
title: "PRODUCTS.PAGES.DETAILS.WIDGETS.OFFERS",
|
|
638
|
+
badge: offersCount,
|
|
639
|
+
loading: offersLoading,
|
|
640
|
+
isVisible: computed(() => !!item.value?.id),
|
|
641
|
+
onClick: () => openBlade({ name: "Offers", options: { product: item.value } }),
|
|
642
|
+
onRefresh: refreshOffers,
|
|
643
|
+
},
|
|
644
|
+
// ... more widgets
|
|
645
|
+
]);
|
|
646
|
+
|
|
647
|
+
return widgets;
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Use in the blade:
|
|
652
|
+
```ts
|
|
653
|
+
const { widgets } = useProductWidgets({ item: entity });
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Use widgets when:
|
|
657
|
+
- Entity has related sub-entities (product → offers, videos, assets)
|
|
658
|
+
- You want sidebar navigation with counts
|
|
659
|
+
- Each widget opens a different child blade
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
370
663
|
## Key Rules
|
|
371
664
|
|
|
372
665
|
1. **`defineBlade({ name: "XxxDetails" })`** — no `url`, no `isWorkspace` for blades opened from list. Add `url` + `isWorkspace: true` only for standalone workspace blades (settings pages, profile pages).
|
|
@@ -376,3 +669,57 @@ The `VcDataTable` here is read-only — no toolbar, no selection, no pagination
|
|
|
376
669
|
5. **After save/delete**: always `callParent("reload")` first, then `closeSelf()`.
|
|
377
670
|
6. **`isVisible`** on toolbar items: use `!!param.value` for edit-only actions (save, reset, delete), `!param.value` for create-only actions.
|
|
378
671
|
7. **`disabled: computed(...)`** on toolbar items must be a computed, not a plain boolean, for reactivity.
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Field Type → Component Mapping
|
|
676
|
+
|
|
677
|
+
Use this table to choose the correct component for each field type during generation. The `formField.type` value comes from the design prompt analysis (Phase 2 of `/vc-app design`) or from API type inference.
|
|
678
|
+
|
|
679
|
+
| Field Type | Component | When to Use | Notes |
|
|
680
|
+
|---|---|---|---|
|
|
681
|
+
| `string` | `VcInput` | Default for short text (name, title, email, phone, URL) | Use `type="number"` for numeric strings, `rules="email"` for emails |
|
|
682
|
+
| `text` | `VcTextarea` | Long text: description, notes, comments, bio | Multi-line input, no formatting |
|
|
683
|
+
| `rich-text` | `VcEditor` | HTML content: article body, email template, product description | WYSIWYG HTML editor |
|
|
684
|
+
| `number` | `VcInput` | Plain numbers: quantity, count, age, priority | Set `type="number"`, use `rules="bigint\|min_value:0"` |
|
|
685
|
+
| `currency` | `VcInputCurrency` | Money: price, cost, amount, salary, budget | Formatted with currency symbol and decimals |
|
|
686
|
+
| `boolean` | `VcSwitch` | Toggle: isActive, isEnabled, isPublished | No `Field` wrapper needed |
|
|
687
|
+
| `boolean` | `VcCheckbox` | Consent/agree: agreeTerms, acceptPolicy | Use when "agree/accept" semantics, not toggle |
|
|
688
|
+
| `date-time` | `VcDatePicker` | Date/datetime: createdDate, deadline, startDate, birthday | Calendar picker, preferred over `VcInput type="datetime-local"` |
|
|
689
|
+
| `enum` | `VcSelect` | Dropdown: status, type, category (6+ options or dynamic) | Use `option-value` + `option-label` |
|
|
690
|
+
| `enum` | `VcRadioGroup` | Radio: priority, type (2-5 static options) | More visual for small option sets |
|
|
691
|
+
| `multi-select` | `VcMultivalue` | Tags/multi-pick: tags, categories, permissions, roles | Tag-style chips with add/remove |
|
|
692
|
+
| `multi-select` | `VcCheckboxGroup` | Multi-check: features, capabilities (few static options) | Vertical checkbox list |
|
|
693
|
+
| `rating` | `VcRating` | Star rating: rating, score, quality | 1-5 star picker |
|
|
694
|
+
| `range` | `VcSlider` | Range: discount, opacity, volume, percentage | Numeric slider with min/max |
|
|
695
|
+
| `color` | `VcColorInput` | Color: brandColor, backgroundColor, themeColor | Color picker with hex input |
|
|
696
|
+
| `image` | `VcImageUpload` | Single image: avatar, logo, thumbnail, banner | Upload with preview |
|
|
697
|
+
| `gallery` | `VcGallery` | Multiple images: productPhotos, screenshots | Multi-image management grid |
|
|
698
|
+
| `file` | `VcFileUpload` | Attachment: document, contract, certificate | File upload with accept filter |
|
|
699
|
+
|
|
700
|
+
### Selection heuristics (when type is ambiguous)
|
|
701
|
+
|
|
702
|
+
When the field type from prompt analysis is plain `"string"`, use field name patterns to upgrade:
|
|
703
|
+
|
|
704
|
+
| Field name pattern | Upgrade to |
|
|
705
|
+
|---|---|
|
|
706
|
+
| `description`, `notes`, `comment`, `bio`, `summary`, `about` | `text` → `VcTextarea` |
|
|
707
|
+
| `body`, `content`, `html`, `template`, `article` | `rich-text` → `VcEditor` |
|
|
708
|
+
| `price`, `cost`, `amount`, `total`, `salary`, `budget`, `fee` | `currency` → `VcInputCurrency` |
|
|
709
|
+
| `avatar`, `logo`, `photo`, `thumbnail`, `banner`, `icon` | `image` → `VcImageUpload` |
|
|
710
|
+
| `photos`, `images`, `screenshots`, `gallery` | `gallery` → `VcGallery` |
|
|
711
|
+
| `file`, `attachment`, `document`, `contract`, `certificate` | `file` → `VcFileUpload` |
|
|
712
|
+
| `tags`, `labels`, `categories`, `roles`, `permissions` | `multi-select` → `VcMultivalue` |
|
|
713
|
+
| `rating`, `score`, `stars` | `rating` → `VcRating` |
|
|
714
|
+
| `color`, `colour`, `brandColor`, `backgroundColor` | `color` → `VcColorInput` |
|
|
715
|
+
| `discount`, `opacity`, `volume`, `percentage`, `progress` | `range` → `VcSlider` |
|
|
716
|
+
| `email` | `string` → `VcInput` with `rules="required\|email"` |
|
|
717
|
+
| `phone`, `tel` | `string` → `VcInput` with `type="tel"` |
|
|
718
|
+
| `url`, `website`, `link` | `string` → `VcInput` with `type="url"` |
|
|
719
|
+
|
|
720
|
+
### Layout heuristics
|
|
721
|
+
|
|
722
|
+
- **3+ fields in a form** → use `VcRow` / `VcCol` grid to arrange fields in 2 columns
|
|
723
|
+
- **5+ fields with logical groups** → wrap groups in `VcCard` with a section header
|
|
724
|
+
- **8+ fields** → consider `VcAccordion` for secondary/advanced fields
|
|
725
|
+
- **Mixed required + optional fields** → put required fields at the top, optional in a collapsible section
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Extension Points Usage Pattern
|
|
2
|
+
|
|
3
|
+
Cross-module UI composition without compile-time dependencies. One module declares a named slot (host), other modules inject components into it (plugin). Fully reactive and order-independent.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
- Module A needs to inject UI into Module B without importing it (e.g., a "Commissions" section inside a "Seller Details" blade owned by another module).
|
|
10
|
+
- Modules load in any order (including remote Module Federation modules).
|
|
11
|
+
|
|
12
|
+
**Do not use** for simple parent-child communication within one module -- use props/slots/provide-inject instead.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Concepts
|
|
17
|
+
|
|
18
|
+
### Two Roles
|
|
19
|
+
|
|
20
|
+
| Role | API | Purpose |
|
|
21
|
+
|------|-----|---------|
|
|
22
|
+
| **Host** | `<ExtensionPoint>` or `defineExtensionPoint()` | Declares a named region that accepts plugins |
|
|
23
|
+
| **Plugin** | `useExtensionPoint()` | Registers components into a named region |
|
|
24
|
+
|
|
25
|
+
Neither side imports the other. They communicate through a shared **name string**.
|
|
26
|
+
|
|
27
|
+
### Order Independence
|
|
28
|
+
|
|
29
|
+
Plugins can register components **before** the host declares the point. The reactive store preserves pre-registered components and upgrades the entry when the host declares.
|
|
30
|
+
|
|
31
|
+
### Priority Sorting
|
|
32
|
+
|
|
33
|
+
Components are sorted by `priority` (ascending, default `0`). Same priority = registration order.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Inbound Extensions (Host Accepts Plugins)
|
|
38
|
+
|
|
39
|
+
A module declares a named extension point in its blade template. Other modules inject components into it.
|
|
40
|
+
|
|
41
|
+
### Declarative: `<ExtensionPoint>` Component
|
|
42
|
+
|
|
43
|
+
The simplest approach -- declares the point and renders registered components in one step:
|
|
44
|
+
|
|
45
|
+
```vue
|
|
46
|
+
<!-- seller-details/pages/seller-details-edit.vue -->
|
|
47
|
+
<template>
|
|
48
|
+
<VcBlade title="Seller Details">
|
|
49
|
+
<form><!-- main form fields --></form>
|
|
50
|
+
|
|
51
|
+
<ExtensionPoint
|
|
52
|
+
v-if="sellerDetails?.id"
|
|
53
|
+
name="seller:commissions"
|
|
54
|
+
wrapper-class="tw-p-2"
|
|
55
|
+
/>
|
|
56
|
+
</VcBlade>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup lang="ts">
|
|
60
|
+
import { ExtensionPoint } from "@vc-shell/framework";
|
|
61
|
+
</script>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Props:**
|
|
65
|
+
|
|
66
|
+
| Prop | Type | Default | Description |
|
|
67
|
+
|------|------|---------|-------------|
|
|
68
|
+
| `name` | `string` | required | Extension point name |
|
|
69
|
+
| `separator` | `boolean` | `false` | Adds `<hr>` before components |
|
|
70
|
+
| `separatorClass` | `string` | built-in | Custom CSS class for `<hr>` |
|
|
71
|
+
| `wrapperClass` | `string` | none | CSS class for wrapper div |
|
|
72
|
+
| `gap` | `string` | none | CSS gap (e.g. `"1rem"`) |
|
|
73
|
+
| `filter` | `Record<string, unknown>` | none | Filter by meta field equality |
|
|
74
|
+
|
|
75
|
+
### Composable: `defineExtensionPoint()`
|
|
76
|
+
|
|
77
|
+
For programmatic access to registered components (e.g., building tabs dynamically):
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { defineExtensionPoint } from "@vc-shell/framework";
|
|
81
|
+
|
|
82
|
+
interface TabMeta {
|
|
83
|
+
zone: "tabs" | "toolbar";
|
|
84
|
+
label: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { components, hasComponents } = defineExtensionPoint<TabMeta>("product:details");
|
|
88
|
+
|
|
89
|
+
// components.value is ComputedRef<ExtensionComponent[]>, sorted by priority
|
|
90
|
+
// hasComponents.value is ComputedRef<boolean>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Scoped Slot for Custom Rendering
|
|
94
|
+
|
|
95
|
+
When the host needs full control over layout:
|
|
96
|
+
|
|
97
|
+
```vue
|
|
98
|
+
<ExtensionPoint name="order:actions" v-slot="{ components, hasComponents }">
|
|
99
|
+
<div v-if="hasComponents" class="tw-flex tw-gap-2">
|
|
100
|
+
<component
|
|
101
|
+
v-for="ext in components"
|
|
102
|
+
:key="ext.id"
|
|
103
|
+
:is="ext.component"
|
|
104
|
+
v-bind="ext.props || {}"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</ExtensionPoint>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Outbound Extensions (Plugin Provides Components)
|
|
113
|
+
|
|
114
|
+
A module registers components into an extension point declared by another module (or the framework itself).
|
|
115
|
+
|
|
116
|
+
### Basic Registration
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// commissions-module/index.ts
|
|
120
|
+
import { defineAppModule, useExtensionPoint } from "@vc-shell/framework";
|
|
121
|
+
import CommissionFields from "./components/CommissionFields.vue";
|
|
122
|
+
|
|
123
|
+
const { add } = useExtensionPoint("seller:commissions");
|
|
124
|
+
add({
|
|
125
|
+
id: "marketplace:commission-fields",
|
|
126
|
+
component: CommissionFields,
|
|
127
|
+
props: { editable: true },
|
|
128
|
+
priority: 10,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export default defineAppModule({ /* locales, blades, etc. */ });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Using Framework Constants
|
|
135
|
+
|
|
136
|
+
The framework exposes typed constants for its own extension points:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { useExtensionPoint, ExtensionPoints } from "@vc-shell/framework";
|
|
140
|
+
import RegistrationButton from "./components/RegistrationButton.vue";
|
|
141
|
+
|
|
142
|
+
const { add } = useExtensionPoint(ExtensionPoints.AUTH_AFTER_FORM);
|
|
143
|
+
add({
|
|
144
|
+
id: "registration:signup-button",
|
|
145
|
+
component: RegistrationButton,
|
|
146
|
+
meta: { type: "action" },
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Currently defined framework constants:
|
|
151
|
+
|
|
152
|
+
| Constant | Value | Location |
|
|
153
|
+
|----------|-------|----------|
|
|
154
|
+
| `ExtensionPoints.AUTH_AFTER_FORM` | `"auth:after-form"` | Login page, below sign-in form |
|
|
155
|
+
|
|
156
|
+
### Removing a Registration
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const { add, remove } = useExtensionPoint("seller:commissions");
|
|
160
|
+
add({ id: "my-fields", component: MyFields });
|
|
161
|
+
|
|
162
|
+
// Later (e.g., during cleanup):
|
|
163
|
+
remove("my-fields");
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Passing Props and Context to Injected Components
|
|
169
|
+
|
|
170
|
+
Props are passed via the `props` field in `add()` and bound with `v-bind`:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
add({
|
|
174
|
+
id: "commission-fields",
|
|
175
|
+
component: CommissionFields,
|
|
176
|
+
props: { editable: true, sellerId: "abc-123" },
|
|
177
|
+
priority: 10,
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The injected component receives them as standard Vue props:
|
|
182
|
+
|
|
183
|
+
```vue
|
|
184
|
+
<script setup lang="ts">
|
|
185
|
+
const props = defineProps<{
|
|
186
|
+
editable?: boolean;
|
|
187
|
+
sellerId?: string;
|
|
188
|
+
}>();
|
|
189
|
+
</script>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
For dynamic context that changes after registration, use reactive values or provide/inject within the host blade.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Metadata Filtering
|
|
197
|
+
|
|
198
|
+
Categorize extensions with `meta` and render different subsets in different locations:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const { add } = useExtensionPoint("product:details");
|
|
202
|
+
add({ id: "specs-tab", component: SpecsTab, meta: { zone: "tabs" }, priority: 10 });
|
|
203
|
+
add({ id: "review-tab", component: ReviewTab, meta: { zone: "tabs" }, priority: 20 });
|
|
204
|
+
add({ id: "quick-action", component: QuickAction, meta: { zone: "toolbar" } });
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```vue
|
|
208
|
+
<template>
|
|
209
|
+
<VcBlade title="Product Details">
|
|
210
|
+
<div class="toolbar">
|
|
211
|
+
<ExtensionPoint name="product:details" :filter="{ zone: 'toolbar' }" />
|
|
212
|
+
</div>
|
|
213
|
+
<div class="tabs">
|
|
214
|
+
<ExtensionPoint name="product:details" :filter="{ zone: 'tabs' }" />
|
|
215
|
+
</div>
|
|
216
|
+
</VcBlade>
|
|
217
|
+
</template>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Filtering uses strict equality (`===`) on every key-value pair; all pairs must match.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Recipe: Adding a Custom Tab to Another Module's Blade
|
|
225
|
+
|
|
226
|
+
**Step 1 -- Host module declares the point:**
|
|
227
|
+
|
|
228
|
+
```vue
|
|
229
|
+
<!-- orders/pages/order-details.vue -->
|
|
230
|
+
<template>
|
|
231
|
+
<VcBlade title="Order Details">
|
|
232
|
+
<VcForm><!-- core order fields --></VcForm>
|
|
233
|
+
|
|
234
|
+
<ExtensionPoint
|
|
235
|
+
name="order:details-tabs"
|
|
236
|
+
gap="1rem"
|
|
237
|
+
wrapper-class="tw-mt-4"
|
|
238
|
+
/>
|
|
239
|
+
</VcBlade>
|
|
240
|
+
</template>
|
|
241
|
+
|
|
242
|
+
<script setup lang="ts">
|
|
243
|
+
import { ExtensionPoint } from "@vc-shell/framework";
|
|
244
|
+
</script>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Step 2 -- Plugin module registers a tab component:**
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// shipping-module/index.ts
|
|
251
|
+
import { defineAppModule, useExtensionPoint } from "@vc-shell/framework";
|
|
252
|
+
import ShippingTab from "./components/ShippingTab.vue";
|
|
253
|
+
|
|
254
|
+
const { add } = useExtensionPoint("order:details-tabs");
|
|
255
|
+
add({
|
|
256
|
+
id: "shipping:tracking-tab",
|
|
257
|
+
component: ShippingTab,
|
|
258
|
+
props: { showMap: true },
|
|
259
|
+
priority: 20,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
export default defineAppModule({ /* ... */ });
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Step 3 -- The injected component:**
|
|
266
|
+
|
|
267
|
+
```vue
|
|
268
|
+
<!-- shipping-module/components/ShippingTab.vue -->
|
|
269
|
+
<template>
|
|
270
|
+
<VcCard header="Shipping & Tracking">
|
|
271
|
+
<div class="tw-p-4">
|
|
272
|
+
<p>Carrier: {{ carrier }}</p>
|
|
273
|
+
<div v-if="showMap"><!-- map widget --></div>
|
|
274
|
+
</div>
|
|
275
|
+
</VcCard>
|
|
276
|
+
</template>
|
|
277
|
+
|
|
278
|
+
<script setup lang="ts">
|
|
279
|
+
import { ref } from "vue";
|
|
280
|
+
defineProps<{ showMap?: boolean }>();
|
|
281
|
+
const carrier = ref("FedEx");
|
|
282
|
+
</script>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Common Mistakes
|
|
288
|
+
|
|
289
|
+
| Mistake | Symptom | Fix |
|
|
290
|
+
|---------|---------|-----|
|
|
291
|
+
| Typo in extension point name | Component registered but never rendered | Check dev console for `"Extension point X is not declared"` warning. Use shared constants. |
|
|
292
|
+
| Duplicate `id` across modules | Later registration silently replaces the earlier one | Prefix IDs with module name: `"shipping:tracking-tab"` |
|
|
293
|
+
| Forgetting `import { ExtensionPoint }` in host template | Vue renders nothing (unknown component) | Add the import in `<script setup>` |
|
|
294
|
+
| Calling `defineExtensionPoint` outside setup context | Computed refs not reactive | Call inside `<script setup>` or a composable used during setup |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Related
|
|
299
|
+
|
|
300
|
+
| Resource | Path |
|
|
301
|
+
|----------|------|
|
|
302
|
+
| Extension Points Plugin (full docs) | `framework/core/plugins/extension-points/extension-points.docs.md` |
|
|
303
|
+
| Store implementation | `framework/core/plugins/extension-points/store.ts` |
|
|
304
|
+
| ExtensionPoint component | `framework/core/plugins/extension-points/ExtensionPoint.vue` |
|
|
305
|
+
| Types | `framework/core/plugins/extension-points/types.ts` |
|
|
306
|
+
| Public API exports | `framework/core/plugins/extension-points/public.ts` |
|
|
307
|
+
| Real-world host usage | `apps/vendor-portal/src/modules/seller-details/pages/seller-details-edit.vue` |
|
|
308
|
+
| Framework host usage | `framework/shell/auth/LoginPage/components/login/Login.vue` |
|