Skip to content

Instantly share code, notes, and snippets.

@djalmajr
Last active December 29, 2024 12:43
Show Gist options
  • Save djalmajr/5d3395f9cf086196331fe4f9d1ed7fb2 to your computer and use it in GitHub Desktop.
Save djalmajr/5d3395f9cf086196331fe4f9d1ed7fb2 to your computer and use it in GitHub Desktop.
Iconify for react-native

Motivation: oktaysenkan/monicon#54 (comment)

// ./node_modules/metro/src/node-haste/DependencyGraph.js

// https://github.com/facebook/metro/issues/330
getSha1(filename) {
  const sha1 = this._fileSystem.getSha1(filename);
  if (!sha1) {
    function getFileHash(file) {
      return require("node:crypto")
        .createHash("sha1")
        .update(fs.readFileSync(file))
        .digest("hex");
    }
    return getFileHash(fs.realpathSync(filename));
  }
  return sha1;
}
{
"platforms": "universal",
"aliases": {
"components": "~/components",
"lib": "~/utils"
}
}
/* ~/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 34.9%;
--primary: 208.83 100% 54.71%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 93%;
--input: 240 5.9% 90%;
--radius: 0.5rem;
--ring: 240 5% 64.9%;
/* Components */
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 240 5% 64.9%;
}
.dark:root {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
/* Components */
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
html {
@apply scroll-smooth;
}
body {
@apply bg-background text-foreground overscroll-none;
/* font-feature-settings: "rlig" 1, "calt" 1; */
font-synthesis-weight: none;
text-rendering: optimizeLegibility;
}
@supports (font: -apple-system-body) and (-webkit-appearance: none) {
[data-wrapper] {
@apply min-[1800px]:border-t;
}
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 5px;
}
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
.chunk-container {
@apply shadow-none;
}
.chunk-container::after {
content: "";
@apply absolute -inset-4 shadow-xl rounded-xl border;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.border-grid {
@apply border-border/30 dark:border-border;
}
.container-wrapper {
@apply min-[1800px]:max-w-[1536px] min-[1800px]:border-x border-border/30 dark:border-border mx-auto w-full;
}
.container {
@apply px-4 xl:px-6 2xl:px-4 mx-auto max-w-[1536px];
}
}
// ~/components/icon.tsx
import { cssInterop } from "nativewind";
import type { TextStyle } from "react-native";
import { SvgXml, type XmlProps } from "react-native-svg";
type IconProps = XmlProps & {
color?: string;
size?: number;
};
type IconInstance = React.FC<Omit<IconProps, "xml">>;
cssInterop(Icon, {
className: {
target: "style",
nativeStyleToProp: {
color: true,
height: true,
width: true,
},
},
});
function Icon({ color, height, size, style, width, ...rest }: IconProps) {
size ||= (style as TextStyle)?.fontSize || 24;
return (
<SvgXml
{...rest}
color={color || "#555"}
height={height || size}
width={width || size}
style={style}
/>
);
}
export { Icon };
export type { IconInstance, IconProps };
const path = require("node:path");
const fs = require("node:fs");
const ROOT = process.cwd();
/**
* @param {import('expo/metro-config').MetroConfig} config
*/
function withIconify(config, { iconsDir = "src/icons" } = {}) {
const iconsPath = path.join(ROOT, iconsDir);
if (!fs.existsSync(iconsPath)) fs.mkdirSync(iconsPath, { recursive: true });
config.resolver.resolveRequest = (context, moduleName, platform) => {
const [, collection, iconName] = moduleName.match(/^~\/icons\/(.+)\/(.+)$/) || [];
if (!collection || !iconName) {
return context.resolveRequest(context, moduleName, platform);
}
const collectionPath = path.join(iconsPath, collection);
if (!fs.existsSync(collectionPath)) fs.mkdirSync(collectionPath);
const filePath = path.join(collectionPath, `${iconName}.tsx`);
if (fs.existsSync(filePath)) return { type: "sourceFile", filePath };
try {
const jsonPath = path.join(
ROOT,
"node_modules",
"@iconify",
"json",
"json",
`${collection}.json`,
);
if (!fs.existsSync(jsonPath)) {
console.error(`Icon set file not found: ${jsonPath}`);
return null;
}
const iconSet = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
const icon = iconSet.icons[iconName];
if (!icon) {
console.error(`Icon not found: ${iconName} in collection ${collection}`);
return null;
}
const file = `// Auto-generated file
import { Icon, type IconProps } from "~/components/icon";
const xml = \`
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 ${icon.width || iconSet.width} ${icon.height || iconSet.height}"
>
${icon.body}
</svg>
\`;
export default (props: Omit<IconProps, "xml">) => <Icon {...props} xml={xml} />;
`;
fs.writeFileSync(filePath, file);
return { type: "sourceFile", filePath };
} catch (error) {
console.error("Icon Resolution Error:", error, { collection, iconName });
return null;
}
};
return config;
}
module.exports = { withIconify };
/**
* @param {import('expo/metro-config').MetroConfig} config
*/
function withSvg(config) {
return {
...config,
resolver: {
...config.resolver,
assetExts: config.resolver.assetExts.filter((ext) => ext !== "svg"),
sourceExts: [...config.resolver.sourceExts, "svg"],
},
transformer: {
...config.transformer,
babelTransformerPath: require.resolve("react-native-svg-transformer"),
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: true,
},
}),
},
};
}
module.exports = { withSvg };
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const { withIconify } = require("./metro-iconify");
const { withSvg } = require("./metro-svg");
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
// Nativewind should be the outermost
module.exports = withNativeWind(withIconify(withSvg(config)), { input: "./global.css" });
/** @type {import('tailwindcss').Config} */
const { hairlineWidth } = require("nativewind/theme");
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: ["./src/**/*.{ts,tsx}"], // "!./**/node_modules"
future: {
hoverOnlyWhenSupported: true,
},
presets: [require("nativewind/preset")],
plugins: [require("tailwindcss-animate")],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
borderWidth: {
hairline: hairlineWidth(),
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"fade-in": {
from: { opacity: "0" },
to: { opacity: "1" },
},
},
animation: {
"fade-in": "fade-in 0.3s ease-out",
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
};
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"paths": { "~/*": ["./src/*"] }
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts"]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment