From 15fa69a903382ab1a45606bacce12b8cc24eda63 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 16 Aug 2026 08:17:35 +0200 Subject: [PATCH 01/33] bump --- icons-react/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icons-react/package.json b/icons-react/package.json index 84af02d..b98091f 100644 --- a/icons-react/package.json +++ b/icons-react/package.json @@ -1,7 +1,7 @@ { "name": "@datalayer/icons-react", "license": "MIT", - "version": "1.0.1", + "version": "1.0.9", "description": "React.js and JupyterLab icons for data products.", "keywords": [ "icons", From 1c9f4ae572dcedf2cc4d99c783e188578b203582 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 16 Aug 2026 11:58:24 +0200 Subject: [PATCH 02/33] colormode --- package.json | 14 +- scripts/build.js | 570 ++++++++++++++------- src/DatalayerIcons.tsx | 1084 ++++++++++++++++++++++------------------ src/index.ts | 2 + tsconfig.json | 2 +- tsconfig.node.json | 7 +- vue/.gitignore | 4 - vue/index.esm.js | 16 - vue/index.js | 16 - vue/package.json | 57 --- 10 files changed, 1010 insertions(+), 762 deletions(-) delete mode 100644 vue/.gitignore delete mode 100644 vue/index.esm.js delete mode 100644 vue/index.js delete mode 100644 vue/package.json diff --git a/package.json b/package.json index f58ade2..a49a223 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,20 @@ "build-data2": "rimraf ./optimized/data && svgo --config=svgo.yaml -f ./svg/data2 -o ./optimized/data2 --pretty --indent=2", "build-eggs": "rimraf ./optimized/eggs && svgo --config=svgo.yaml -f ./svg/eggs -o ./optimized/eggs --pretty --indent=2", "build-react": "node ./scripts/build.js icons-react", - "build-vue": "node ./scripts/build.js vue", "clean": "rimraf lib build dist tsconfig.tsbuildinfo optimized/data1 optimized/data2 optimized/eggs react/data1 react/data2 react/eggs", "dev": "vite", "landing:app": "tsc --watch", "preview": "vite preview", - "start": "vite" + "start": "vite", + "watch:lib": "tsc -b -w" }, "dependencies": { + "@datalayer/icons-react": "^1.0.9", + "@datalayer/primer-addons": "^1.0.12", "@jupyterlab/ui-components": "^4.0.0", + "@primer/octicons-react": "^19.29.1", + "@primer/react": "^37.31.0", + "html-to-image": "^1.9.0", "react": "18.3.1", "react-dom": "18.3.1", "styled-components": "^5.3.10" @@ -38,11 +43,8 @@ "devDependencies": { "@babel/core": "^7.12.10", "@babel/plugin-transform-react-jsx": "^7.12.12", - "@datalayer/primer-addons": "^1.0.12", "@primer/css": "^21.5.1", - "@primer/octicons-react": "^19.29.1", "@primer/primitives": "^10.7.0", - "@primer/react": "^37.31.0", "@primer/brand-primitives": "^0.51.0", "@svgr/core": "^5.5.0", "@svgr/plugin-svgo": "^5.5.0", @@ -51,11 +53,9 @@ "@types/react-dom": "18.3.6", "@types/styled-components": "^5.1.34", "@vitejs/plugin-react": "^4.3.3", - "@vue/compiler-dom": "^3.0.5", "autoprefixer": "^10.4.14", "camelcase": "^6.0.0", "cheerio": "^1.0.0-rc.12", - "html-to-image": "^1.9.0", "postcss": "^8.4.23", "prettier": "^3.3.2", "react-router-dom": "^6.30.1", diff --git a/scripts/build.js b/scripts/build.js index 35dde75..c4f5698 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,45 +1,261 @@ -const fs = require('fs').promises; +const fs = require("fs").promises; -const { rimraf } = require('rimraf'); +const { rimraf } = require("rimraf"); -const babel = require('@babel/core'); -const camelcase = require('camelcase'); -const cheerio = require('cheerio'); +const babel = require("@babel/core"); +const camelcase = require("camelcase"); +const cheerio = require("cheerio"); // `@svgr/core` v6+ exports its async converter as `default`; older v5 exposed // `transform`. Handle both so the build works regardless of the installed // version. -const svgrCore = require('@svgr/core'); +const svgrCore = require("@svgr/core"); const svgr = svgrCore.transform || svgrCore.default; -const { compile: compileVue } = require('@vue/compiler-dom') +const { dirname } = require("path"); -const { dirname } = require('path') +function parsePaintColor(paint) { + const value = paint + .trim() + .replace(/\s*!important\s*$/i, "") + .toLowerCase(); + const named = { + black: [0, 0, 0], + white: [255, 255, 255], + }; + if (named[value]) { + return named[value]; + } + const hex = value.match( + /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i, + ); + if (hex) { + let channels = hex[1]; + if (channels.length === 3 || channels.length === 4) { + channels = channels + .slice(0, 3) + .split("") + .map((channel) => channel + channel) + .join(""); + } + return [0, 2, 4].map((offset) => + parseInt(channels.slice(offset, offset + 2), 16), + ); + } + const rgb = value.match( + /^rgba?\(\s*([\d.]+)%?[,\s]+([\d.]+)%?[,\s]+([\d.]+)%?/i, + ); + if (rgb) { + const usesPercent = value.slice(0, value.indexOf(")")).includes("%"); + return rgb.slice(1, 4).map((channel) => { + const numeric = Number(channel); + return usesPercent ? numeric * 2.55 : numeric; + }); + } + return null; +} -let transforms = { +function extractGradientColors(svg) { + const $ = cheerio.load(svg, { xmlMode: true }); + const gradients = new Map(); + $("linearGradient, radialGradient").each((_index, gradient) => { + const element = $(gradient); + const id = element.attr("id"); + if (!id) return; + const colors = element + .find("stop") + .toArray() + .flatMap((stop) => { + const stopElement = $(stop); + const direct = stopElement.attr("stop-color"); + const styled = stopElement + .attr("style") + ?.match(/(?:^|;)\s*stop-color\s*:\s*([^;]+)/i)?.[1]; + const parsed = parsePaintColor(direct || styled || ""); + return parsed ? [parsed] : []; + }); + gradients.set(id, { + colors, + reference: ( + element.attr("href") || + element.attr("xlink:href") || + "" + ).replace(/^#/, ""), + }); + }); + + const resolved = new Map(); + const resolve = (id, visited = new Set()) => { + if (resolved.has(id)) return resolved.get(id); + if (visited.has(id)) return null; + visited.add(id); + const gradient = gradients.get(id); + if (!gradient) return null; + const colors = gradient.colors.length + ? gradient.colors + : gradient.reference + ? [resolve(gradient.reference, visited)].filter(Boolean) + : []; + if (!colors.length) return null; + const average = [0, 1, 2].map( + (index) => + colors.reduce((sum, color) => sum + color[index], 0) / colors.length, + ); + resolved.set(id, average); + return average; + }; + for (const id of gradients.keys()) resolve(id); + return resolved; +} + +function resolvePaintColor(paint, gradientColors) { + const direct = parsePaintColor(paint); + if (direct) return direct; + const gradient = paint.match(/url\(\s*["']?#([^"')\s]+)["']?\s*\)/i); + if (!gradient) return null; + const id = gradient[1].replace(/^prefix__/, ""); + return gradientColors.get(id) || null; +} + +function paintStrength(paint, gradientColors) { + const channels = resolvePaintColor(paint, gradientColors); + if (!channels) { + return 72; + } + const linear = channels.map((channel) => { + const normalized = channel / 255; + return normalized <= 0.04045 + ? normalized / 12.92 + : ((normalized + 0.055) / 1.055) ** 2.4; + }); + const luminance = + 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]; + const chroma = (Math.max(...channels) - Math.min(...channels)) / 255; + + // Map native luminance into opaque shades of one hue. Mixing toward black + // preserves the source's light-fill/dark-outline ordering without allowing + // the swatch background to contaminate the requested theme or accent color. + return Math.round(Math.min(96, 38 + 52 * luminance + 10 * chroma)); +} - 'icons-react': async (svg, componentName, format, style) => { +function tonalPaint(paint, baseColor, gradientColors) { + const strength = paintStrength(paint, gradientColors); + return `color-mix(in srgb, ${baseColor} ${strength}%, black)`; +} + +function adaptiveTonalPaint(paint, baseColor, gradientColors) { + const darkStrength = paintStrength(paint, gradientColors); + const lightStrength = Math.max(38, Math.min(96, 134 - darkStrength)); + const canvas = "var(--bgColor-default, var(--color-canvas-default, Canvas))"; + return `light-dark(color-mix(in srgb, ${baseColor} ${lightStrength}%, ${canvas}), color-mix(in srgb, ${baseColor} ${darkStrength}%, ${canvas}))`; +} + +function parseCssDeclarations(css) { + return css.split(";").flatMap((declaration) => { + const separator = declaration.indexOf(":"); + if (separator === -1) return []; + const property = declaration.slice(0, separator).trim(); + const value = declaration.slice(separator + 1).trim(); + return property && value ? [[property, value]] : []; + }); +} + +function inlineSvgStyles(svg) { + const $ = cheerio.load(svg, { xmlMode: true }); + const inlineDeclarations = $("[style]") + .toArray() + .map((element) => ({ + element, + declarations: parseCssDeclarations($(element).attr("style") || ""), + })); + const stylesByElement = new Map(); + const applyStyle = (element, declarations) => { + const styles = stylesByElement.get(element) || new Map(); + for (const [property, value] of declarations) { + styles.set(property, value); + } + stylesByElement.set(element, styles); + }; + + $("style").each((_index, style) => { + const stylesheet = $(style).text(); + for (const rule of stylesheet.matchAll(/([^{}]+)\{([^{}]*)\}/g)) { + for (const selector of rule[1] + .split(",") + .map((value) => value.trim()) + .filter(Boolean)) { + $(selector).each((_matchIndex, element) => { + applyStyle(element, parseCssDeclarations(rule[2])); + }); + } + } + }); + + // Inline style attributes have higher precedence than stylesheet rules. + for (const { element, declarations } of inlineDeclarations) { + applyStyle(element, declarations); + } + for (const [element, styles] of stylesByElement) { + $(element).attr( + "style", + [...styles].map(([property, value]) => `${property}:${value}`).join(";"), + ); + } + $("style").remove(); + return $.xml($("svg").first()); +} + +let transforms = { + "icons-react": async (svg, componentName, format) => { + const svgInlined = inlineSvgStyles(svg); + const gradientColors = extractGradientColors(svgInlined); + const hasNativePaints = [ + ...svgInlined.matchAll( + /\b(?:fill|stroke)\s*(?:=\s*"([^"]+)"|:\s*([^;"]+))/gi, + ), + ].some( + (match) => + !["none", "currentcolor"].includes( + (match[1] || match[2]).trim().toLowerCase(), + ), + ); // Strip fill="currentColor" from child SVG elements (path, circle, rect, etc.) // so they INHERIT from the root, which can be overridden by caller props. // Without this, child elements always resolve currentColor via CSS `color`, // ignoring any `fill` prop passed to the . - const svgCleaned = svg.replace( - /<(path|circle|rect|polygon|polyline|ellipse|line|g)\b([^>]*?)\s+fill\s*=\s*"currentColor"/g, - '<$1$2' - ); - - let component = await svgr( - svgCleaned, - { - ref: true, - titleProp: true, - plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], - }, - { componentName } - ) + const svgCleaned = svgInlined + .replace( + /<(path|circle|rect|polygon|polyline|ellipse|line|g)\b([^>]*?)\s+fill\s*=\s*"currentColor"/g, + "<$1$2", + ) + // Artwork must not introduce navigation, and currentColor inside an SVG + // link would resolve from the host application's link styling. + .replace(/]*>/gi, "") + .replace(/<\/a>/gi, ""); + + let component; + try { + component = await svgr( + svgCleaned, + { + ref: true, + titleProp: true, + plugins: ["@svgr/plugin-svgo", "@svgr/plugin-jsx"], + }, + { componentName }, + ); + } catch (error) { + console.error(`Failed to transform ${componentName}`); + throw error; + } let { code } = await babel.transformAsync(component, { - plugins: [[require('@babel/plugin-transform-react-jsx'), { useBuiltIns: true, throwIfNamespace: false }]], - }) + plugins: [ + [ + require("@babel/plugin-transform-react-jsx"), + { useBuiltIns: true, throwIfNamespace: false }, + ], + ], + }); const SIZE_MAP = { small: 16, @@ -47,44 +263,63 @@ let transforms = { large: 64, }; - const svgElement = cheerio.load(svg)('svg') - const svgViewBox = svgElement.attr('viewBox').split(" ") + const svgElement = cheerio.load(svg)("svg"); + const svgViewBox = svgElement.attr("viewBox").split(" "); const width = Number(svgViewBox[2]); const height = Number(svgViewBox[3]); - let lines = code.split('\n'); + let lines = code.split("\n"); - lines.splice(1, 0, `\nconst sizeMap = ${JSON.stringify(SIZE_MAP, null, 2)};\n`); + lines.splice( + 1, + 0, + `\nconst sizeMap = ${JSON.stringify(SIZE_MAP, null, 2)};\n`, + ); lines.splice(5, 0, ` size,`); - if (!componentName.endsWith('NoopIcon')) { - if (style !== 'data2') { - lines.splice(6, 0, ` colored,`, ` themed,`); - } + if (!componentName.endsWith("NoopIcon")) { + lines.splice(6, 0, ` colored,`, ` themed,`); } - lines.splice(14, 0, ` ${width >= height ? 'width' : 'height'}: size ? typeof size === "string" ? sizeMap[size] : size : "16px",`); - code = lines.join('\n'); - - if (!componentName.endsWith('NoopIcon')) { - if (style !== 'data2') { - // White fills stay white. Every other (non-colored) fill defaults to - // plain `currentColor`. Theming is opt-in: when `themed` is true the - // fill instead uses `var(--datalayer-icon-fg, currentColor)`, letting - // each icon "auto detect" the active theme primary colour set by an - // ancestor. When `colored` is true the original brand colour is kept. - const WHITE_FILLS = new Set(['#fff', '#fffff', 'white', '#FFF', '#FFFFFF']); - code = code.replaceAll(/fill: "([#a-zA-Z0-9]+)",/g, (_match, origFill) => { - if (origFill === 'currentColor') { - return `fill: themed ? 'var(--datalayer-icon-fg, currentColor)' : 'currentColor',`; + lines.splice( + 14, + 0, + ` ${width >= height ? "width" : "height"}: size ? typeof size === "string" ? sizeMap[size] : size : "16px",`, + ); + code = lines.join("\n"); + + if (!componentName.endsWith("NoopIcon")) { + // Keep the source artwork for `colored`. Otherwise collapse every + // visible fill and stroke to one inherited color. This applies equally + // to data1 and data2, including gradients and white artwork. Paints set + // to `none` remain transparent so outline icons keep their geometry. + const themedPaint = + "var(--datalayer-icon-color, var(--datalayer-icon-fg, currentColor))"; + const inheritedPaint = "var(--datalayer-icon-color, currentColor)"; + const monochromePaint = `(themed ? ${JSON.stringify(themedPaint)} : ${JSON.stringify(inheritedPaint)})`; + const monochromeColor = `(themed ? 'var(--datalayer-icon-fg, currentColor)' : 'inherit')`; + const flatTone = (originalPaint) => + `(props.color != null ? ${JSON.stringify(tonalPaint(originalPaint, inheritedPaint, gradientColors))} : (themed ? ${JSON.stringify(adaptiveTonalPaint(originalPaint, themedPaint, gradientColors))} : ${JSON.stringify(adaptiveTonalPaint(originalPaint, inheritedPaint, gradientColors))}))`; + let canToneRootCurrentColor = hasNativePaints; + code = code.replaceAll( + /\b(fill|stroke|color): "([^"]+)"(,?)/g, + (_match, attribute, originalPaint, comma) => { + if (originalPaint.toLowerCase() === "none") { + return `${attribute}: "none"${comma}`; } - if (WHITE_FILLS.has(origFill)) { - return `fill: colored ? '${origFill}' : 'white',`; + if (originalPaint === "currentColor") { + if (canToneRootCurrentColor && attribute !== "color") { + canToneRootCurrentColor = false; + return `${attribute}: ${flatTone(originalPaint)}${comma}`; + } + return `${attribute}: ${attribute === "color" ? monochromeColor : monochromePaint}${comma}`; } - return `fill: colored ? '${origFill}' : (themed ? 'var(--datalayer-icon-fg, currentColor)' : 'currentColor'),`; - }); - } + const flatPaint = + attribute === "color" ? monochromeColor : flatTone(originalPaint); + return `${attribute}: colored ? ${JSON.stringify(originalPaint)} : ${flatPaint}${comma}`; + }, + ); } // Inject `colormode` prop support (opt-in, default false). When truthy, @@ -93,175 +328,160 @@ let transforms = { // value if passed as 'light' | 'dark'). This causes Primer's CSS variables // to resolve to the opposite theme inside the wrapper, effectively // rendering the icon in the inverted colormode without affecting siblings. - const inverseHelper = `function _DlIconInverseColormode({\n mode,\n children\n}) {\n const ref = React.useRef(null);\n const [resolved, setResolved] = React.useState(typeof mode === "string" ? mode : null);\n React.useLayoutEffect(() => {\n if (typeof mode === "string") {\n setResolved(mode);\n return;\n }\n if (typeof document === "undefined") return;\n let cur = null;\n let el = ref.current && ref.current.parentElement;\n while (el) {\n const m = el.getAttribute && el.getAttribute("data-color-mode");\n if (m) { cur = m; break; }\n el = el.parentElement;\n }\n if (!cur || cur === "auto") {\n cur = (typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light";\n }\n setResolved(cur === "dark" ? "light" : "dark");\n }, [mode]);\n return /*#__PURE__*/React.createElement("span", {\n ref: ref,\n "data-color-mode": resolved || "light",\n "data-light-theme": "light",\n "data-dark-theme": "dark",\n style: { display: "inline-flex", lineHeight: 0 }\n }, children);\n}\n\n`; + const inverseHelper = `function _DlIconInverseColormode({\n mode,\n children\n}) {\n const ref = React.useRef(null);\n const [appearance, setAppearance] = React.useState({\n mode: typeof mode === "string" ? mode : null,\n lightTheme: null,\n darkTheme: null\n });\n React.useLayoutEffect(() => {\n if (typeof document === "undefined") return;\n let currentMode = null;\n let lightTheme = null;\n let darkTheme = null;\n let el = ref.current && ref.current.parentElement;\n while (el && (!currentMode || !lightTheme || !darkTheme)) {\n if (el.getAttribute) {\n currentMode = currentMode || el.getAttribute("data-color-mode");\n lightTheme = lightTheme || el.getAttribute("data-light-theme");\n darkTheme = darkTheme || el.getAttribute("data-dark-theme");\n }\n el = el.parentElement;\n }\n if (!currentMode || currentMode === "auto") {\n currentMode = (typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light";\n }\n setAppearance({\n mode: typeof mode === "string" ? mode : (currentMode === "dark" ? "light" : "dark"),\n lightTheme,\n darkTheme\n });\n }, [mode]);\n return /*#__PURE__*/React.createElement("span", {\n ref: ref,\n "data-color-mode": appearance.mode || "light",\n "data-light-theme": appearance.lightTheme || undefined,\n "data-dark-theme": appearance.darkTheme || undefined,\n style: {\n display: "inline-flex",\n lineHeight: 0,\n color: "var(--fgColor-default, var(--color-fg-default, currentColor))"\n }\n }, children);\n}\n\n`; // 1) Inject the helper before the icon component declaration. - code = code.replace(/(function \w+\(\{)/, inverseHelper + '$1'); + code = code.replace(/(function \w+\(\{)/, inverseHelper + "$1"); // 2) Add `colormode` to the destructured props (just before `...props`). - code = code.replace(/( \.\.\.props\n\}, svgRef\) \{)/, ' colormode,\n$1'); + code = code.replace(/( \.\.\.props\n\}, svgRef\) \{)/, " colormode,\n$1"); // 3) Wrap the returned svg element so it can be conditionally placed inside // the inverse-colormode wrapper. code = code.replace( / return (\/\*#__PURE__\*\/React\.createElement\("svg",[\s\S]*?\));\n\}\nconst ForwardRef/, - ' const _svgEl = $1;\n if (!colormode) return _svgEl;\n return /*#__PURE__*/React.createElement(_DlIconInverseColormode, {\n mode: typeof colormode === "string" ? colormode : undefined\n }, _svgEl);\n}\nconst ForwardRef' + ' const _svgEl = $1;\n if (!colormode) return _svgEl;\n return /*#__PURE__*/React.createElement(_DlIconInverseColormode, {\n mode: typeof colormode === "string" ? colormode : undefined\n }, _svgEl);\n}\nconst ForwardRef', ); - // Ensure the explicit `color` prop always wins over inherited - // `--datalayer-icon-fg` by setting a local CSS variable on the . - // This keeps existing behavior intact when `color` is not provided. + // Carry the explicit `color` prop in a dedicated variable used by every + // generated paint. This remains authoritative even when consumer CSS sets + // `color` on SVG descendants or links. code = code.replace( - ' }, props),', - ' }, props, {\n style: props && props.color != null ? Object.assign({}, props.style, {\n "--datalayer-icon-fg": props.color\n }) : props && props.style\n }),' + " }, props),", + ' }, props, {\n "data-datalayer-icon": "",\n style: props && props.color != null ? Object.assign({}, props.style, {\n "--datalayer-icon-color": props.color,\n color: props.color\n }) : props && props.style\n }),', ); - if (format === 'esm') { - return code - } - - return code - .replace('import * as React from "react"', 'const React = require("react")') - .replace('export default', 'module.exports =') - }, + const colorModeCss = + '[data-color-mode="light"]{--datalayer-icon-color-scheme:light}[data-color-mode="dark"]{--datalayer-icon-color-scheme:dark}svg[data-datalayer-icon]{color-scheme:var(--datalayer-icon-color-scheme, light dark)}'; + code = code.replace( + " }), title ?", + ` }), /*#__PURE__*/React.createElement("style", null, ${JSON.stringify(colorModeCss)}), title ?`, + ); - 'icons-vue': (svg, componentName, format) => { - let { code } = compileVue(svg, { - mode: 'module', - }) - if (format === 'esm') { - return code.replace('export function', 'export default function') + if (format === "esm") { + return code; } + return code .replace( - /import\s+\{\s*([^}]+)\s*\}\s+from\s+(['"])(.*?)\2/, - (_match, imports, _quote, mod) => { - let newImports = imports - .split(',') - .map((i) => i.trim().replace(/\s+as\s+/, ': ')) - .join(', ') - - return `const { ${newImports} } = require("${mod}")` - } + 'import * as React from "react"', + 'const React = require("react")', ) - .replace('export function render', 'module.exports = function render') + .replace("export default", "module.exports ="); }, - -} +}; async function getIcons(flavor) { - let files = await fs.readdir(`./optimized/${flavor}`) + let files = await fs.readdir(`./optimized/${flavor}`); return Promise.all( files.map(async (file) => ({ - svg: await fs.readFile(`./optimized/${flavor}/${file}`, 'utf8'), - svgName: file.replace(/\.svg$/, ''), - componentInstance: `${camelcase(file.replace(/\.svg$/, ''), { + svg: await fs.readFile(`./optimized/${flavor}/${file}`, "utf8"), + svgName: file.replace(/\.svg$/, ""), + componentInstance: `${camelcase(file.replace(/\.svg$/, ""), { pascalCase: false, })}Icon`, - componentName: `${camelcase(file.replace(/\.svg$/, ''), { + componentName: `${camelcase(file.replace(/\.svg$/, ""), { pascalCase: true, })}Icon`, - })) - ) + })), + ); } function exportAll(icons, format, includeExtension = true) { - return icons .map(({ componentInstance, componentName }) => { - let extension = includeExtension ? '.js' : '' - if (format === 'esm') { + let extension = includeExtension ? ".js" : ""; + if (format === "esm") { return `export { default as ${componentName} } from './${componentName}${extension}' -// export { default as ${componentInstance}LabIcon } from './${componentName}LabIcon${extension}'` +// export { default as ${componentInstance}LabIcon } from './${componentName}LabIcon${extension}'`; } return `module.exports.${componentName} = require("./${componentName}${extension}") -// module.exports.${componentInstance}LabIcon = require("./${componentName}LabIcon${extension}")` +// module.exports.${componentInstance}LabIcon = require("./${componentName}LabIcon${extension}")`; }) - .join('\n') - } + .join("\n"); +} async function ensureWrite(file, text) { - await fs.mkdir(dirname(file), { recursive: true }) - await fs.writeFile(file, text, 'utf8') + await fs.mkdir(dirname(file), { recursive: true }); + await fs.writeFile(file, text, "utf8"); } async function ensureWriteJson(file, json) { - await ensureWrite(file, JSON.stringify(json, null, 2) + '\n') + await ensureWrite(file, JSON.stringify(json, null, 2) + "\n"); } async function buildIcons(package, flavor, format) { - - let outDir = `./${package}/${flavor}` - if (format === 'esm') { - outDir += '/esm' - }; + let outDir = `./${package}/${flavor}`; + if (format === "esm") { + outDir += "/esm"; + } let icons = await getIcons(flavor); await Promise.all( - - icons.flatMap(async ({ componentName, componentInstance, svgName, svg }) => { - - let content = await transforms[package](svg, componentName, format, flavor) - - let types = package === 'icons-react' - ? `import * as React from 'react'; + icons.flatMap( + async ({ componentName, componentInstance, svgName, svg }) => { + let content = await transforms[package]( + svg, + componentName, + format, + flavor, + ); + + const types = `import * as React from 'react'; declare const ${componentName}: React.ForwardRefExoticComponent> & { title?: string, titleId?: string, size?: "small" | "medium" | "large" | number, colored?: boolean, themed?: boolean, colormode?: boolean | "light" | "dark" } & React.RefAttributes>; -export default ${componentName};` - : `import type { FunctionalComponent, HTMLAttributes, VNodeProps } from 'vue'; -declare const ${componentName}: FunctionalComponent; -export default ${componentName};` - // Strip the `class` attribute from the ROOT element before writing - // the .svg that LabIcon imports as its `svgstr`. JupyterLab's - // LabIcon.getReactAttrs() renders the root svg as a React element and - // camelCases attribute names — but camelCase('class') === 'class', so a - // root `class` becomes an invalid React DOM prop and triggers: "Invalid - // DOM property `class`. Did you mean `className`?". Only the root class is - // removed (leftover design-tool marker like `cls-1`/`feather`/`lucide`); - // child element classes and any