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

feat(ffe-icons): lagrer alle relevante varianter av symbols som svg #1737

Closed
wants to merge 4 commits into from
Closed
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/update-ffe-icons.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: update ffe-icons svgs

on:
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18

- name: Install dependencies
run: npm ci

- name: Build icons
run: npm run build:icons
working-directory: packages/ffe-icons

- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'fix(ffe-icons): oppdater ikon SVGer'
title: 'feat(ffe-icons): oppdater ikon SVGer'
body: "Oppdaterer SVG'ene i ffe-icons til å passe med de som er tilgjengelige i Material Symbols"
branch: 'auto-update-icons'
7 changes: 7 additions & 0 deletions component-overview/examples/icons/Icon-ariahidden.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Icon } from '@sb1/ffe-icons-react';
import batteryIcon from '@sb1/ffe-icons/icons/300/filled/sm/battery_90.svg';
() => {

return <Icon filePath={batteryIcon} size="sm" />;

}
6 changes: 6 additions & 0 deletions component-overview/examples/icons/Icon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Icon } from '@sb1/ffe-icons-react';
import batteryIcon from '@sb1/ffe-icons/icons/300/filled/sm/battery_90.svg';

() => {
return <Icon filePath={batteryIcon} size="sm" ariaLabel="Batteri" />;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { InfoMessage } from '@sb1/ffe-message-box-react';
import { Paragraph } from '@sb1/ffe-core-react';
import { HandlevognIkon } from '@sb1/ffe-icons-react';
import Symbol from '@sb1/ffe-symbols-react';

<InfoMessage title="Handlevognen din er tom" icon={<HandlevognIkon title="Handlevogn, ikon" />}>
<InfoMessage title="Handlevognen din er tom" icon={<Symbol ariaLabel="Handlevogn" icon="shopping_cart" />}>
<Paragraph>Du har ingenting i handlevognen din.</Paragraph>
</InfoMessage>
7 changes: 2 additions & 5 deletions component-overview/examples/typography/LinkIcon.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { LinkIcon } from '@sb1/ffe-core-react';
import { SnakkebobleIkon } from '@sb1/ffe-icons-react';
import Symbol from '@sb1/ffe-symbols-react';

<LinkIcon href="https://www.sparebank1.no">
<SnakkebobleIkon
title="Snakk med oss"
style={{ height: '80px' }}
/>
<Symbol ariaLabel="chat" icon="chat" />
</LinkIcon>
1 change: 1 addition & 0 deletions component-overview/src/style.less
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
@import '@sb1/ffe-system-message/less/ffe-system-message';
@import '@sb1/ffe-tables/less/tables';
@import '@sb1/ffe-tabs/less/tabs';
@import '@sb1/ffe-icons/less/ffe-icons';
5 changes: 3 additions & 2 deletions packages/ffe-icons-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"files": [
"lib",
"types",
"es"
"es",
"src"
],
"main": "lib/index.js",
"types": "types/index.d.ts",
Expand All @@ -18,7 +19,7 @@
"url": "ssh://[email protected]:SpareBank1/designsystem.git"
},
"scripts": {
"build": "node scripts/build.js && ffe-buildtool babel --copy-typedef=gen-src/index.d.ts gen-src",
"build": "ffe-buildtool babel",
"clean": "rimraf gen-src lib es types",
"lint": "eslint scripts",
"test": "eslint gen-src"
Expand Down
5 changes: 4 additions & 1 deletion packages/ffe-icons-react/scripts/lib/readIconFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ const caseUtil = require('case');
module.exports = files =>
files.reduce((acc, filePath) => {
const fileName = path.basename(filePath, '.svg');
const safeFileName = !isNaN(fileName.charAt(0))
? `ffe${fileName}`
: fileName;

acc.push({
fileName,
iconName: caseUtil.pascal(fileName),
iconName: caseUtil.pascal(safeFileName),
svg: svg2jsxStr(fs.readFileSync(filePath, 'utf-8')),
});

Expand Down
35 changes: 35 additions & 0 deletions packages/ffe-icons-react/src/Icon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import classNames from 'classnames';
import { string, oneOf } from 'prop-types';

const Icon = props => {
const { filePath, className, ariaLabel, size, ...rest } = props;

return (
<span
role="img"
aria-label={ariaLabel}
aria-hidden={!ariaLabel}
className={classNames('ffe-icons', `ffe-icons--${size}`, className)}
style={{ maskImage: `url(${filePath})` }}
{...rest}
/>
);
};

Icon.defaultProps = {
size: 'md',
};

Icon.propTypes = {
/** The path to the svg-file */
filePath: string.isRequired,
/** Additional classnames */
className: string,
/** Aria label, if ariaLabel is hull it'll sett aria-hidden to true */
ariaLabel: string,
/** Size of the container around the icon */
size: oneOf(['sm', 'md', 'lg', 'xl']),
};

export default Icon;
12 changes: 12 additions & 0 deletions packages/ffe-icons-react/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from 'react';

export interface IconProps {
filePath: string;
className?: string;
ariaLabel?: string;
size: 'sm' | 'md' | 'lg' | 'xl';
}

declare class Icon extends React.Component<IconProps, any> {}

export default Icon;
3 changes: 3 additions & 0 deletions packages/ffe-icons-react/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Icon from './Icon';

export { Icon };
106 changes: 47 additions & 59 deletions packages/ffe-icons/bin/build.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,48 @@
#!/usr/bin/env node
'use strict'; // eslint-disable-line strict

const fs = require('fs');
const path = require('path');
const svgstore = require('svgstore');
const mkdirp = require('mkdirp');

const ICONS_PATH = path.join(__dirname, '..', 'icons');

// convenience to avoid having file extension in config
const appendSvgExtension = icons =>
icons.map(name => (name.endsWith('.svg') ? name : `${name}.svg`));

const options = require('yargs')
.config('opts')
.options({
icons: {
default: '**/*.svg',
type: 'array',
coerce: appendSvgExtension,
},
projectIcons: {
type: 'array',
coerce: appendSvgExtension,
},
dest: {
default: 'dist',
normalize: true,
coerce: path.resolve,
},
}).argv;

const matchesIcon = icons =>
icons.includes('*.svg') || icons.includes('**/*.svg')
? () => true
: fileName => icons.includes(path.basename(fileName));

const sprite = svgstore();

fs.readdirSync(ICONS_PATH)
.filter(fileName => fileName.match(/\.svg$/))
.filter(matchesIcon(options.icons))
.forEach(fileName => {
const iconPath = path.join(ICONS_PATH, fileName);
const iconName = path.basename(fileName, '.svg');
sprite.add(iconName, fs.readFileSync(iconPath), 'utf-8');
});

if (options.projectIcons) {
options.projectIcons.forEach(fileName => {
const iconPath = path.join(fileName);
const iconName = path.basename(fileName, '.svg');
sprite.add(iconName, fs.readFileSync(iconPath), 'utf-8');
});
}

mkdirp.sync(options.dest);

fs.writeFileSync(path.join(options.dest, 'ffe-icons.svg'), sprite.toString());
const fs = require('fs');
const { makedirs } = require('./utils');
const { getIconNames } = require('./getIconNames');
const { getDownloads, downloadAll } = require('./downloadSvgs');
const {
createListOfRemovedIcons,
deleteRemovedIconsFiles,
} = require('./deleteSvg');

(async () => {
const weights = [300, 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 = [];

for (const weight of weights) {
for (const fillValue of fill) {
const type = fillValue === 0 ? 'filled' : 'open';
for (const size of sizes) {
const dirPath = path.resolve(
__dirname,
`../icons/${weight}/${type}/${size.name}`,
);
if (!fs.existsSync(dirPath)) {
await makedirs(dirPath);
}
if (listOfRemovedIcons.length > 0) {
await deleteRemovedIconsFiles(listOfRemovedIcons, dirPath);
}
downloads = downloads.concat(
getDownloads(iconNames, weight, fillValue, size, dirPath),
);
}
}
}
console.log('Downloading SVG files...');
await downloadAll(downloads);
console.log('All done!');
})();
49 changes: 49 additions & 0 deletions packages/ffe-icons/bin/deleteSvg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const fs = require('fs/promises');
const path = require('path');

/* Function: createListOfRemovedIcons
Creates and returns an array of all the filenames of svg-files that exist,
but are no longer mentioned in the Material Symbols Codepoints.

Since we know all the different subfolder / variations of the icons contain the same iconnames,
we only need to check 1 folder.
*/
const createListOfRemovedIcons = async iconNames => {
const directory = path.resolve(__dirname, '../icons/300/filled/lg');
try {
await fs.access(directory);
const filesInDir = await fs.readdir(directory);
const removedIcons = filesInDir.filter(
fileName => !iconNames.includes(fileName.replace('.svg', '')),
);
return removedIcons;
} catch (err) {
console.log('Directory does not exist in check for removed icons');
return [];
}
};

/* Function: deleteSvgFile
Does the actual deleting of the file
*/
const deleteSvgFile = async fileName => {
try {
await fs.unlink(fileName);
console.log(`Deleted file ${fileName}`);
} catch (err) {
console.error(`Failed to delete file ${fileName}: ${err}`);
}
};

/* Function: deleteRemovedIconsFiles
Loop through the list of fileNames that should be deleted in a specific directory
and call the delete function.
*/
const deleteRemovedIconsFiles = async (listOfRemovedIcons, directory) => {
for (const fileName of listOfRemovedIcons) {
const filePath = path.join(directory, fileName);
await deleteSvgFile(filePath);
}
};

module.exports = { createListOfRemovedIcons, deleteRemovedIconsFiles };
78 changes: 78 additions & 0 deletions packages/ffe-icons/bin/downloadSvgs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const { apply, fileExists } = require('./utils');
const path = require('path');
const fs = require('fs/promises');

/* Function: generateDownloadUrl
Takes the icon name, fill, weight and size, and generate the url to download the svg from.
*/
const generateDownloadUrl = (iconName, fill, weight, size) => {
let style = `wght${weight}${fill === 0 ? '' : 'fill1'}`;
if (fill === 0 && weight === 400) {
style = 'default'; // Can technically be removed since we're not supporting the standard value of 400 font weight. But keeping it in to make sure nothing break if we decide to add it later
}
return `https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsrounded/${iconName}/${style}/${size}px.svg`;
};

/* Function: getDownloads
Returns an array for each variation of the icons with the download url, filename and filepath.
The array is later used to know what to download and where to save the files.
*/
const getDownloads = (iconNames, weight, fill, size, dirPath) => {
const downloads = [];
if (!iconNames || !weight || !size || fill === undefined) {
throw new Error('iconNames, weight, fill or size is not provided');
}
for (const icon of iconNames) {
const safeIconName = icon; // Fix for icons that has a number as the first character - which is not valid const name
downloads.push({
url: generateDownloadUrl(safeIconName, fill, weight, size.opsz),
fileName: `${icon}.svg`,
filePath: dirPath,
});
}
return downloads;
};

/* Function: download
Does the actual downloading of the file
*/
const download = async downloadElement => {
const { url, fileName, filePath } = downloadElement;
const fileLocation = path.resolve(__dirname, `${filePath}/${fileName}`);
try {
console.log(`Downloading ${fileLocation}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.text();
await fs.writeFile(fileLocation, data);
} catch (error) {
console.error(
`Failed to download file from ${url} to ${fileLocation}. Error: ${error.message}`,
);
}
};

/* Function: downloadAll
Takes all the downloads, and apply the download function to each of them.
Also let you set if you want to ignore existing files, this is on by default.
*/
const downloadAll = async (downloads, { ignoreExisting = true } = {}) => {
let allDownloads = [];
if (ignoreExisting) {
for (const file of downloads) {
const fileAlreadyExists = await fileExists(
`${file.filePath}/${file.fileName}`,
);
if (!fileAlreadyExists) {
allDownloads.push(file);
}
}
} else {
allDownloads = downloads;
}
await apply(download, allDownloads);
};

module.exports = { getDownloads, downloadAll };
Loading
Loading