Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nye størrelser på ikoner, lg går fra 40px til 32px og xl går fra 48 til 40 #2477

Merged
merged 1 commit into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
161 changes: 117 additions & 44 deletions packages/ffe-icons/bin/build.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,125 @@
const path = require('path');
const fs = require('fs');
const fs = require('fs/promises');
const { makedirs } = require('./utils');
const { getIconNames } = require('./getIconNames');
const { getDownloads, downloadAll } = require('./downloadSvgs');
const {
createListOfRemovedIcons,
deleteRemovedIconsFiles,
} = require('./deleteSvg');

(async () => {
const weights = [300, 400, 500];
const sizes = [
{ name: 'sm', opsz: 20 },
{ name: 'md', opsz: 24 },
{ name: 'lg', opsz: 40 },
{ name: 'xl', opsz: 48 },
];
const fill = [0, 1];

const iconNames = await getIconNames();
const listOfRemovedIcons = await createListOfRemovedIcons(iconNames);
let downloads = [];
// eslint-disable-next-line no-unused-vars
for (const weight of weights) {
// eslint-disable-next-line no-unused-vars
for (const fillValue of fill) {
const type = fillValue === 1 ? 'filled' : 'open';

// eslint-disable-next-line no-unused-vars
for (const size of sizes) {
let folderPath = `../icons/${type}/${weight}/${size.name}`;
if (type === 'filled') {
folderPath = `../icons/${type}/${size.name}`;
}
const dirPath = path.resolve(__dirname, folderPath);
if (!fs.existsSync(dirPath)) {
await makedirs(dirPath);
}
if (listOfRemovedIcons.length > 0) {
await deleteRemovedIconsFiles(listOfRemovedIcons, dirPath);
}
downloads = downloads.concat(
getDownloads(iconNames, weight, fillValue, size, dirPath),
const { resizeAllSvgs } = require('./resizeSvg');

/**
* Configuration for icon sizes and their target dimensions
* @type {Array<{name: string, size: number, download: boolean}>}
*/
const SIZES = [
{ name: 'sm', size: 20, download: true },
{ name: 'md', size: 24, download: true },
{ name: 'lg', size: 32, download: false },
{ name: 'xl', size: 40, download: true },
];

const WEIGHTS = [300, 400, 500];
const FILLS = [0, 1]; // 0 = outlined, 1 = filled

/**
* Creates the necessary directory structure for icons
* @param {string} type - 'filled' or 'open'
* @param {number} weight - Font weight
* @param {string} size - Size name
* @returns {Promise<string>} - Path to created directory
*/
async function createIconDirectory(type, weight, size) {
const basePath = path.resolve(__dirname, '../icons');
const dirPath =
type === 'filled'
? path.join(basePath, type, size)
: path.join(basePath, type, weight.toString(), size);

await makedirs(dirPath);
return dirPath;
}

/**
* Downloads and processes icons for a specific configuration
* @param {Array<string>} iconNames - List of icon names to process
* @param {string} type - 'filled' or 'open'
* @param {number} weight - Font weight
* @param {number} fillValue - Fill state (0 or 1)
*/
async function processIconSet(iconNames, type, weight, fillValue) {
console.log(`\n📦 Processing ${type} icons (weight: ${weight})`);

// Download directly for sizes marked for download
const downloadSizes = SIZES.filter(s => s.download);
for (const sizeConfig of downloadSizes) {
const dirPath = await createIconDirectory(
type,
weight,
sizeConfig.name,
);

const downloads = getDownloads(
iconNames,
weight,
fillValue,
{ name: sizeConfig.name, opsz: sizeConfig.size },
dirPath,
);

console.log(`\n⬇️ Downloading ${sizeConfig.size}px SVG files...`);
await downloadAll(downloads);
}

// Create 32px version from 40px
const xlSize = SIZES.find(s => s.name === 'xl');
const lgSize = SIZES.find(s => s.name === 'lg');

if (xlSize && lgSize) {
const xlDirPath = await createIconDirectory(type, weight, xlSize.name);
const lgDirPath = await createIconDirectory(type, weight, lgSize.name);

// Copy 40px files to 32px directory
const files = await fs.readdir(xlDirPath);
for (const file of files) {
if (file.endsWith('.svg')) {
await fs.copyFile(
path.join(xlDirPath, file),
path.join(lgDirPath, file),
);
}
}

// Resize 40px to 32px
console.log('\n📐 Creating 32px version from 40px...');
await resizeAllSvgs(lgDirPath, lgSize.size, {
continueOnError: true,
verbose: true,
});
}
}

/**
* Main build function
*/
async function build() {
try {
console.log('🚀 Starting optimized icon build process...');

console.log('📋 Fetching icon names...');
const iconNames = await getIconNames();
console.log(`✓ Found ${iconNames.length} icons`);

for (const weight of WEIGHTS) {
for (const fillValue of FILLS) {
const type = fillValue === 1 ? 'filled' : 'open';
await processIconSet(iconNames, type, weight, fillValue);
}
}

console.log('\n✨ Build completed successfully!');
} catch (error) {
console.error('\n❌ Build failed:', error);
process.exit(1);
}
console.log('Downloading SVG files...');
await downloadAll(downloads);
console.log('All done!');
})();
}

// Run the build process
build();
224 changes: 224 additions & 0 deletions packages/ffe-icons/bin/resizeSvg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
const fs = require('fs/promises');
const path = require('path');

/**
* Validates and cleans SVG content
* @param {string} content - SVG content to validate
* @returns {string} - Cleaned SVG content
*/
function validateAndCleanSvg(content) {
if (!content || typeof content !== 'string') {
throw new Error('SVG content must be a non-empty string');
}

// Remove BOM and whitespace
let cleaned = content.trim().replace(/^\uFEFF/, '');

// Handle XML declaration
if (cleaned.startsWith('<?xml')) {
const xmlEnd = cleaned.indexOf('?>');
if (xmlEnd !== -1) {
cleaned = cleaned.slice(xmlEnd + 2).trim();
}
}

// Handle potential doctype
if (cleaned.startsWith('<!DOCTYPE')) {
const doctypeEnd = cleaned.indexOf('>');
if (doctypeEnd !== -1) {
cleaned = cleaned.slice(doctypeEnd + 1).trim();
}
}

// Find SVG tag
const svgStart = cleaned.indexOf('<svg');
if (svgStart === -1) {
throw new Error(`File does not contain an SVG tag`);
}

// Only keep from SVG tag onwards
return cleaned.slice(svgStart);
}

/**
* Updates SVG size while preserving viewBox and aspect ratio
* @param {Buffer|string} svgContent - Original SVG content
* @param {number} size - Target size in pixels
* @returns {string} - Modified SVG content
*/
function updateSvgSize(svgContent, size) {
try {
// Convert Buffer to string if needed
const content = Buffer.isBuffer(svgContent)
? svgContent.toString('utf8')
: svgContent;

// Clean and validate content
const cleanedContent = validateAndCleanSvg(content);

// Find SVG opening tag and its attributes
const svgTagMatch = cleanedContent.match(/<svg([^>]*)>/);
if (!svgTagMatch) {
throw new Error('Could not parse SVG tag');
}

const originalAttrs = svgTagMatch[1];

// Extract existing attributes
const attrs = new Map();
const attrRegex = /(\w+)=["']([^"']+)["']/g;
let match;

// eslint-disable-next-line no-cond-assign
while ((match = attrRegex.exec(originalAttrs)) !== null) {
const [, name, value] = match;
if (name !== 'width' && name !== 'height') {
attrs.set(name, value);
}
}

// Handle viewBox
if (!attrs.has('viewBox')) {
// Try to get original dimensions
const width = originalAttrs.match(/width=["'](\d+)/)?.[1] || size;
const height = originalAttrs.match(/height=["'](\d+)/)?.[1] || size;
attrs.set('viewBox', `0 0 ${width} ${height}`);
}

// Set size attributes
attrs.set('width', size);
attrs.set('height', size);

// Ensure xmlns is present
if (!attrs.has('xmlns')) {
attrs.set('xmlns', 'http://www.w3.org/2000/svg');
}

// Build new SVG tag
const newAttrs = Array.from(attrs.entries())
.map(([name, value]) => `${name}="${value}"`)
.join(' ');

const newTag = `<svg ${newAttrs}>`;

// Replace original tag
return cleanedContent.replace(/<svg[^>]*>/, newTag);
} catch (error) {
throw new Error(`SVG processing failed: ${error.message}`);
}
}

/**
* Processes a single SVG file
* @param {string} filePath - Path to SVG file
* @param {number} targetSize - Desired size in pixels
* @param {Object} options - Processing options
* @returns {Promise<boolean>} - Success status
*/
async function resizeSvg(filePath, targetSize, options = {}) {
const { skipErrors = false, verbose = false } = options;

try {
const content = await fs.readFile(filePath);
const resizedContent = updateSvgSize(content, targetSize);
await fs.writeFile(filePath, resizedContent);

if (verbose) {
console.log(
`✓ Resized ${path.basename(filePath)} to ${targetSize}px`,
);
}
return true;
} catch (error) {
const message = `Failed to process ${path.basename(filePath)}: ${error.message}`;
if (skipErrors) {
console.error(`⚠️ ${message}`);
return false;
}
throw new Error(message);
}
}

/**
* Processes all SVGs in a directory
* @param {string} directory - Directory containing SVGs
* @param {number} targetSize - Desired size in pixels
* @param {Object} options - Processing options
*/
async function resizeAllSvgs(directory, targetSize, options = {}) {
const {
continueOnError = true,
verbose = false,
concurrency = 10,
} = options;

try {
// Ensure directory exists
await fs.access(directory);

// Get SVG files
const files = await fs.readdir(directory);
const svgFiles = files.filter(file =>
file.toLowerCase().endsWith('.svg'),
);

if (svgFiles.length === 0) {
if (verbose) {
console.log(`No SVG files found in ${directory}`);
}
return;
}

if (verbose) {
console.log(`Processing ${svgFiles.length} SVG files...`);
}

// Process files in batches
const batches = [];
for (let i = 0; i < svgFiles.length; i += concurrency) {
const batch = svgFiles.slice(i, i + concurrency);
batches.push(batch);
}

let successful = 0;
let failed = 0;

for (const batch of batches) {
const results = await Promise.all(
batch.map(file =>
resizeSvg(path.join(directory, file), targetSize, {
skipErrors: continueOnError,
verbose,
}),
),
);

successful += results.filter(Boolean).length;
failed += results.filter(r => !r).length;
}

if (verbose) {
console.log(`\nProcessing complete:`);
console.log(`✓ Successfully processed: ${successful} files`);
if (failed > 0) {
console.log(`⚠️ Failed to process: ${failed} files`);
}
}

if (failed > 0 && !continueOnError) {
throw new Error(`Failed to process ${failed} files`);
}
} catch (error) {
if (!continueOnError) {
throw new Error(`Directory processing failed: ${error.message}`);
}
console.error(`⚠️ ${error.message}`);
}
}

module.exports = {
resizeSvg,
resizeAllSvgs,
updateSvgSize,
validateAndCleanSvg,
};
2 changes: 1 addition & 1 deletion packages/ffe-icons/icons/filled/lg/10k.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading