feat(web): adopt vuesax icons and shared skeleton loading
Replace the inline SVG icon set with the vuesax linear pack, add skeleton primitives for loading states, and keep page scroll inside the app shell. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const src = 'icons (1).json';
|
||||
const destDir = 'apps/web/src/icons';
|
||||
const dest = path.join(destDir, 'vuesax-linear.json');
|
||||
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
const raw = JSON.parse(fs.readFileSync(src, 'utf8'));
|
||||
|
||||
/** Normalize hardcoded stroke/fill hex to currentColor so theme/CSS color works. */
|
||||
function normalizeSvg(svg) {
|
||||
return String(svg)
|
||||
.replace(/\sstroke="#[0-9A-Fa-f]{3,8}"/g, ' stroke="currentColor"')
|
||||
.replace(/\sfill="#[0-9A-Fa-f]{3,8}"/g, ' fill="currentColor"')
|
||||
.replace(/\n+/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
const out = {};
|
||||
for (const [key, svg] of Object.entries(raw)) {
|
||||
const short = key.startsWith('vuesax-linear:')
|
||||
? key.slice('vuesax-linear:'.length)
|
||||
: key;
|
||||
out[short] = normalizeSvg(svg);
|
||||
}
|
||||
|
||||
fs.writeFileSync(dest, JSON.stringify(out));
|
||||
console.log('wrote', dest, 'icons', Object.keys(out).length);
|
||||
|
||||
// Verify mapping coverage for planned aliases
|
||||
const keys = new Set(Object.keys(out));
|
||||
const needed = [
|
||||
'add', 'home', 'user', 'profile-2user', 'search-normal', 'search-status',
|
||||
'setting-2', 'setting', 'calendar', 'calendar-1', 'document', 'document-text',
|
||||
'briefcase', 'tick-circle', 'tick-square', 'close-circle', 'close-square',
|
||||
'trash', 'edit', 'edit-2', 'eye', 'eye-slash', 'lock', 'logout', 'notification',
|
||||
'bell', 'message', 'messages-1', 'sms', 'call', 'chart', 'chart-2', 'chart-1',
|
||||
'arrow-right', 'arrow-left', 'arrow-up', 'arrow-down', 'arrow-down-1',
|
||||
'filter', 'funnel', 'star', 'gift', 'cup', 'medal', 'crown', 'warning-2',
|
||||
'info-circle', 'more', 'more-circle', 'menu', 'grid-1', 'grid-2', 'element-3',
|
||||
'layer', 'refresh', 'refresh-2', 'reload', 'send-2', 'send', 'paperclip',
|
||||
'attach-circle', 'camera', 'gallery', 'microphone', 'microphone-2', 'stop',
|
||||
'minus', 'dollar-circle', 'money', 'wallet', 'wallet-2', 'bank', 'card',
|
||||
'building', 'buildings', 'flag', 'location', 'map', 'printer', 'export',
|
||||
'import', 'document-download', 'document-upload', 'link', 'link-2', 'copy',
|
||||
'save-2', 'archive', 'box', 'box-1', 'shopping-bag', 'shopping-cart',
|
||||
'truck', 'car', 'security', 'shield', 'shield-tick', 'flash', 'lamp-on',
|
||||
'lamp-charge', 'emoji-happy', 'emoji-normal', 'robot', 'cpu', 'task',
|
||||
'task-square', 'clipboard', 'clipboard-text', 'note', 'note-1', 'book',
|
||||
'bookmark', 'tag', 'tag-2', 'hashtag', 'percentage-square', 'discount-shape',
|
||||
'receipt', 'receipt-2', 'receipt-item', 'profile-add', 'user-add', 'people',
|
||||
'category', 'element-4', 'slider', 'slider-horizontal', 'slider-vertical',
|
||||
'sound', 'volume-high', 'volume-low', 'music', 'play', 'pause', 'record',
|
||||
'fullscreen', 'maximize', 'maximize-3', 'maximize-4', 'scan', 'scan-barcode',
|
||||
'zoom', 'search-zoom-in', 'search-zoom-out', 'arrow-swap-horizontal',
|
||||
'arrange-square', 'row-vertical', 'row-horizontal', 'text', 'textalign-left',
|
||||
'text-bold', 'text-italic', 'text-underline', 'quote-down', 'quote-up',
|
||||
'like', 'dislike', 'heart', 'lovely', 'danger', 'info-circle',
|
||||
'arrow-circle-right', 'arrow-circle-left', 'arrow-circle-up', 'arrow-circle-down',
|
||||
];
|
||||
const missing = needed.filter((k) => !keys.has(k));
|
||||
console.log('missing samples', missing.length, missing.slice(0, 40).join(', '));
|
||||
console.log('search*', [...keys].filter((k) => k.includes('search')).slice(0, 20).join(', '));
|
||||
console.log('setting*', [...keys].filter((k) => k.includes('setting')).slice(0, 20).join(', '));
|
||||
console.log('user*', [...keys].filter((k) => k.startsWith('user') || k.includes('profile')).slice(0, 25).join(', '));
|
||||
Reference in New Issue
Block a user