@vc-shell/vc-app-skill 2.0.0-alpha.24 → 2.0.0-alpha.26
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/sync-to-docs.cjs +360 -0
- package/package.json +3 -2
- package/runtime/VERSION +1 -1
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/composables/useBladeWidgets.docs.md +164 -9
- package/runtime/knowledge/docs/core/composables/useWidgets/useWidgets.docs.md +2 -2
- package/runtime/knowledge/docs/injection-keys.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/molecules/vc-pagination/vc-pagination.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/organisms/{vc-table → vc-data-table}/vc-data-table.docs.md +2 -2
- package/runtime/knowledge/docs/core/composables/useWidget/useWidget.docs.md +0 -159
- /package/runtime/knowledge/docs/ui/components/organisms/{vc-table → vc-data-table}/composables/table-composables.docs.md +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# [2.0.0-alpha.26](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.25...v2.0.0-alpha.26) (2026-03-25)
|
|
2
|
+
|
|
3
|
+
**Note:** Version bump only for package @vc-shell/vc-app-skill
|
|
4
|
+
|
|
5
|
+
# [2.0.0-alpha.25](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.24...v2.0.0-alpha.25) (2026-03-25)
|
|
6
|
+
|
|
7
|
+
**Note:** Version bump only for package @vc-shell/vc-app-skill
|
|
8
|
+
|
|
1
9
|
# [2.0.0-alpha.24](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.23...v2.0.0-alpha.24) (2026-03-25)
|
|
2
10
|
|
|
3
11
|
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
8
|
+
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
9
|
+
const FRAMEWORK_DIR = path.join(ROOT, 'framework');
|
|
10
|
+
|
|
11
|
+
const STORYBOOK_BASE = 'https://vc-shell-storybook.govirto.com';
|
|
12
|
+
|
|
13
|
+
// Resolve vc-docs path from --docs-path arg or VC_DOCS_PATH env or default
|
|
14
|
+
const docsPathArg = process.argv.find((a) => a.startsWith('--docs-path='));
|
|
15
|
+
const VC_DOCS_PATH =
|
|
16
|
+
(docsPathArg && docsPathArg.split('=')[1]) ||
|
|
17
|
+
process.env.VC_DOCS_PATH ||
|
|
18
|
+
path.resolve(ROOT, '..', 'vc-docs');
|
|
19
|
+
|
|
20
|
+
const MKDOCS_YML = path.join(VC_DOCS_PATH, 'platform', 'developer-guide', 'mkdocs.yml');
|
|
21
|
+
const DOCS_OUT = path.join(VC_DOCS_PATH, 'platform', 'developer-guide', 'docs', 'custom-apps-development', 'vc-shell');
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(VC_DOCS_PATH)) {
|
|
24
|
+
console.error(`vc-docs repo not found at: ${VC_DOCS_PATH}`);
|
|
25
|
+
console.error('Use --docs-path=/path/to/vc-docs or set VC_DOCS_PATH env var');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Path mapping ────────────────────────────────────────────────────────────
|
|
30
|
+
// framework relative path → vc-docs relative path (under DOCS_OUT) + nav category
|
|
31
|
+
function mapPath(relPath) {
|
|
32
|
+
const parts = relPath.replace(/\.docs\.md$/, '').split(path.sep);
|
|
33
|
+
|
|
34
|
+
// core/composables/{name}/index → Essentials/composables/{name}.md
|
|
35
|
+
// core/composables/{name}.docs.md (flat) → Essentials/composables/{name}.md
|
|
36
|
+
if (parts[0] === 'core' && parts[1] === 'composables') {
|
|
37
|
+
const name = parts.length >= 3 ? parts[2] : parts[1];
|
|
38
|
+
return { outPath: `Essentials/composables/${name}.md`, navCategory: 'Composables' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// core/plugins/{name}/* → Essentials/plugins/{name}.md
|
|
42
|
+
if (parts[0] === 'core' && parts[1] === 'plugins') {
|
|
43
|
+
const name = parts[2];
|
|
44
|
+
return { outPath: `Essentials/plugins/${name}.md`, navCategory: 'Plugins' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// core/api/* → Essentials/API-Integration/{name}.md
|
|
48
|
+
if (parts[0] === 'core' && parts[1] === 'api') {
|
|
49
|
+
const name = parts[parts.length - 1];
|
|
50
|
+
return { outPath: `Essentials/API-Integration/${name}.md`, navCategory: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// core/blade-navigation/* → Essentials/composables/{name}.md
|
|
54
|
+
if (parts[0] === 'core' && parts[1] === 'blade-navigation') {
|
|
55
|
+
const name = parts[parts.length - 1];
|
|
56
|
+
return { outPath: `Essentials/composables/${name}.md`, navCategory: 'Composables' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// core/notifications/* → Essentials/shared/components/notifications.md
|
|
60
|
+
if (parts[0] === 'core' && parts[1] === 'notifications') {
|
|
61
|
+
return { outPath: 'Essentials/shared/components/notifications.md', navCategory: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ui/components/{tier}/{name}/* → Essentials/ui-components/{name}.md
|
|
65
|
+
// Skip internal composable docs (table-composables) — only sync the main component doc
|
|
66
|
+
if (parts[0] === 'ui' && parts[1] === 'components') {
|
|
67
|
+
const tier = parts[2]; // atoms, molecules, organisms
|
|
68
|
+
const compDir = parts[3]; // vc-badge, vc-select, etc.
|
|
69
|
+
const fileName = parts[parts.length - 1];
|
|
70
|
+
if (compDir) {
|
|
71
|
+
// Skip sub-directory docs (e.g. composables/table-composables.docs.md) — main doc covers these
|
|
72
|
+
if (parts.length > 5) return null;
|
|
73
|
+
// vc-table dir contains VcDataTable — use the actual component name
|
|
74
|
+
const outName = compDir === 'vc-table' ? 'vc-data-table' : compDir;
|
|
75
|
+
return { outPath: `Essentials/ui-components/${outName}.md`, navCategory: 'UI Components', tier };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ui/composables/* → skip (internal table composables)
|
|
80
|
+
if (parts[0] === 'ui' && parts[1] === 'composables') {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// shell/components/{name}/* → Essentials/shared/components/{name}.md
|
|
85
|
+
if (parts[0] === 'shell' && parts[1] === 'components') {
|
|
86
|
+
const name = parts[2];
|
|
87
|
+
return { outPath: `Essentials/shared/components/${name}.md`, navCategory: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// shell/dashboard/{name}/* → Essentials/shared/components/{name}.md
|
|
91
|
+
if (parts[0] === 'shell' && parts[1] === 'dashboard') {
|
|
92
|
+
const name = parts[2];
|
|
93
|
+
return { outPath: `Essentials/shared/components/${name}.md`, navCategory: null };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Anything else we don't map (core/types, core/utilities, etc.)
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Storybook helpers ───────────────────────────────────────────────────────
|
|
101
|
+
function findStorybookTitle(compDir) {
|
|
102
|
+
const storiesGlob = fs.readdirSync(compDir).find((f) => f.endsWith('.stories.ts'));
|
|
103
|
+
if (!storiesGlob) return null;
|
|
104
|
+
|
|
105
|
+
const content = fs.readFileSync(path.join(compDir, storiesGlob), 'utf-8');
|
|
106
|
+
const match = content.match(/title:\s*["']([^"']+)["']/);
|
|
107
|
+
return match ? match[1] : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function storybookTitleToId(title) {
|
|
111
|
+
// "Data Display/VcBadge" → "data-display-vcbadge"
|
|
112
|
+
return title
|
|
113
|
+
.toLowerCase()
|
|
114
|
+
.replace(/\s+/g, '-')
|
|
115
|
+
.replace(/\//g, '-');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildStorybookSection(title, componentName) {
|
|
119
|
+
const id = storybookTitleToId(title);
|
|
120
|
+
const displayName = componentName.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
121
|
+
return [
|
|
122
|
+
'',
|
|
123
|
+
'## Storybook',
|
|
124
|
+
'',
|
|
125
|
+
`[${displayName} Storybook](${STORYBOOK_BASE}/?path=/docs/${id}--docs)`,
|
|
126
|
+
'',
|
|
127
|
+
'<iframe',
|
|
128
|
+
` src="${STORYBOOK_BASE}/iframe.html?id=${id}--docs&viewMode=story&shortcuts=false&singleStory=true"`,
|
|
129
|
+
' width="1000"',
|
|
130
|
+
' height="500"',
|
|
131
|
+
'></iframe>',
|
|
132
|
+
'',
|
|
133
|
+
].join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Content transformation ──────────────────────────────────────────────────
|
|
137
|
+
function transformContent(content, mapping, frameworkRelPath) {
|
|
138
|
+
let result = content;
|
|
139
|
+
|
|
140
|
+
// If UI component, inject Storybook iframe after first heading
|
|
141
|
+
if (mapping.navCategory === 'UI Components' && mapping.tier) {
|
|
142
|
+
const compName = path.basename(mapping.outPath, '.md');
|
|
143
|
+
const compDir = path.join(FRAMEWORK_DIR, path.dirname(frameworkRelPath));
|
|
144
|
+
const storybookTitle = findStorybookTitle(compDir);
|
|
145
|
+
|
|
146
|
+
if (storybookTitle) {
|
|
147
|
+
const storySection = buildStorybookSection(storybookTitle, compName);
|
|
148
|
+
// Insert after first # heading line
|
|
149
|
+
result = result.replace(/^(# .+\n)/, `$1${storySection}\n`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Find all docs ───────────────────────────────────────────────────────────
|
|
157
|
+
function findDocsFiles(dir, base) {
|
|
158
|
+
const results = [];
|
|
159
|
+
if (!fs.existsSync(dir)) return results;
|
|
160
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
161
|
+
const fullPath = path.join(dir, entry.name);
|
|
162
|
+
if (entry.isDirectory()) {
|
|
163
|
+
results.push(...findDocsFiles(fullPath, base));
|
|
164
|
+
} else if (entry.name.endsWith('.docs.md')) {
|
|
165
|
+
results.push(path.relative(base, fullPath));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return results;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Nav update helpers ──────────────────────────────────────────────────────
|
|
172
|
+
function extractTitle(content) {
|
|
173
|
+
const match = content.match(/^# (.+)$/m);
|
|
174
|
+
return match ? match[1] : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function updateNavSection(mkdocsContent, category, entries) {
|
|
178
|
+
// entries: [{title, path}]
|
|
179
|
+
// We update the specific nav category section in mkdocs.yml
|
|
180
|
+
// This is a targeted update — find the category line and replace its children
|
|
181
|
+
|
|
182
|
+
const lines = mkdocsContent.split('\n');
|
|
183
|
+
const result = [];
|
|
184
|
+
let inCategory = false;
|
|
185
|
+
let categoryIndent = 0;
|
|
186
|
+
let replaced = false;
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < lines.length; i++) {
|
|
189
|
+
const line = lines[i];
|
|
190
|
+
const trimmed = line.trimStart();
|
|
191
|
+
|
|
192
|
+
// Find the category header (e.g., "- Composables:" or "- UI Components:")
|
|
193
|
+
if (!replaced && trimmed === `- ${category}:`) {
|
|
194
|
+
categoryIndent = line.length - trimmed.length;
|
|
195
|
+
inCategory = true;
|
|
196
|
+
result.push(line);
|
|
197
|
+
|
|
198
|
+
// Add all entries with proper indentation
|
|
199
|
+
const childIndent = ' '.repeat(categoryIndent + 6); // 6 more for child items
|
|
200
|
+
const sortedEntries = entries.sort((a, b) => a.title.localeCompare(b.title));
|
|
201
|
+
for (const entry of sortedEntries) {
|
|
202
|
+
result.push(`${childIndent}- ${entry.title}: ${entry.navPath}`);
|
|
203
|
+
}
|
|
204
|
+
replaced = true;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Skip old children of this category
|
|
209
|
+
if (inCategory) {
|
|
210
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
211
|
+
if (line.trim() === '' || lineIndent > categoryIndent) {
|
|
212
|
+
// Still inside category children, skip
|
|
213
|
+
if (line.trim().startsWith('-') && lineIndent > categoryIndent) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (line.trim() !== '' && (line.length - line.trimStart().length) <= categoryIndent) {
|
|
218
|
+
inCategory = false;
|
|
219
|
+
// This line belongs to next section, keep it
|
|
220
|
+
result.push(line);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (line.trim() === '') {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
result.push(line);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result.join('\n');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
236
|
+
const frameworkDocs = findDocsFiles(FRAMEWORK_DIR, FRAMEWORK_DIR);
|
|
237
|
+
|
|
238
|
+
let synced = 0;
|
|
239
|
+
let skipped = 0;
|
|
240
|
+
let unmapped = 0;
|
|
241
|
+
|
|
242
|
+
// Track nav entries per category
|
|
243
|
+
const navEntries = {
|
|
244
|
+
Composables: [],
|
|
245
|
+
Plugins: [],
|
|
246
|
+
'UI Components': [],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Pre-populate nav entries from existing mkdocs.yml to preserve manual entries
|
|
250
|
+
// (we only update categories we manage)
|
|
251
|
+
|
|
252
|
+
const NAV_PREFIX = 'custom-apps-development/vc-shell/';
|
|
253
|
+
|
|
254
|
+
for (const relPath of frameworkDocs) {
|
|
255
|
+
const mapping = mapPath(relPath);
|
|
256
|
+
if (!mapping) {
|
|
257
|
+
unmapped++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const src = path.join(FRAMEWORK_DIR, relPath);
|
|
262
|
+
const dest = path.join(DOCS_OUT, mapping.outPath);
|
|
263
|
+
const srcContent = fs.readFileSync(src, 'utf-8');
|
|
264
|
+
|
|
265
|
+
// Transform content
|
|
266
|
+
const transformed = transformContent(srcContent, mapping, relPath);
|
|
267
|
+
|
|
268
|
+
// Check if dest is identical
|
|
269
|
+
if (fs.existsSync(dest)) {
|
|
270
|
+
const destContent = fs.readFileSync(dest, 'utf-8');
|
|
271
|
+
if (destContent === transformed) {
|
|
272
|
+
skipped++;
|
|
273
|
+
// Still collect nav entry
|
|
274
|
+
if (mapping.navCategory && navEntries[mapping.navCategory]) {
|
|
275
|
+
const title = extractTitle(srcContent) || path.basename(mapping.outPath, '.md');
|
|
276
|
+
navEntries[mapping.navCategory].push({
|
|
277
|
+
title,
|
|
278
|
+
navPath: NAV_PREFIX + mapping.outPath,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
286
|
+
fs.writeFileSync(dest, transformed);
|
|
287
|
+
console.log(` synced: ${relPath} → ${mapping.outPath}`);
|
|
288
|
+
synced++;
|
|
289
|
+
|
|
290
|
+
// Collect nav entry
|
|
291
|
+
if (mapping.navCategory && navEntries[mapping.navCategory]) {
|
|
292
|
+
const title = extractTitle(srcContent) || path.basename(mapping.outPath, '.md');
|
|
293
|
+
navEntries[mapping.navCategory].push({
|
|
294
|
+
title,
|
|
295
|
+
navPath: NAV_PREFIX + mapping.outPath,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Update mkdocs.yml nav sections
|
|
301
|
+
if (fs.existsSync(MKDOCS_YML)) {
|
|
302
|
+
let mkdocsContent = fs.readFileSync(MKDOCS_YML, 'utf-8');
|
|
303
|
+
let navUpdated = false;
|
|
304
|
+
|
|
305
|
+
for (const [category, entries] of Object.entries(navEntries)) {
|
|
306
|
+
if (entries.length === 0) continue;
|
|
307
|
+
|
|
308
|
+
const before = mkdocsContent;
|
|
309
|
+
mkdocsContent = updateNavSection(mkdocsContent, category, entries);
|
|
310
|
+
if (mkdocsContent !== before) navUpdated = true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (navUpdated) {
|
|
314
|
+
fs.writeFileSync(MKDOCS_YML, mkdocsContent);
|
|
315
|
+
console.log(`\n mkdocs.yml nav updated`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── Mark orphaned docs ──────────────────────────────────────────────────────
|
|
320
|
+
// Docs in vc-docs that no longer have a source in framework get a deprecation banner
|
|
321
|
+
const DEPRECATION_BANNER = [
|
|
322
|
+
'!!! warning "Deprecated"',
|
|
323
|
+
' This API has been removed from the current version of the framework.',
|
|
324
|
+
' This page is kept for reference only. Do not use in new code.',
|
|
325
|
+
'',
|
|
326
|
+
].join('\n');
|
|
327
|
+
|
|
328
|
+
const managedDirs = [
|
|
329
|
+
path.join(DOCS_OUT, 'Essentials', 'composables'),
|
|
330
|
+
path.join(DOCS_OUT, 'Essentials', 'plugins'),
|
|
331
|
+
path.join(DOCS_OUT, 'Essentials', 'ui-components'),
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
// Build set of all output paths we just synced
|
|
335
|
+
const syncedOutPaths = new Set();
|
|
336
|
+
for (const relPath of frameworkDocs) {
|
|
337
|
+
const mapping = mapPath(relPath);
|
|
338
|
+
if (mapping) syncedOutPaths.add(path.join(DOCS_OUT, mapping.outPath));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let deprecated = 0;
|
|
342
|
+
for (const dir of managedDirs) {
|
|
343
|
+
if (!fs.existsSync(dir)) continue;
|
|
344
|
+
for (const file of fs.readdirSync(dir)) {
|
|
345
|
+
if (!file.endsWith('.md')) continue;
|
|
346
|
+
const filePath = path.join(dir, file);
|
|
347
|
+
if (syncedOutPaths.has(filePath)) continue;
|
|
348
|
+
|
|
349
|
+
// File exists in vc-docs but not in framework — mark as deprecated
|
|
350
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
351
|
+
if (content.includes('This API has been removed')) continue; // already marked
|
|
352
|
+
|
|
353
|
+
fs.writeFileSync(filePath, DEPRECATION_BANNER + '\n' + content);
|
|
354
|
+
const relFile = path.relative(DOCS_OUT, filePath);
|
|
355
|
+
console.log(` deprecated: ${relFile}`);
|
|
356
|
+
deprecated++;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log(`\nSync to vc-docs complete: ${synced} synced, ${skipped} unchanged, ${unmapped} unmapped, ${deprecated} deprecated`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vc-shell/vc-app-skill",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.26",
|
|
4
4
|
"description": "AI coding skill for scaffolding and generating VirtoCommerce Shell applications. Works with Claude Code, OpenCode, Gemini, Codex, Cursor.",
|
|
5
5
|
"bin": "./bin/install.cjs",
|
|
6
6
|
"files": [
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"sync-docs": "node bin/sync-docs.cjs",
|
|
23
|
-
"knowledge-stats": "node bin/knowledge-stats.cjs"
|
|
23
|
+
"knowledge-stats": "node bin/knowledge-stats.cjs",
|
|
24
|
+
"sync-to-docs": "node bin/sync-to-docs.cjs"
|
|
24
25
|
},
|
|
25
26
|
"publishConfig": {
|
|
26
27
|
"access": "public",
|
package/runtime/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.0.0-alpha.
|
|
1
|
+
2.0.0-alpha.26
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Synced from framework at commit
|
|
1
|
+
Synced from framework at commit 6f3f68723 on 2026-03-25T13:28:17.111Z
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
# useBladeWidgets
|
|
1
|
+
# useBladeWidgets / useWidgetTrigger
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two composables for the widget system — one for the **blade side**, one for the **widget side**.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
| Composable | Called from | Purpose |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `useBladeWidgets` | Blade component | Register headless widgets + get `refresh()` / `refreshAll()` |
|
|
8
|
+
| `useWidgetTrigger` | External widget component | Register trigger callbacks (`onRefresh`, `onClick`) via provide/inject |
|
|
9
|
+
|
|
10
|
+
Headless widgets are defined as plain configuration objects with reactive refs for dynamic values like badge counts and loading states. External component-based widgets use `useWidgetTrigger` to register their refresh callbacks so the hosting blade can trigger them.
|
|
6
11
|
|
|
7
12
|
## When to Use
|
|
8
13
|
|
|
9
|
-
- Register sidebar widgets (counters, action buttons) for a blade without creating Vue components
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- When NOT to use: for widgets that need their own template or complex UI (use a full widget component instead)
|
|
14
|
+
- **`useBladeWidgets`**: Register sidebar widgets (counters, action buttons) for a blade without creating Vue components. Refresh widget data after blade operations (save, delete).
|
|
15
|
+
- **`useWidgetTrigger`**: Inside an external widget component (registered via `registerExternalWidget`) to register `onRefresh` / `onClick` callbacks. The blade can then call `refresh(widgetId)` or `refreshAll()` to trigger them.
|
|
16
|
+
- When NOT to use `useBladeWidgets`: for widgets that need their own template or complex UI (use `registerExternalWidget` + `useWidgetTrigger` instead).
|
|
13
17
|
|
|
14
18
|
## Basic Usage
|
|
15
19
|
|
|
@@ -70,6 +74,121 @@ refreshAll();
|
|
|
70
74
|
| `refresh` | `(widgetId: string) => void` | Trigger `onRefresh` on a specific widget |
|
|
71
75
|
| `refreshAll` | `() => void` | Trigger `onRefresh` on all widgets that have one |
|
|
72
76
|
|
|
77
|
+
## useWidgetTrigger
|
|
78
|
+
|
|
79
|
+
Widget-side composable for external component-based widgets. Registers a trigger contract (`onRefresh`, `onClick`, `badge`) via provide/inject — no props, IDs, or service knowledge required.
|
|
80
|
+
|
|
81
|
+
### Basic Usage
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { useWidgetTrigger } from '@vc-shell/framework';
|
|
85
|
+
|
|
86
|
+
// Inside an external widget component:
|
|
87
|
+
useWidgetTrigger({ onRefresh: loadData });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### IWidgetTrigger
|
|
91
|
+
|
|
92
|
+
| Field | Type | Required | Description |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| `icon` | `string` | No | Lucide icon name for dropdown rendering |
|
|
95
|
+
| `title` | `string` | No | Display title (fallback: widget's title) |
|
|
96
|
+
| `badge` | `Ref<number \| string>` | No | Reactive badge value |
|
|
97
|
+
| `onClick` | `() => void` | No | Handler called when widget is clicked in dropdown |
|
|
98
|
+
| `onRefresh` | `() => void \| Promise<void>` | No | Handler called to refresh widget data |
|
|
99
|
+
| `disabled` | `Ref<boolean> \| boolean` | No | Disabled state |
|
|
100
|
+
|
|
101
|
+
### How It Works
|
|
102
|
+
|
|
103
|
+
1. `WidgetContainer` wraps each component-based widget in a `WidgetScope` provider
|
|
104
|
+
2. `WidgetScope` provides a `setTrigger` function scoped to the specific widget ID and blade ID
|
|
105
|
+
3. `useWidgetTrigger` injects this scope and calls `setTrigger` — no props or IDs needed
|
|
106
|
+
4. When the blade calls `refresh(widgetId)` or `refreshAll()`, the registered `onRefresh` is invoked
|
|
107
|
+
|
|
108
|
+
## Recipe: External Widget with Refresh
|
|
109
|
+
|
|
110
|
+
A complete example of an external widget that shows an unread message count and supports refresh from the blade:
|
|
111
|
+
|
|
112
|
+
**1. Register the external widget (module index.ts):**
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { createAppModule, registerExternalWidget, IBladeInstance } from "@vc-shell/framework";
|
|
116
|
+
import { markRaw } from "vue";
|
|
117
|
+
import { MessageWidget } from "./components/widgets";
|
|
118
|
+
|
|
119
|
+
registerExternalWidget({
|
|
120
|
+
id: "MessageWidget",
|
|
121
|
+
component: markRaw(MessageWidget),
|
|
122
|
+
targetBlades: ["ProductDetails", "OrderDetails"],
|
|
123
|
+
isVisible: (blade?: IBladeInstance) => !!blade?.param,
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**2. Widget component (message-widget.vue):**
|
|
128
|
+
|
|
129
|
+
```vue
|
|
130
|
+
<template>
|
|
131
|
+
<VcWidget
|
|
132
|
+
v-loading:500="loading"
|
|
133
|
+
:title="$t('MESSENGER.WIDGET.TITLE')"
|
|
134
|
+
icon="lucide-message-circle"
|
|
135
|
+
:value="messageCount"
|
|
136
|
+
@click="openMessageBlade"
|
|
137
|
+
/>
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<script setup lang="ts">
|
|
141
|
+
import { ref, computed, onMounted } from "vue";
|
|
142
|
+
import {
|
|
143
|
+
loading as vLoading,
|
|
144
|
+
useBlade,
|
|
145
|
+
injectBladeContext,
|
|
146
|
+
useWidgetTrigger,
|
|
147
|
+
VcWidget,
|
|
148
|
+
} from "@vc-shell/framework";
|
|
149
|
+
|
|
150
|
+
const ctx = injectBladeContext();
|
|
151
|
+
const entityId = computed(() => (ctx.value.item as { id?: string })?.id ?? "");
|
|
152
|
+
|
|
153
|
+
const messageCount = ref(0);
|
|
154
|
+
const loading = ref(false);
|
|
155
|
+
|
|
156
|
+
const loadData = async () => {
|
|
157
|
+
loading.value = true;
|
|
158
|
+
try {
|
|
159
|
+
messageCount.value = await api.getUnreadCount(entityId.value);
|
|
160
|
+
} finally {
|
|
161
|
+
loading.value = false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Register refresh callback — blade can call refreshAll() after save
|
|
166
|
+
useWidgetTrigger({ onRefresh: loadData });
|
|
167
|
+
|
|
168
|
+
onMounted(() => {
|
|
169
|
+
if (entityId.value) loadData();
|
|
170
|
+
});
|
|
171
|
+
</script>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**3. Blade refreshes widgets after save:**
|
|
175
|
+
|
|
176
|
+
```vue
|
|
177
|
+
<script setup lang="ts">
|
|
178
|
+
import { useBladeWidgets } from "@vc-shell/framework";
|
|
179
|
+
|
|
180
|
+
// Empty array — blade doesn't register headless widgets,
|
|
181
|
+
// but gets refresh/refreshAll for external widgets
|
|
182
|
+
const { refresh, refreshAll } = useBladeWidgets([]);
|
|
183
|
+
|
|
184
|
+
async function save() {
|
|
185
|
+
await api.saveProduct(product.value);
|
|
186
|
+
refreshAll(); // refresh all widgets (including MessageWidget)
|
|
187
|
+
// or: refresh("MessageWidget"); // refresh a specific widget by ID
|
|
188
|
+
}
|
|
189
|
+
</script>
|
|
190
|
+
```
|
|
191
|
+
|
|
73
192
|
## Recipe: Product Detail Blade with Multiple Widgets
|
|
74
193
|
|
|
75
194
|
```vue
|
|
@@ -126,10 +245,15 @@ async function save() {
|
|
|
126
245
|
|
|
127
246
|
## Prerequisites
|
|
128
247
|
|
|
248
|
+
**`useBladeWidgets`**:
|
|
129
249
|
- Must be called inside a blade component rendered by `VcBladeSlot` (requires `BladeDescriptorKey` injection).
|
|
130
250
|
- `WidgetService` must be provided in the component tree (automatically available in vc-shell apps).
|
|
131
251
|
- Calling outside a blade context throws an error with a descriptive message.
|
|
132
252
|
|
|
253
|
+
**`useWidgetTrigger`**:
|
|
254
|
+
- Must be called inside a widget component rendered by `WidgetContainer` (requires `WidgetScopeKey` injection).
|
|
255
|
+
- If called outside a widget scope, logs a warning and does nothing (does not throw).
|
|
256
|
+
|
|
133
257
|
## Details
|
|
134
258
|
|
|
135
259
|
- **Lifecycle management**: Widgets are registered in `onMounted` and unregistered in `onUnmounted`. This ensures the WidgetService always reflects the currently visible blades.
|
|
@@ -143,8 +267,39 @@ async function save() {
|
|
|
143
267
|
- Keep widget IDs unique within a blade. Duplicate IDs will overwrite previous registrations.
|
|
144
268
|
- Combine with `defineBladeContext` to expose blade entity data that widget components (non-headless) can consume via `injectBladeContext`.
|
|
145
269
|
|
|
270
|
+
## Common Mistakes
|
|
271
|
+
|
|
272
|
+
### Calling useWidgetTrigger outside WidgetContainer scope
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// Wrong — called in a standalone component, not rendered inside a blade widget slot
|
|
276
|
+
export default defineComponent({
|
|
277
|
+
setup() {
|
|
278
|
+
useWidgetTrigger({ onRefresh: loadData }); // ⚠️ Logs warning, trigger not registered
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// Correct — called inside a widget component registered via registerExternalWidget
|
|
285
|
+
// and rendered by WidgetContainer within a blade
|
|
286
|
+
useWidgetTrigger({ onRefresh: loadData }); // ✓ WidgetScope provides context
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Forgetting to pass empty array to useBladeWidgets for refresh-only usage
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// Wrong — useBladeWidgets requires an array argument
|
|
293
|
+
const { refreshAll } = useBladeWidgets(); // TS error
|
|
294
|
+
|
|
295
|
+
// Correct — pass empty array when you only need refresh/refreshAll
|
|
296
|
+
const { refreshAll } = useBladeWidgets([]);
|
|
297
|
+
```
|
|
298
|
+
|
|
146
299
|
## Related
|
|
147
300
|
|
|
148
|
-
- `defineBladeContext` -- expose blade data
|
|
149
|
-
- `
|
|
301
|
+
- `defineBladeContext` / `injectBladeContext` -- expose/consume blade data in external widgets
|
|
302
|
+
- `registerExternalWidget` -- register a component-based widget globally for target blades
|
|
303
|
+
- `WidgetService` in `@core/services/widget-service` -- underlying service
|
|
304
|
+
- `WidgetScope` in `vc-blade/_internal/widgets/WidgetScope.vue` -- provides `WidgetScopeKey` to widget components
|
|
150
305
|
- `VcBladeSlot` -- the blade wrapper that provides `BladeDescriptorKey`
|
|
@@ -9,7 +9,7 @@ Also exports `provideWidgetService()` for framework-level initialization and `re
|
|
|
9
9
|
- When you need low-level access to the widget service (register, unregister, query widgets)
|
|
10
10
|
- When building framework infrastructure that manages widget lifecycles
|
|
11
11
|
- When pre-registering widgets from a module's `install()` function before the component tree exists
|
|
12
|
-
- When NOT to use: for typical blade widget work, prefer `useBladeWidgets()` which handles lifecycle automatically. For widget-side logic (badge, refresh), use
|
|
12
|
+
- When NOT to use: for typical blade widget work, prefer `useBladeWidgets()` which handles lifecycle automatically. For widget-side logic (badge, refresh), use headless widgets.
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
|
15
15
|
|
|
@@ -46,7 +46,7 @@ None.
|
|
|
46
46
|
| `setActiveWidget` | `(args) => void` | Sets a widget as active with its exposed instance. |
|
|
47
47
|
| `updateActiveWidget` | `() => void` | Triggers the active widget's update function. Deprecated -- use headless widgets instead. |
|
|
48
48
|
| `isWidgetRegistered` | `(id: string) => boolean` | Checks if a widget ID exists in any blade's registry. |
|
|
49
|
-
| `updateWidget` | `(args) => void` | Updates properties of a registered widget (trigger, badge, etc.).
|
|
49
|
+
| `updateWidget` | `(args) => void` | Updates properties of a registered widget (trigger, badge, etc.). |
|
|
50
50
|
| `resolveWidgetProps` | `(widget, bladeData) => Record<string, unknown>` | Resolves widget props from blade data. Deprecated. |
|
|
51
51
|
| `getExternalWidgetsForBlade` | `(bladeId: string) => IExternalWidgetRegistration[]` | Gets external widgets that target a specific blade (registered by other modules). |
|
|
52
52
|
| `getAllExternalWidgets` | `() => IExternalWidgetRegistration[]` | Gets all registered external widgets across all modules. |
|
|
@@ -62,7 +62,7 @@ This centralized approach has several advantages:
|
|
|
62
62
|
|
|
63
63
|
| Key | Type | Description |
|
|
64
64
|
|-----|------|-------------|
|
|
65
|
-
| `
|
|
65
|
+
| `WidgetScopeKey` | `IWidgetScope` | Widget scope (provided by WidgetContainer for component-based widgets via `WidgetScope.vue`) |
|
|
66
66
|
| `AppRootElementKey` | `Ref<HTMLElement \| undefined>` | App root element for scoped Teleport |
|
|
67
67
|
| `EmbeddedModeKey` | `boolean` | Whether the app runs in embedded mode |
|
|
68
68
|
| `ShellIndicatorsKey` | `ComputedRef<boolean>` | Unread indicator state for sidebar |
|
|
@@ -286,5 +286,5 @@ currentPage.value = Math.min(currentPage.value, totalPages.value);
|
|
|
286
286
|
|
|
287
287
|
## Related Components
|
|
288
288
|
|
|
289
|
-
- [VcDataTable](../../organisms/vc-table
|
|
289
|
+
- [VcDataTable](../../organisms/vc-table) -- Data table that commonly pairs with pagination for large datasets
|
|
290
290
|
- [VcSelect](../vc-select/) -- Can be used alongside pagination for a "rows per page" selector
|
|
@@ -24,8 +24,8 @@ Columns are defined as `<VcColumn>` child components -- no configuration objects
|
|
|
24
24
|
|----------|-----------|
|
|
25
25
|
| Tabular data with sorting, selection, pagination | **VcDataTable** |
|
|
26
26
|
| Simple short list without table features | `v-for` with custom markup |
|
|
27
|
-
| Image/card grid layout | [VcGallery](../vc-gallery
|
|
28
|
-
| Key-value detail display | [VcField](../../molecules/vc-field
|
|
27
|
+
| Image/card grid layout | [VcGallery](../vc-gallery) |
|
|
28
|
+
| Key-value detail display | [VcField](../../molecules/vc-field) or [VcCard](../../atoms/vc-card) |
|
|
29
29
|
|
|
30
30
|
Use VcDataTable whenever you need structured rows and columns with any combination of sorting, filtering, inline editing, or column management. **Do not use** VcDataTable for simple lists of 5-10 items that need no table features -- a plain `v-for` loop is lighter. For thumbnail/card grids, prefer VcGallery.
|
|
31
31
|
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
# useWidget
|
|
2
|
-
|
|
3
|
-
Widget-side composable that auto-discovers widget identity from `WidgetProvider` and provides a `setTrigger()` method to register refresh/badge contracts with the widget service. This composable is the counterpart to the blade-side `useWidgets()` / `useBladeWidgets()` -- while those manage widget registration from the blade's perspective, `useWidget()` is called from inside the widget component itself to communicate its state (badge count, icon, refresh callback) back to the widget system.
|
|
4
|
-
|
|
5
|
-
**Deprecated**: Prefer headless widgets via `useBladeWidgets()` instead. This composable is only needed for legacy SFC widgets that register trigger contracts manually.
|
|
6
|
-
|
|
7
|
-
## When to Use
|
|
8
|
-
|
|
9
|
-
- Inside a widget component wrapped by `WidgetProvider` to register badge counts, refresh callbacks, or click handlers
|
|
10
|
-
- When building a custom blade widget that needs to communicate its state back to the parent blade's widget panel
|
|
11
|
-
- When NOT to use: outside a `WidgetProvider` context (will throw). For managing widgets from the blade side, use `useWidgets()` or `useBladeWidgets()` instead. For new widgets, prefer the headless widget pattern.
|
|
12
|
-
|
|
13
|
-
## Quick Start
|
|
14
|
-
|
|
15
|
-
```vue
|
|
16
|
-
<script setup lang="ts">
|
|
17
|
-
import { useWidget } from '@vc-shell/framework';
|
|
18
|
-
import { ref, computed, onMounted } from 'vue';
|
|
19
|
-
|
|
20
|
-
const { widgetId, setTrigger } = useWidget();
|
|
21
|
-
|
|
22
|
-
const items = ref<unknown[]>([]);
|
|
23
|
-
const unreadCount = computed(() => items.value.filter((i) => !i.isRead).length);
|
|
24
|
-
|
|
25
|
-
async function fetchData() {
|
|
26
|
-
items.value = await api.getWidgetItems();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Register the trigger contract: badge + refresh callback
|
|
30
|
-
setTrigger({
|
|
31
|
-
badge: unreadCount,
|
|
32
|
-
icon: 'fas fa-inbox',
|
|
33
|
-
title: 'Messages',
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
onMounted(fetchData);
|
|
37
|
-
</script>
|
|
38
|
-
|
|
39
|
-
<template>
|
|
40
|
-
<div class="tw-p-4">
|
|
41
|
-
<p class="tw-text-sm tw-text-gray-500">Widget ID: {{ widgetId }}</p>
|
|
42
|
-
<ul>
|
|
43
|
-
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
|
|
44
|
-
</ul>
|
|
45
|
-
</div>
|
|
46
|
-
</template>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## API
|
|
50
|
-
|
|
51
|
-
### Parameters
|
|
52
|
-
|
|
53
|
-
None. Identity is auto-discovered from the `WidgetProvider` injection context. The composable injects `WidgetIdKey` for the widget ID, `WidgetServiceKey` for the service, and `BladeDescriptorKey` for the parent blade context.
|
|
54
|
-
|
|
55
|
-
### Returns
|
|
56
|
-
|
|
57
|
-
| Property | Type | Description |
|
|
58
|
-
|----------|------|-------------|
|
|
59
|
-
| `widgetId` | `string` | The widget's unique ID, injected by `WidgetProvider`. |
|
|
60
|
-
| `setTrigger` | `(trigger: IWidgetTrigger) => void` | Registers a trigger contract (badge, icon, title, callbacks) for this widget. Can be called multiple times to update the trigger. |
|
|
61
|
-
|
|
62
|
-
### IWidgetTrigger
|
|
63
|
-
|
|
64
|
-
| Property | Type | Description |
|
|
65
|
-
|----------|------|-------------|
|
|
66
|
-
| `icon` | `string?` | Lucide or FontAwesome icon name for the widget's display in the dropdown/panel. |
|
|
67
|
-
| `title` | `string?` | Display title for the widget. Falls back to `IWidget.title` if not set. |
|
|
68
|
-
| `badge` | `Ref<number \| string> \| ComputedRef<number \| string>?` | Reactive badge value displayed on the widget button/tab. Pass a computed ref for automatic updates. |
|
|
69
|
-
|
|
70
|
-
## How It Works
|
|
71
|
-
|
|
72
|
-
1. **Identity discovery**: On creation, the composable injects `WidgetIdKey` (provided by `WidgetProvider`) and `WidgetServiceKey` (provided by `provideWidgetService()`). It also injects `BladeDescriptorKey` to know which blade this widget belongs to.
|
|
73
|
-
|
|
74
|
-
2. **Trigger registration**: When you call `setTrigger(trigger)`, the composable calls `widgetService.updateWidget({ id: widgetId, bladeId, widget: { trigger } })`. This stores the trigger contract in the widget service, where the blade's widget panel can read it.
|
|
75
|
-
|
|
76
|
-
3. **Reactive badges**: Because `badge` accepts a `Ref` or `ComputedRef`, the widget panel automatically updates whenever the badge value changes. There is no need to call `setTrigger` again after the initial registration.
|
|
77
|
-
|
|
78
|
-
## Recipe: Widget with Live Badge Count
|
|
79
|
-
|
|
80
|
-
```vue
|
|
81
|
-
<script setup lang="ts">
|
|
82
|
-
import { useWidget } from '@vc-shell/framework';
|
|
83
|
-
import { ref, computed, onMounted } from 'vue';
|
|
84
|
-
|
|
85
|
-
const { setTrigger } = useWidget();
|
|
86
|
-
const pendingOrders = ref<Order[]>([]);
|
|
87
|
-
|
|
88
|
-
async function loadPendingOrders() {
|
|
89
|
-
pendingOrders.value = await orderApi.getPending();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Badge shows the count of pending orders
|
|
93
|
-
setTrigger({
|
|
94
|
-
badge: computed(() => pendingOrders.value.length),
|
|
95
|
-
icon: 'fas fa-clock',
|
|
96
|
-
title: 'Pending Orders',
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
onMounted(loadPendingOrders);
|
|
100
|
-
|
|
101
|
-
// Optionally refresh periodically
|
|
102
|
-
const interval = setInterval(loadPendingOrders, 30_000);
|
|
103
|
-
onUnmounted(() => clearInterval(interval));
|
|
104
|
-
</script>
|
|
105
|
-
|
|
106
|
-
<template>
|
|
107
|
-
<div class="tw-space-y-2">
|
|
108
|
-
<div v-for="order in pendingOrders" :key="order.id" class="tw-p-2 tw-border tw-rounded">
|
|
109
|
-
<p class="tw-font-medium">{{ order.number }}</p>
|
|
110
|
-
<p class="tw-text-sm tw-text-gray-500">{{ order.status }}</p>
|
|
111
|
-
</div>
|
|
112
|
-
<p v-if="!pendingOrders.length" class="tw-text-gray-400">No pending orders</p>
|
|
113
|
-
</div>
|
|
114
|
-
</template>
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Recipe: Widget That Updates on Blade Data Change
|
|
118
|
-
|
|
119
|
-
```vue
|
|
120
|
-
<script setup lang="ts">
|
|
121
|
-
import { useWidget } from '@vc-shell/framework';
|
|
122
|
-
import { inject, watch, ref, computed } from 'vue';
|
|
123
|
-
|
|
124
|
-
const { setTrigger } = useWidget();
|
|
125
|
-
|
|
126
|
-
// Inject blade data from parent
|
|
127
|
-
const bladeData = inject('bladeData') as Ref<{ orderId: string }>;
|
|
128
|
-
const comments = ref<Comment[]>([]);
|
|
129
|
-
|
|
130
|
-
async function loadComments(orderId: string) {
|
|
131
|
-
comments.value = await commentApi.getByOrder(orderId);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Reload when blade data changes
|
|
135
|
-
watch(() => bladeData.value.orderId, (id) => {
|
|
136
|
-
if (id) loadComments(id);
|
|
137
|
-
}, { immediate: true });
|
|
138
|
-
|
|
139
|
-
setTrigger({
|
|
140
|
-
badge: computed(() => comments.value.length),
|
|
141
|
-
icon: 'fas fa-comments',
|
|
142
|
-
title: 'Comments',
|
|
143
|
-
});
|
|
144
|
-
</script>
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
## Tips
|
|
148
|
-
|
|
149
|
-
- **Must be inside `WidgetProvider`.** This composable relies on `WidgetIdKey` being injected by `WidgetProvider`. If called outside that context, it throws an `InjectionError`. The `WidgetProvider` component is automatically rendered by the blade widget system.
|
|
150
|
-
- **`setTrigger` can be called multiple times.** Each call replaces the previous trigger contract. However, since `badge` is reactive, you typically only need to call `setTrigger` once during setup.
|
|
151
|
-
- **Badge accepts both numbers and strings.** You can use `computed(() => 3)` for a numeric badge or `computed(() => '!')` for a text indicator.
|
|
152
|
-
- **Prefer headless widgets for new code.** The `useBladeWidgets()` composable supports a headless pattern where widgets are defined declaratively without a separate SFC. This is simpler and avoids the WidgetProvider ceremony.
|
|
153
|
-
|
|
154
|
-
## Related
|
|
155
|
-
|
|
156
|
-
- [useWidgets](../useWidgets/useWidgets.docs.md) -- blade-level widget service access
|
|
157
|
-
- [useBladeWidgets](../useBladeWidgets/) -- lifecycle-managed widget registration for blades (preferred API for new code)
|
|
158
|
-
- `WidgetProvider` -- the component that provides the `WidgetIdKey` injection
|
|
159
|
-
- `framework/injection-keys.ts` -- defines `WidgetIdKey` and `WidgetServiceKey`
|