diff --git a/docs/contributor-docs/adding-icons.md b/docs/contributor-docs/adding-icons.md index 989f0890f4..b79cb4896e 100644 --- a/docs/contributor-docs/adding-icons.md +++ b/docs/contributor-docs/adding-icons.md @@ -7,10 +7,7 @@ order: 9 ## Adding and Modifying Icons - Use dashes in the name of the .svg files (e.g `calendar-month`). - Use the same name for the "line" and "solid" variants, and save them in the respective folder, e.g. `Solid/calendar-month` and `Line/calendar-month`. - -- Copy the new icon files in the `/svg/Solid` and `/svg/Line` directories. - +- Use the same name for the "Line" and "Solid" variants, and save them in the respective folder, e.g. `instructure-ui/packages/ui-icons/svg/Line/calendar-month.svg` and `instructure-ui/packages/ui-icons/svg/Solid/calendar-month.svg`. - Double-check that the SVG size is 1920x1920. ```js @@ -65,15 +62,11 @@ type: code ``` -- If the icon has to bidirectional (being mirrored in RTL mode, typically arrow icons), add the icon name to the bidirectional list in `packages/ui-icons/icons.config.js`. Deprecated icons are handled here as well. - -- Run `npm run export:icons` from the repository root directory to generate the icons. This script will also take care of further optimizations on the SVG files (e.g. removing the `fill`s). The configs for this are located in `packages/ui-icons-build/lib/tasks/optimize-svgs/index.js` and `packages/ui-icons/svgo.config.js`. - -- Run `npm install && npm run bootstrap`. +- If the icon has to be bidirectional (being mirrored in RTL mode, typically arrow icons), add the icon name to the bidirectional list in `packages/ui-icons/icons.config.js`. Deprecated icons are handled here as well. -- Finally, run `npm run dev` from the repository root directory to start the local server and check the generated output. +- Run `npm run bootstrap`. -- Verify icons display correctly by checking under [Icons](/#icons) in the main nav. Check all 3 versions (React, SVG and icon font). +- Finally, run `npm run dev` and verify that the icons are displayed correctly under [Icons](/#icons). Check all 3 versions (React, SVG and icon font). (Note: The fonts are sometimes not rendered correctly, but we decided not to fix them, because they are not really used anywhere, and we might stop supporting icon fonts in the future in general.) @@ -94,3 +87,9 @@ type: code Draw inside the lines. - Fill the space edge-to-edge as much as possible. The build process will add margins as needed. + +- Don't use inline styles + +- Don't use `class` or `for` attributes + +- Always have `` as the root tag diff --git a/package-lock.json b/package-lock.json index 7075c1141d..3531793a51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13218,14 +13218,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/code-point-at": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/codemirror": { "version": "5.65.16", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz", @@ -20116,14 +20108,6 @@ "node": ">=10.13.0" } }, - "node_modules/invert-kv": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", @@ -22180,17 +22164,6 @@ "node": ">=14.0.0" } }, - "node_modules/lcid": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "invert-kv": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lerna": { "version": "8.1.9", "resolved": "https://registry.npmjs.org/lerna/-/lerna-8.1.9.tgz", @@ -25391,14 +25364,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nwsapi": { "version": "2.2.10", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", @@ -26133,17 +26098,6 @@ "node": ">=0.10.0" } }, - "node_modules/os-locale": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lcid": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/os-tmpdir": { "version": "1.0.2", "license": "MIT", @@ -27886,15 +27840,6 @@ "async-limiter": "~1.0.0" } }, - "node_modules/q": { - "version": "1.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, "node_modules/qjobs": { "version": "1.2.0", "license": "MIT", @@ -30359,113 +30304,6 @@ "node": ">=12.0.0" } }, - "node_modules/svg-to-jsx": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4.0.1", - "q": "^1.4.1", - "xml2js": "^0.4.10", - "xmlbuilder": "^13.0.2", - "yargs": "^3.21.0" - }, - "bin": { - "svg-to-jsx": "bin/svg-to-jsx" - } - }, - "node_modules/svg-to-jsx/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svg-to-jsx/node_modules/camelcase": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svg-to-jsx/node_modules/cliui": { - "version": "3.2.0", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/svg-to-jsx/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svg-to-jsx/node_modules/string-width": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svg-to-jsx/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svg-to-jsx/node_modules/wrap-ansi": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svg-to-jsx/node_modules/y18n": { - "version": "3.2.2", - "dev": true, - "license": "ISC" - }, - "node_modules/svg-to-jsx/node_modules/yargs": { - "version": "3.32.0", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^2.0.1", - "cliui": "^3.0.3", - "decamelize": "^1.1.1", - "os-locale": "^1.4.0", - "string-width": "^1.0.1", - "window-size": "^0.1.4", - "y18n": "^3.2.0" - } - }, "node_modules/svg2ttf": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/svg2ttf/-/svg2ttf-6.0.3.tgz", @@ -34109,17 +33947,6 @@ "version": "2.0.1", "license": "MIT" }, - "node_modules/window-size": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "bin": { - "window-size": "cli.js" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "dev": true, @@ -34327,34 +34154,6 @@ "node": ">=18" } }, - "node_modules/xml2js": { - "version": "0.4.23", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xmlbuilder": { - "version": "13.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, "node_modules/xmlchars": { "version": "2.2.0", "dev": true, @@ -37131,7 +36930,6 @@ }, "devDependencies": { "fantasticon": "^3.0.0", - "svg-to-jsx": "^1.0.4", "svgo": "^3.3.2" }, "optionalDependencies": { diff --git a/packages/ui-icons/svg/Line/ai-colored.svg b/packages/ui-icons/svg/Line/ai-colored.svg new file mode 100644 index 0000000000..ca946dbcd9 --- /dev/null +++ b/packages/ui-icons/svg/Line/ai-colored.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui-icons/svg/Line/compare.svg b/packages/ui-icons/svg/Line/compare.svg new file mode 100644 index 0000000000..1de3bdd498 --- /dev/null +++ b/packages/ui-icons/svg/Line/compare.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/ui-icons/svg/Solid/ai-colored.svg b/packages/ui-icons/svg/Solid/ai-colored.svg new file mode 100644 index 0000000000..ca946dbcd9 --- /dev/null +++ b/packages/ui-icons/svg/Solid/ai-colored.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui-icons/svg/Solid/compare.svg b/packages/ui-icons/svg/Solid/compare.svg new file mode 100644 index 0000000000..08108dcdc3 --- /dev/null +++ b/packages/ui-icons/svg/Solid/compare.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui-scripts/lib/icons/generate-react-components.js b/packages/ui-scripts/lib/icons/generate-react-components.js index 8b1f237bcf..fc5f1a35fc 100644 --- a/packages/ui-scripts/lib/icons/generate-react-components.js +++ b/packages/ui-scripts/lib/icons/generate-react-components.js @@ -22,7 +22,7 @@ * SOFTWARE. */ import fs from 'fs' -import svg2jsx from 'svg-to-jsx' +import { svg2jsx } from './svg2jsx.js' const NOTICE_HEADER = `/* * The MIT License (MIT) @@ -55,7 +55,7 @@ async function generateIconComponent(glyph) { const source = src.match( /]*?(?:viewBox="(\b[^"]*)")?>([\s\S]*?)<\/svg>/ )[2] - const jsxSource = await svg2jsx(source) + const jsxSource = svg2jsx(source) const viewBox = src.match(/viewBox="(.*?)"/)[1] const content = `import { Component } from 'react' @@ -112,13 +112,11 @@ export { ${name}${variant} } } function generateIconIndex(glyphs) { - let content = glyphs.map((glyph) => { - return `export { ${glyph.name}${glyph.variant} } from './${glyph.name}${glyph.variant}'` - }) - //TODO: this is a temp solution it will be removed when icon generation will be overhauled - content.push(`export { IconAiColoredLine } from './IconAiColoredLine'`) - content.push(`export { IconAiColoredSolid } from './IconAiColoredSolid'`) - content = content.join('\n\n') + const content = glyphs + .map((glyph) => { + return `export { ${glyph.name}${glyph.variant} } from './${glyph.name}${glyph.variant}'` + }) + .join('\n\n') return NOTICE_HEADER + content } @@ -136,231 +134,6 @@ export default function generateReactComponents(glyphs, destination) { }) }) - //TODO: this is a temp solution it will be removed when icon generation will be overhauled - fs.writeFile( - './__build__/IconAiColoredSolid.tsx', - `/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - import { Component } from 'react' - import { SVGIcon } from '@instructure/ui-svg-images' - import type { SVGIconProps } from '@instructure/ui-svg-images' - - class IconAiColoredSolid extends Component { - static glyphName = 'ai-colored' - static variant = 'Solid' - static displayName = 'IconAiColoredSolid' - - // eslint-disable-next-line react/forbid-foreign-prop-types - static propTypes = { ...SVGIcon.propTypes } - static allowedProps = [...SVGIcon.allowedProps] - - ref: Element | null = null - - handleRef = (el: Element | null) => { - const { elementRef } = this.props - - this.ref = el - - if (typeof elementRef === 'function') { - elementRef(el) - } - } - - render() { - - return ( - - - - - - - - - - - - - - - - - - - - ) - } - } - - export default IconAiColoredSolid - export { IconAiColoredSolid } - `, - (err) => { - if (err) { - console.error(err) - } - // file written successfully - } - ) - fs.writeFile( - './__build__/IconAiColoredLine.tsx', - `/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - import { Component } from 'react' - import { SVGIcon } from '@instructure/ui-svg-images' - import type { SVGIconProps } from '@instructure/ui-svg-images' - - class IconAiColoredLine extends Component { - static glyphName = 'ai-colored' - static variant = 'Line' - static displayName = 'IconAiColoredLine' - - // eslint-disable-next-line react/forbid-foreign-prop-types - static propTypes = { ...SVGIcon.propTypes } - static allowedProps = [...SVGIcon.allowedProps] - - ref: Element | null = null - - handleRef = (el: Element | null) => { - const { elementRef } = this.props - - this.ref = el - - if (typeof elementRef === 'function') { - elementRef(el) - } - } - - render() { - - return ( - - - - - - - - - - - - - - - - - - - - ) - } - } - - export default IconAiColoredLine - export { IconAiColoredLine } - `, - (err) => { - if (err) { - console.error(err) - } - // file written successfully - } - ) const indexFilePath = `${destination}index.ts` const indexContent = generateIconIndex(glyphs) fs.writeFile(indexFilePath, indexContent, (err) => { diff --git a/packages/ui-scripts/lib/icons/svg2jsx.js b/packages/ui-scripts/lib/icons/svg2jsx.js new file mode 100644 index 0000000000..20f53c3669 --- /dev/null +++ b/packages/ui-scripts/lib/icons/svg2jsx.js @@ -0,0 +1,109 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// this is a custom svg to jsx converter function which should be able to handle all of our svgs with some assumptions: +// - the root tag is always +// - there are no inline styles +// - there are no classnames +// - there is no `for` of `htmlFor` attr +export function svg2jsx(svgString) { + if (!svgString || !svgString.trim()) { + return '' + } + + let jsxString = svgString + + // Convert attribute names + const convertAttrName = (name) => { + // Specific SVG attributes that often need direct camelCase even with colons + if (name === 'xlink:href') return 'xlinkHref' + if (name === 'xml:space') return 'xmlSpace' + if (name === 'xml:lang') return 'xmlLang' + + // General kebab-case to camelCase, excluding data-*, aria-*, and xmlns attributes + if ( + name.startsWith('data-') || + name.startsWith('aria-') || + name.startsWith('xmlns') + ) { + return name + } + return name.replace(/-([a-z])/g, (g) => g[1].toUpperCase()) + } + + // Process tags and attributes + jsxString = jsxString.replace( + /<([a-zA-Z0-9:]+)([^>]*?)(\/?)>/g, + (match, tagName, attrsStr, selfClosing) => { + let newAttrs = '' + if (attrsStr) { + attrsStr.replace( + /([a-zA-Z0-9:_-]+)\s*=\s*(["'])(.*?)\2/g, + (attrMatch, name, quote, value) => { + const newName = convertAttrName(name) + // Escape curly braces within attribute values for JSX + const escapedValue = value + .replace(/{/g, '{') + .replace(/}/g, '}') + newAttrs += ` ${newName}="${escapedValue}"` + return '' // Necessary for replace to work properly when iterating + } + ) + } + + return `<${tagName}${newAttrs}${ + selfClosing || + [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'param', + 'path', + 'source', + 'track', + 'wbr', + 'circle', + 'ellipse', + 'line', + 'polygon', + 'polyline', + 'rect', + 'stop', + 'use' + ].includes(tagName) + ? ' /' + : '' + }>` + } + ) + + return jsxString.trim() +} diff --git a/packages/ui-scripts/package.json b/packages/ui-scripts/package.json index caabe49203..ea8b607ad9 100644 --- a/packages/ui-scripts/package.json +++ b/packages/ui-scripts/package.json @@ -52,7 +52,6 @@ }, "devDependencies": { "fantasticon": "^3.0.0", - "svg-to-jsx": "^1.0.4", "svgo": "^3.3.2" } }