Skip to content

Commit

Permalink
UI kit: icons (#135)
Browse files Browse the repository at this point in the history
Icon components are generated from their SVG files, using the `scripts/update-icons.ts` script. The process of adding a new icon is as follows:

- Export the icon:
  - SVG format.
  - 24x24 dimensions.
  - Make sure strokes are outlined (`fill`).
  - Use `#000000` for the color.
- Place the icon in `frontend/uikit/src/icons/svg`.
- Run the script: `cd frontend/uikit && pnpm update-icons`.
- Commit the changes (SVG icon + corresponding component).
  • Loading branch information
bpierre authored Apr 29, 2024
1 parent 56bc27c commit 13cb1b4
Show file tree
Hide file tree
Showing 26 changed files with 324 additions and 7 deletions.
38 changes: 38 additions & 0 deletions frontend/uikit-gallery/src/Icon/1. Default.fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { colors } from "@liquity2/uikit";
import * as icons from "@liquity2/uikit/icons";
import { useFixtureSelect } from "react-cosmos/client";

const colorsContrasted = Object.fromEntries(
Object.entries(colors)
.filter(([name]) => {
const parsed = name.split(":");
const shadeNumber = parseInt(parsed[1], 10);
return !isNaN(shadeNumber) && shadeNumber >= 500;
}),
) as Record<keyof typeof colors, string>;

const colorNames = Object.keys(colorsContrasted) as (keyof typeof colorsContrasted)[];

export default function IconFixture() {
const [colorMode] = useFixtureSelect("color", {
options: colorNames,
defaultValue: "blue:950",
});
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: 8,
padding: 16,
color: colorsContrasted[colorMode],
}}
>
{Object
.entries(icons)
.map(([name, Icon]) => <Icon key={name} />)}
</div>
);
}
20 changes: 15 additions & 5 deletions frontend/uikit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
"main": "./dist/uikit.js",
"module": "./dist/uikit.js",
"types": "./dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/uikit.js"
"import": "./dist/index.js"
},
"./icons": {
"types": "./dist/icons/index.d.ts",
"import": "./dist/icons/index.js"
},
"./index.css": "./src/index.css",
"./panda.config": "./panda.config.ts",
Expand Down Expand Up @@ -38,7 +43,8 @@
"build": "pnpm panda-codegen && tsc && vite build",
"dev": "vite build --watch",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"panda-codegen": "panda codegen --silent"
"panda-codegen": "panda codegen --silent",
"update-icons": "pnpm tsx scripts/update-icons.ts"
},
"peerDependencies": {
"@pandacss/dev": "^0.37.2",
Expand All @@ -52,18 +58,22 @@
"react-cool-dimensions": "^3.0.1"
},
"devDependencies": {
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@svgr/plugin-svgo": "^8.1.0",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"dprint": "^0.45.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-ts": "^3.4.5",
"tsx": "^4.7.1",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vite-plugin-dts": "^3.8.3"
"vite-plugin-dts": "^3.8.3",
"zx": "^7.2.3"
}
}
123 changes: 123 additions & 0 deletions frontend/uikit/scripts/update-icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { transform } from "@svgr/core";
import { readFile, writeFile } from "node:fs/promises";
import { join as pathJoin, parse as pathParse } from "node:path";
import { $, argv, echo, glob } from "zx";

const HELP = `
Optimizes and converts the SVG icons found in src/icons/svg/ into components.
Usage:
pnpm tsx update-icons.ts
Options:
--help, -h Show this help message.
`;

export async function main() {
const options = {
help: "help" in argv || "h" in argv,
};

if (options.help) {
echo`${HELP}`;
process.exit(0);
}

const icons = await glob(
pathJoin(import.meta.dirname, "../src/icons/svg/*.svg"),
);

const components = await Promise.all(
icons.map(async (path) => {
const svg = await readFile(path, "utf8");
const { name } = pathParse(path);
const componentName = `Icon${kebabToPascal(name)}`;
const transformed = await transform(svg, {
plugins: ["@svgr/plugin-svgo", "@svgr/plugin-jsx"],
icon: 24,
typescript: true,
template: svgrTemplate,
jsxRuntime: "automatic",
exportType: "named",
expandProps: false,
replaceAttrValues: {
"#000": "currentColor",
"#000000": "currentColor",
},
svgoConfig: {
plugins: [
"removeUselessDefs",
"removeXMLNS",
"cleanupIds",
],
},
}, {
componentName,
});

await writeFile(
pathJoin(import.meta.dirname, `../src/icons/${componentName}.tsx`),
"// this file was generated by scripts/update-icons.ts\n"
+ "// please do not edit it manually\n\n"
+ await format(transformed),
);

return { name, componentName };
}),
);

await writeFile(
pathJoin(import.meta.dirname, `../src/icons/index.ts`),
"// this file was generated by scripts/update-icons.ts\n"
+ "// please do not edit it manually\n\n"
+ await format(
components
.map(({ componentName }) => `export { ${componentName} } from "./${componentName}";`)
.join("\n"),
),
);

console.log(`\nGenerated ${components.length} icon components:\n`);
console.log(
components.map(({ name, componentName }) => (
`src/icons/${componentName}.tsx (${name}.svg)`
)).join("\n"),
);
console.log("\nIndex file updated: src/icons/index.ts\n");
}

type Template = NonNullable<
NonNullable<Parameters<typeof transform>[1]>["template"]
>;

