@vc-shell/vc-app-skill 2.0.0-alpha.25 → 2.0.0-alpha.27
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/ui/components/molecules/vc-pagination/vc-pagination.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/organisms/vc-app/vc-app.docs.md +48 -5
- package/runtime/knowledge/docs/ui/components/organisms/{vc-table → vc-data-table}/vc-data-table.docs.md +2 -2
- /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.27](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.26...v2.0.0-alpha.27) (2026-03-25)
|
|
2
|
+
|
|
3
|
+
**Note:** Version bump only for package @vc-shell/vc-app-skill
|
|
4
|
+
|
|
5
|
+
# [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)
|
|
6
|
+
|
|
7
|
+
**Note:** Version bump only for package @vc-shell/vc-app-skill
|
|
8
|
+
|
|
1
9
|
# [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)
|
|
2
10
|
|
|
3
11
|
**Note:** Version bump only for package @vc-shell/vc-app-skill
|
|
@@ -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.27",
|
|
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.27
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Synced from framework at commit
|
|
1
|
+
Synced from framework at commit 8dddac30c on 2026-03-25T15:16:59.790Z
|
|
@@ -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
|
|
@@ -44,18 +44,20 @@ const user = reactive({ name: "John", role: "Admin" });
|
|
|
44
44
|
| `name` | `string` | -- | Current user display name. |
|
|
45
45
|
| `role` | `string` | -- | Current user role label. |
|
|
46
46
|
| `disableMenu` | `boolean` | `false` | Hide navigation menu items. |
|
|
47
|
-
| `
|
|
47
|
+
| `disableAppHub` | `boolean` | `false` | Hide the Applications section inside the App Hub. |
|
|
48
|
+
| `showSearch` | `boolean` | `false` | Show a search input in the sidebar that filters menu items by title. |
|
|
49
|
+
| `searchPlaceholder` | `string` | `"Search keyword"` | Placeholder text for the sidebar search input. Falls back to i18n key `SHELL.SIDEBAR.SEARCH_PLACEHOLDER`. |
|
|
48
50
|
|
|
49
51
|
## Slots
|
|
50
52
|
|
|
51
53
|
| Slot | Props | Description |
|
|
52
54
|
|------|-------|-------------|
|
|
53
55
|
| `layout` | `{ isMobile, sidebar, appsList, switchApp, openRoot, handleMenuItemClick }` | Override the entire layout (sidebar + navigation). |
|
|
54
|
-
| `menu` | `{ expanded, onItemClick }` | Custom navigation menu. |
|
|
56
|
+
| `menu` | `{ expanded, onItemClick, searchQuery }` | Custom navigation menu. `searchQuery` contains the current search input value (empty string when search is inactive). |
|
|
55
57
|
| `sidebar-header` | `{ logo, expanded, isMobile }` | Custom sidebar header. |
|
|
56
58
|
| `sidebar-footer` | `{ avatar, name, role }` | Custom sidebar footer (user info). |
|
|
57
59
|
| `workspace` | `{ isAuthenticated }` | Override the blade navigation workspace. |
|
|
58
|
-
| `app-
|
|
60
|
+
| `app-hub` | `{ appsList, switchApp }` | Custom content for the Applications section of the App Hub. |
|
|
59
61
|
|
|
60
62
|
## Architecture
|
|
61
63
|
|
|
@@ -72,13 +74,54 @@ VcApp orchestrates several internal systems:
|
|
|
72
74
|
|
|
73
75
|
On desktop viewports, VcApp renders a collapsible sidebar on the left with navigation menu items, user info in the footer, and the blade workspace on the right. On mobile viewports, the sidebar is replaced by a top bar with a hamburger menu that opens a slide-over navigation panel.
|
|
74
76
|
|
|
77
|
+
### Sidebar Menu Search
|
|
78
|
+
|
|
79
|
+
When `showSearch` is `true`, a search input appears at the top of the sidebar (desktop) or mobile navigation panel. It filters menu items in real time (300ms debounce) by matching the search query against translated item titles:
|
|
80
|
+
|
|
81
|
+
- **Standalone items** — shown if their title contains the query.
|
|
82
|
+
- **Group items** — if the group title matches, all accessible children are shown. Otherwise, only children whose titles match are displayed.
|
|
83
|
+
- The search query is automatically cleared when a menu item is clicked or when the sidebar collapses.
|
|
84
|
+
|
|
85
|
+
On desktop, the search input is visible only when the sidebar is expanded (pinned). On mobile, it appears inside the slide-out navigation panel.
|
|
86
|
+
|
|
87
|
+
If you use the `menu` slot to provide a custom menu, the `searchQuery` prop is passed to your slot so you can implement your own filtering logic.
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<template>
|
|
91
|
+
<VcApp :is-ready="true" logo="/logo.svg" title="Admin" show-search search-placeholder="Find a module...">
|
|
92
|
+
<!-- Default menu with built-in filtering — no extra code needed -->
|
|
93
|
+
</VcApp>
|
|
94
|
+
</template>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Custom Menu with Search
|
|
98
|
+
|
|
99
|
+
```vue
|
|
100
|
+
<template>
|
|
101
|
+
<VcApp :is-ready="true" logo="/logo.svg" title="Admin" show-search>
|
|
102
|
+
<template #menu="{ expanded, onItemClick, searchQuery }">
|
|
103
|
+
<MyCustomMenu
|
|
104
|
+
:expanded="expanded"
|
|
105
|
+
:filter="searchQuery"
|
|
106
|
+
@select="onItemClick"
|
|
107
|
+
/>
|
|
108
|
+
</template>
|
|
109
|
+
</VcApp>
|
|
110
|
+
</template>
|
|
111
|
+
```
|
|
112
|
+
|
|
75
113
|
### Dynamic Module Registration
|
|
76
114
|
|
|
77
115
|
Modules registered via `useDynamicModules()` are loaded at runtime. Each module can contribute menu items, blades, toolbar actions, settings pages, and dashboard widgets. VcApp handles module loading errors gracefully by displaying notification toasts.
|
|
78
116
|
|
|
79
|
-
###
|
|
117
|
+
### App Hub
|
|
118
|
+
|
|
119
|
+
The App Hub is a popover panel (desktop) or a swipeable tab (mobile) that combines two sections:
|
|
120
|
+
|
|
121
|
+
- **Applications** — tile grid of registered apps (e.g., Vendor Portal, Marketplace Admin). Clicking an app switches context without a full page reload. This section can be hidden with `disableAppHub` or customized via the `app-hub` slot. The list is searchable via a built-in search input inside the hub.
|
|
122
|
+
- **Widgets** — registered app bar widgets (notifications, background tasks, etc.). Clicking a widget expands its content inline (desktop) or navigates to its panel (mobile). Widgets are registered via `useAppBarWidget()` and can display badges for unread counts.
|
|
80
123
|
|
|
81
|
-
|
|
124
|
+
On desktop, the App Hub opens from the sidebar header menu button (`AppHubPopover`). On mobile, it appears as a second tab ("Hub") in the slide-out navigation panel — users can swipe between Menu and Hub tabs.
|
|
82
125
|
|
|
83
126
|
## Recipe: Minimal App Setup
|
|
84
127
|
|
|
@@ -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
|
|