diff --git a/packages/react-native-icons/.babelrc b/packages/react-native-icons/.babelrc new file mode 100644 index 0000000000..a02540ef7f --- /dev/null +++ b/packages/react-native-icons/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/preset-typescript"], + "compact": false +} diff --git a/packages/react-native-icons/.npmignore b/packages/react-native-icons/.npmignore new file mode 100644 index 0000000000..b512c09d47 --- /dev/null +++ b/packages/react-native-icons/.npmignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/packages/react-native-icons/README.md b/packages/react-native-icons/README.md new file mode 100644 index 0000000000..32dcbe314d --- /dev/null +++ b/packages/react-native-icons/README.md @@ -0,0 +1,2 @@ +@fluentui/react-native-icons +=== diff --git a/packages/react-native-icons/convert.js b/packages/react-native-icons/convert.js new file mode 100644 index 0000000000..1704d72e79 --- /dev/null +++ b/packages/react-native-icons/convert.js @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const svgr = require("@svgr/core"); +const fs = require("fs"); +const path = require("path"); +const process = require("process"); +const argv = require("yargs").boolean("selector").default("selector", false).argv; +const _ = require("lodash"); + +const SRC_PATH = argv.source; +const DEST_PATH = argv.dest; + +const TSX_EXTENSION = '.tsx' + +if (!SRC_PATH) { + throw new Error("Icon source folder not specified by --source"); +} +if (!DEST_PATH) { + throw new Error("Output destination folder not specified by --dest"); +} + +if (!fs.existsSync(DEST_PATH)) { + fs.mkdirSync(DEST_PATH); +} + +processFiles(SRC_PATH, DEST_PATH) + +function processFiles(src, dest) { + /** @type string[] */ + const indexContents = []; + + // make file for resizeable icons + const iconPath = path.join(dest, 'icons') + const iconContents = processFolder(src, dest, true) + + if (fs.existsSync(iconPath)) { + fs.rmSync(iconPath, { recursive: true, force: true } ); + } + fs.mkdirSync(iconPath); + + iconContents.forEach((chunk, i) => { + const chunkFileName = `chunk-${i}` + const chunkPath = path.resolve(iconPath, `${chunkFileName}.tsx`); + indexContents.push(`export * from './icons/${chunkFileName}'`); + fs.writeFileSync(chunkPath, chunk, (err) => { + if (err) throw err; + }); + }); + + // make file for sized icons + const sizedIconPath = path.join(dest, 'sizedIcons'); + const sizedIconContents = processFolder(src, dest, false) + if (fs.existsSync(sizedIconPath)) { + fs.rmSync(sizedIconPath, { recursive: true, force: true } ); + } + fs.mkdirSync(sizedIconPath); + + sizedIconContents.forEach((chunk, i) => { + const chunkFileName = `chunk-${i}` + const chunkPath = path.resolve(sizedIconPath, `${chunkFileName}.tsx`); + indexContents.push(`export * from './sizedIcons/${chunkFileName}'`); + fs.writeFileSync(chunkPath, chunk, (err) => { + if (err) throw err; + }); + }); + + const indexPath = path.join(dest, 'index.tsx') + // Finally add the interface definition and then write out the index. + indexContents.push('export { FluentReactNativeIconsProps } from \'./utils/FluentReactNativeIconsProps.types\''); + indexContents.push('export { default as wrapIcon } from \'./utils/wrapIcon\''); + //indexContents.push('export { default as bundleIcon } from \'./utils/bundleIcon\''); + indexContents.push('export * from \'./utils/useIconState\''); + //indexContents.push('export * from \'./utils/constants\''); + + fs.writeFileSync(indexPath, indexContents.join('\n'), (err) => { + if (err) throw err; + }); + +} + +/** + * Process a folder of svg files and convert them to React components, following naming patterns for the FluentUI System Icons + * @param {string} srcPath + * @param {boolean} resizable + * @returns { string [] } - chunked icon files to insert + */ +function processFolder(srcPath, destPath, resizable) { + var files = fs.readdirSync(srcPath) + + // These options will be passed to svgr/core + // See https://react-svgr.com/docs/options/ for more info + var svgrOpts = { + template: fileTemplate, + expandProps: 'start', // HTML attributes/props for things like accessibility can be passed in, and will be expanded on the svg object at the start of the object + svgProps: { style: '{style}'}, // In RN style attribute is used for styling + replaceAttrValues: { '#212121': '{primaryFill}' }, // We are designating primaryFill as the primary color for filling. If not provided, it defaults to null. + typescript: true, + icon: true + } + + var svgrOptsSizedIcons = { + template: fileTemplate, + expandProps: 'start', // HTML attributes/props for things like accessibility can be passed in, and will be expanded on the svg object at the start of the object + svgProps: { style: '{style}'}, // In RN style attribute is used for styling + replaceAttrValues: { '#212121': '{primaryFill}' }, // We are designating primaryFill as the primary color for filling. If not provided, it defaults to null. + typescript: true + } + + /** @type string[] */ + const iconExports = []; + files.forEach(function (file, index) { + var srcFile = path.join(srcPath, file) + if (fs.lstatSync(srcFile).isDirectory()) { + // for now, ignore subdirectories/localization, until we have a plan for handling it + // Will likely involve appending the lang/locale to the end of the friendly name for the unique component name + // var joinedDestPath = path.join(destPath, file) + // if (!fs.existsSync(joinedDestPath)) { + // fs.mkdirSync(joinedDestPath); + // } + // indexContents += processFolder(srcFile, joinedDestPath) + } else { + if(resizable && !file.includes("20")) { + return + } + var iconName = file.substr(0, file.length - 4) // strip '.svg' + iconName = iconName.replace("ic_fluent_", "") // strip ic_fluent_ + iconName = resizable ? iconName.replace("20", "") : iconName + var destFilename = _.camelCase(iconName) // We want them to be camelCase, so access_time would become accessTime here + destFilename = destFilename.replace(destFilename.substring(0, 1), destFilename.substring(0, 1).toUpperCase()) // capitalize the first letter + + var iconContent = fs.readFileSync(srcFile, { encoding: "utf8" }) + + var jsxCode = resizable ? svgr.default.sync(iconContent, svgrOpts, { filePath: file }) : svgr.default.sync(iconContent, svgrOptsSizedIcons, { filePath: file }) + var rnRegex = new RegExp('(<(|\/))(svg|path|rect|g)', 'g') + var rnCode = jsxCode.replace(rnRegex, function(result) { + var charRegex = new RegExp('[a-zA-Z]'); + return result.replace(charRegex, firstSymbol => firstSymbol.toUpperCase()) }); + var jsCode = +` + +const ${destFilename}Icon = (props: FluentReactNativeIconsProps) => { + const { fill: primaryFill = 'currentColor', style } = props; + return ${rnCode}; +} +export const ${destFilename} = /*#__PURE__*/wrapIcon(/*#__PURE__*/${destFilename}Icon, '${destFilename}'); + ` + iconExports.push(jsCode); + } + }); + + // chunk all icons into separate files to keep build reasonably fast + /** @type string[][] */ + const iconChunks = []; + while(iconExports.length > 0) { + iconChunks.push(iconExports.splice(0, 500)); + } + + for(const chunk of iconChunks) { + chunk.unshift(`import { Path, Svg, Rect, G } from 'react-native-svg';`) + chunk.unshift(`import wrapIcon from "../utils/wrapIcon";`) + chunk.unshift(`import { FluentReactNativeIconsProps } from "../utils/FluentReactNativeIconsProps.types";`) + chunk.unshift(`import * as React from "react";`) + } + + /** @type string[] */ + const chunkContent = iconChunks.map(chunk => chunk.join('\n')); + + return chunkContent; +} + +function fileTemplate( + { template }, + opts, + { imports, interfaces, componentName, props, jsx, exports } +) { + const plugins = ['jsx', 'typescript'] + const tpl = template.smart({ plugins }) + + componentName.name = componentName.name.substring(3) + componentName.name = componentName.name.replace('IcFluent', '') + + return jsx; +} \ No newline at end of file diff --git a/packages/react-native-icons/package.json b/packages/react-native-icons/package.json new file mode 100644 index 0000000000..05beb3bfd2 --- /dev/null +++ b/packages/react-native-icons/package.json @@ -0,0 +1,70 @@ +{ + "name": "@fluentui/react-native-icons", + "version": "0.0.1", + "sideEffects": false, + "main": "lib-cjs/index.js", + "module": "lib/index.js", + "typings": "lib/index.d.ts", + "description": "Fluent System Icons are a collection of familiar, friendly, and modern icons from Microsoft.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui-system-icons.git" + }, + "scripts": { + "clean": "find ./src -type f ! -name \"wrapIcon.tsx\" -name \"*.tsx\" -delete", + "cleanSvg": "rm -rf ./intermediate", + "copy": "node ../../importer/generate.js --source=../../assets --dest=./intermediate --extension=svg --target=react", + "convert:svg": "node convert.js --source=./intermediate --dest=./src", + "rollup": "node ./generateRollup.js", + "optimize": "svgo --config svgo.config.js --folder=./intermediate --precision=2", + "unfill": "find ./intermediate -type f -name \"*.svg\" -exec sed -i.bak 's/fill=\"none\"//g' {} \\; && find ./intermediate -type f -name \"*.bak\" -delete", + "build": "npm run copy && npm run convert:svg && npm run cleanSvg && npm run build:esm && npm run build:cjs", + "build:cjs": "tsc --module commonjs --outDir lib-cjs && babel lib-cjs --out-dir lib-cjs", + "build:esm": "tsc && babel lib --out-dir lib" + }, + "devDependencies": { + "@babel/cli": "^7.16.0", + "@babel/core": "^7.16.0", + "@babel/preset-typescript": "^7.16.7", + "@svgr/core": "^5.5.0", + "@types/react": "^17.0.2", + "@types/react-native": "^0.68.0", + "cpy-cli": "^4.1.0", + "glob": "^7.2.0", + "lodash": "^4.17.21", + "mkdirp": "^1.0.4", + "react": "^17.0.1", + "react-native": "^0.68.0", + "react-native-svg": "12.5.0", + "renamer": "^2.0.1", + "svgo": "^2.8.0", + "typescript": "~4.1.0", + "yargs": "^14.0.0" + }, + "dependencies": { + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0", + "react-native": "^0.68.0" + }, + "files": [ + "lib/", + "lib-cjs/" + ], + "exports": { + ".": { + "default": { + "types": "./lib/index.d.ts", + "import": "./lib/index.js", + "require": "./lib-cjs/index.js" + } + }, + "./lib/svg": { + "types": "./lib/index.d.ts", + "import": "./lib/index.js", + "require": "./lib-cjs/index.js" + } + } +} diff --git a/packages/react-native-icons/src/.gitignore b/packages/react-native-icons/src/.gitignore new file mode 100644 index 0000000000..5e449fc15f --- /dev/null +++ b/packages/react-native-icons/src/.gitignore @@ -0,0 +1 @@ +fonts/ \ No newline at end of file diff --git a/packages/react-native-icons/src/custom.d.ts b/packages/react-native-icons/src/custom.d.ts new file mode 100644 index 0000000000..a3da4a991d --- /dev/null +++ b/packages/react-native-icons/src/custom.d.ts @@ -0,0 +1,6 @@ +import 'react-native-svg'; +declare module 'react-native-svg' { + export interface SvgProps { + xmlns?: string; + } +} \ No newline at end of file diff --git a/packages/react-native-icons/src/utils/FluentReactNativeIconsProps.types.ts b/packages/react-native-icons/src/utils/FluentReactNativeIconsProps.types.ts new file mode 100644 index 0000000000..2df5c8cf9f --- /dev/null +++ b/packages/react-native-icons/src/utils/FluentReactNativeIconsProps.types.ts @@ -0,0 +1,9 @@ +import { ImageStyle, StyleProp, TextStyle } from 'react-native'; +import { SvgProps } from 'react-native-svg'; + +export type FluentReactNativeIconsProps = SvgProps & { + primaryFill?: string + style?: StyleProp; + filled?: boolean; + title?: string; +} \ No newline at end of file diff --git a/packages/react-native-icons/src/utils/useIconState.tsx b/packages/react-native-icons/src/utils/useIconState.tsx new file mode 100644 index 0000000000..462bf3e876 --- /dev/null +++ b/packages/react-native-icons/src/utils/useIconState.tsx @@ -0,0 +1,26 @@ +import { FluentReactNativeIconsProps } from "./FluentReactNativeIconsProps.types"; + +export const useIconState = (props: FluentReactNativeIconsProps): Omit => { + const { title, primaryFill = "currentColor", ...rest } = props; + const state = { + ...rest, + title: undefined, + fill: primaryFill + } as Omit; + + // TODO add here styles for RN + //const styles = useRootStyles(); + //state.className = mergeClasses(styles.root, state.className); + + if (title) { + state['acessibilityLabel'] = title; + } + + if (!state['acessibilityLabel'] && !state['accessibilityLabelledBy']) { + state['accessibilityHidden'] = true; + } else { + state['accessibilityRole'] = 'image'; + } + + return state; +}; diff --git a/packages/react-native-icons/src/utils/wrapIcon.tsx b/packages/react-native-icons/src/utils/wrapIcon.tsx new file mode 100644 index 0000000000..562fb48185 --- /dev/null +++ b/packages/react-native-icons/src/utils/wrapIcon.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; +import { FluentReactNativeIconsProps } from "./FluentReactNativeIconsProps.types"; +import { useIconState } from "./useIconState"; + +const wrapIcon = (Icon: (iconProps: FluentReactNativeIconsProps) => JSX.Element, displayName?: string) => { + const WrappedIcon = (props: FluentReactNativeIconsProps) => { + const state = useIconState(props); + return + } + WrappedIcon.displayName = displayName; + return WrappedIcon; +} + +export default wrapIcon; \ No newline at end of file diff --git a/packages/react-native-icons/svgo.config.js b/packages/react-native-icons/svgo.config.js new file mode 100644 index 0000000000..918fd68997 --- /dev/null +++ b/packages/react-native-icons/svgo.config.js @@ -0,0 +1,13 @@ +module.exports = { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + mergePaths: false + }, + }, + }, + ], +}; \ No newline at end of file diff --git a/packages/react-native-icons/tsconfig.json b/packages/react-native-icons/tsconfig.json new file mode 100644 index 0000000000..db9aeca234 --- /dev/null +++ b/packages/react-native-icons/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "outDir": "lib", + "target": "ES6", + "module": "esnext", + "lib": ["es6", "dom"], + "declaration": true, + "experimentalDecorators": true, + "importHelpers": true, + "forceConsistentCasingInFileNames": true, + "strictNullChecks": true, + "moduleResolution": "node", + "jsx": "react", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src", "custom.d.ts"], + "exclude": ["lib", "lib-cjs"] +} \ No newline at end of file