const svgrTemplate: Template = function template(...[variables, { tpl }]) {
return tpl`
// this file was generated by scripts/update-icons.ts
// please do not edit it manually
${variables.interfaces};
${variables.imports};
export function ${variables.componentName} (${variables.props}) {
return ${variables.jsx};
}
`;
};

async function format(content: string) {
const dprint = $`dprint fmt --stdin tsx`.quiet();
dprint.stdin.write(content);
dprint.stdin.end();
const { stdout } = await dprint;
return stdout;
}

// kebab case (e.g. arrow-left) => Pascal case (e.g. ArrowLeft)
function kebabToPascal(str: string) {
return str
.split("-")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("");
}

main();
14 changes: 14 additions & 0 deletions frontend/uikit/src/icons/IconAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconAccount() {
return (
<svg width={24} height={24} fill="none">
<path
fill="currentColor"
d="M18.5 8.192c0 3.42-2.91 6.191-6.5 6.191s-6.5-2.772-6.5-6.191S8.41 2 12 2s6.5 2.772 6.5 6.192"
/>
<path fill="currentColor" d="M4 14.381h16V22H4z" />
</svg>
);
}
15 changes: 15 additions & 0 deletions frontend/uikit/src/icons/IconArrowBack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconArrowBack() {
return (
<svg width={24} height={24} fill="none">
<path
fill="currentColor"
fillRule="evenodd"
d="m7.682 12.2 2.807 2.809L9.5 16 5 11.5 9.498 7l.991.991L7.682 10.8H18v1.402z"
clipRule="evenodd"
/>
</svg>
);
}
10 changes: 10 additions & 0 deletions frontend/uikit/src/icons/IconArrowRight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconArrowRight() {
return (
<svg width={24} height={24} fill="none">
<path fill="currentColor" d="m12 4-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z" />
</svg>
);
}
11 changes: 11 additions & 0 deletions frontend/uikit/src/icons/IconBorrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconBorrow() {
return (
<svg width={24} height={24} fill="none">
<path fill="currentColor" d="M9.364 18.364A9 9 0 0 1 3 21V3a9 9 0 0 1 6.364 15.364" />
<path fill="currentColor" d="M12 12a9 9 0 0 1 9-9v18a9 9 0 0 1-9-9" />
</svg>
);
}
13 changes: 13 additions & 0 deletions frontend/uikit/src/icons/IconCross.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconCross() {
return (
<svg width={24} height={24} fill="none">
<path
fill="currentColor"
d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
/>
</svg>
);
}
15 changes: 15 additions & 0 deletions frontend/uikit/src/icons/IconEarn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconEarn() {
return (
<svg width={24} height={24} fill="none">
<path
fill="currentColor"
fillRule="evenodd"
d="M21 3H3v18h18zm-8.999 14.143a5.143 5.143 0 1 0 0-10.286 5.143 5.143 0 0 0 0 10.286"
clipRule="evenodd"
/>
</svg>
);
}
10 changes: 10 additions & 0 deletions frontend/uikit/src/icons/IconExternal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconExternal() {
return (
<svg width={24} height={24} fill="none">
<path fill="currentColor" d="M6 6v2h8.59L5 17.59 6.41 19 16 9.41V18h2V6z" />
</svg>
);
}
10 changes: 10 additions & 0 deletions frontend/uikit/src/icons/IconLeverage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconLeverage() {
return (
<svg width={24} height={24} fill="none">
<path fill="currentColor" d="M3 14.572h6.429V21H3zM9.428 3h11.571v11.571H9.428z" />
</svg>
);
}
10 changes: 10 additions & 0 deletions frontend/uikit/src/icons/IconPlus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconPlus() {
return (
<svg width={24} height={24} fill="none">
<path fill="currentColor" d="M19 12.998h-6v6h-2v-6H5v-2h6v-6h2v6h6z" />
</svg>
);
}
10 changes: 10 additions & 0 deletions frontend/uikit/src/icons/IconStake.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export function IconStake() {
return (
<svg width={24} height={24} fill="none">
<path fill="currentColor" d="M9 15h6v6H9zM9 3h6v6H9zM15 9h6v6h-6zM3 9h6v6H3z" />
</svg>
);
}
13 changes: 13 additions & 0 deletions frontend/uikit/src/icons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// this file was generated by scripts/update-icons.ts
// please do not edit it manually

export { IconAccount } from "./IconAccount";
export { IconArrowBack } from "./IconArrowBack";
export { IconArrowRight } from "./IconArrowRight";
export { IconBorrow } from "./IconBorrow";
export { IconCross } from "./IconCross";
export { IconEarn } from "./IconEarn";
export { IconExternal } from "./IconExternal";
export { IconLeverage } from "./IconLeverage";
export { IconPlus } from "./IconPlus";
export { IconStake } from "./IconStake";
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/account.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/arrow-back.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/arrow-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/borrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/cross.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/earn.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/external.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/leverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/icons/svg/stake.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/uikit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type { BrandColorName, ThemeColorName, ThemeDescriptor } from "./Theme/Th

export { Button } from "./Button/Button";
export { FormField } from "./FormField/FormField";
export * from "./icons";
export { TextInput } from "./Input/TextInput";
export { InputField } from "./InputField/InputField";
export { Modal } from "./Modal/Modal";
Expand Down
Loading

0 comments on commit 13cb1b4

Please sign in to comment.