diff --git a/.gitignore b/.gitignore
index 08ad3fc7..6d5ce648 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,8 +10,6 @@ packages/**/dist/
packages/**/lib/
packages/**/types/
storybook-static/
-packages/react/src
-packages/vue/src
**/stats.html
.idea
/.yarnrc
diff --git a/apps/react-nextjs-app-router/.gitignore b/apps/react-nextjs-app-router/.gitignore
new file mode 100644
index 00000000..5ef6a520
--- /dev/null
+++ b/apps/react-nextjs-app-router/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/apps/react-nextjs-app-router/README.md b/apps/react-nextjs-app-router/README.md
new file mode 100644
index 00000000..e215bc4c
--- /dev/null
+++ b/apps/react-nextjs-app-router/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/apps/react-nextjs-app-router/eslint.config.mjs b/apps/react-nextjs-app-router/eslint.config.mjs
new file mode 100644
index 00000000..c85fb67c
--- /dev/null
+++ b/apps/react-nextjs-app-router/eslint.config.mjs
@@ -0,0 +1,16 @@
+import { dirname } from "path";
+import { fileURLToPath } from "url";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
+];
+
+export default eslintConfig;
diff --git a/apps/react-nextjs-app-router/next.config.ts b/apps/react-nextjs-app-router/next.config.ts
new file mode 100644
index 00000000..e9ffa308
--- /dev/null
+++ b/apps/react-nextjs-app-router/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ /* config options here */
+};
+
+export default nextConfig;
diff --git a/apps/react-nextjs-app-router/package.json b/apps/react-nextjs-app-router/package.json
new file mode 100644
index 00000000..8522cc4c
--- /dev/null
+++ b/apps/react-nextjs-app-router/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "react-nextjs-app-router",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --turbopack",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@interchain-ui/react": "workspace:*",
+ "clsx": "^2.1.1",
+ "next": "15.2.3",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3",
+ "@tailwindcss/postcss": "^4",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "15.2.3",
+ "tailwindcss": "^4",
+ "typescript": "^5"
+ }
+}
\ No newline at end of file
diff --git a/apps/react-nextjs-app-router/postcss.config.mjs b/apps/react-nextjs-app-router/postcss.config.mjs
new file mode 100644
index 00000000..c7bcb4b1
--- /dev/null
+++ b/apps/react-nextjs-app-router/postcss.config.mjs
@@ -0,0 +1,5 @@
+const config = {
+ plugins: ["@tailwindcss/postcss"],
+};
+
+export default config;
diff --git a/apps/react-nextjs-app-router/public/file.svg b/apps/react-nextjs-app-router/public/file.svg
new file mode 100644
index 00000000..004145cd
--- /dev/null
+++ b/apps/react-nextjs-app-router/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/react-nextjs-app-router/public/globe.svg b/apps/react-nextjs-app-router/public/globe.svg
new file mode 100644
index 00000000..567f17b0
--- /dev/null
+++ b/apps/react-nextjs-app-router/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/react-nextjs-app-router/public/next.svg b/apps/react-nextjs-app-router/public/next.svg
new file mode 100644
index 00000000..5174b28c
--- /dev/null
+++ b/apps/react-nextjs-app-router/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/react-nextjs-app-router/public/vercel.svg b/apps/react-nextjs-app-router/public/vercel.svg
new file mode 100644
index 00000000..77053960
--- /dev/null
+++ b/apps/react-nextjs-app-router/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/react-nextjs-app-router/public/window.svg b/apps/react-nextjs-app-router/public/window.svg
new file mode 100644
index 00000000..b2b2a44f
--- /dev/null
+++ b/apps/react-nextjs-app-router/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/react-nextjs-app-router/src/app/favicon.ico b/apps/react-nextjs-app-router/src/app/favicon.ico
new file mode 100644
index 00000000..718d6fea
Binary files /dev/null and b/apps/react-nextjs-app-router/src/app/favicon.ico differ
diff --git a/apps/react-nextjs-app-router/src/app/globals.css b/apps/react-nextjs-app-router/src/app/globals.css
new file mode 100644
index 00000000..9159b88a
--- /dev/null
+++ b/apps/react-nextjs-app-router/src/app/globals.css
@@ -0,0 +1,26 @@
+@import "tailwindcss";
+
+:root {
+ --background: #ffffff;
+ --foreground: #171717;
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0a0a0a;
+ --foreground: #ededed;
+ }
+}
+
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: Arial, Helvetica, sans-serif;
+}
\ No newline at end of file
diff --git a/apps/react-nextjs-app-router/src/app/layout.tsx b/apps/react-nextjs-app-router/src/app/layout.tsx
new file mode 100644
index 00000000..56855403
--- /dev/null
+++ b/apps/react-nextjs-app-router/src/app/layout.tsx
@@ -0,0 +1,38 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "./globals.css";
+import "@interchain-ui/react/styles";
+import { ThemeProvider } from "./providers/theme-provider";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "Interchain UI Components",
+ description: "Component playground for Interchain UI components",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/apps/react-nextjs-app-router/src/app/page.tsx b/apps/react-nextjs-app-router/src/app/page.tsx
new file mode 100644
index 00000000..aaed5f71
--- /dev/null
+++ b/apps/react-nextjs-app-router/src/app/page.tsx
@@ -0,0 +1,5 @@
+import HomeScreen from "@/components/screens/home-screen";
+
+export default function Home() {
+ return ;
+}
diff --git a/apps/react-nextjs-app-router/src/app/providers/theme-provider.tsx b/apps/react-nextjs-app-router/src/app/providers/theme-provider.tsx
new file mode 100644
index 00000000..07a78109
--- /dev/null
+++ b/apps/react-nextjs-app-router/src/app/providers/theme-provider.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ ThemeProvider as InterchainThemeProvider,
+ Box,
+ IconButton,
+ useTheme,
+} from "@interchain-ui/react";
+import clsx from "clsx";
+
+const lightTheme = {
+ name: "light",
+ vars: {
+ colors: {
+ background: "#ffffff",
+ foreground: "#171717",
+ },
+ },
+};
+
+const darkTheme = {
+ name: "dark",
+ vars: {
+ colors: {
+ background: "#0a0a0a",
+ foreground: "#ededed",
+ },
+ },
+};
+
+export function ThemeProvider({ children }: { children: React.ReactNode }) {
+ const { theme, themeClass, setTheme } = useTheme();
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ {
+ if (theme === "light") {
+ return setTheme("dark");
+ }
+ return setTheme("light");
+ }}
+ />
+
+
+
+
+ );
+}
diff --git a/apps/react-nextjs-app-router/src/components/screens/home-screen.tsx b/apps/react-nextjs-app-router/src/components/screens/home-screen.tsx
new file mode 100644
index 00000000..71897fcb
--- /dev/null
+++ b/apps/react-nextjs-app-router/src/components/screens/home-screen.tsx
@@ -0,0 +1,346 @@
+"use client";
+
+import { useState } from "react";
+import {
+ Text,
+ Box,
+ Stack,
+ Container,
+ Button,
+ Avatar,
+ Tooltip,
+ IconButton,
+ TextField,
+ Spinner,
+ Icon,
+ Divider,
+ useTheme,
+ Tabs,
+} from "@interchain-ui/react";
+import { VerticalTabs } from "../ui/vertical-tabs";
+
+// Types
+interface ComponentExample {
+ name: string;
+ component: React.ReactNode;
+ description?: string;
+}
+
+interface ComponentCategory {
+ name: string;
+ examples: ComponentExample[];
+}
+
+// Component categories data
+const componentCategories: ComponentCategory[] = [
+ {
+ name: "Buttons",
+ examples: [
+ {
+ name: "Primary Button",
+ component: ,
+ description: "Default button with primary styling",
+ },
+ {
+ name: "Secondary Button",
+ component: ,
+ description: "Secondary action button",
+ },
+ {
+ name: "Tertiary Button",
+ component: ,
+ description: "Tertiary action button",
+ },
+ {
+ name: "Ghost Button",
+ component: (
+
+ ),
+ description: "Button with ghost variant",
+ },
+ {
+ name: "Outlined Button",
+ component: (
+
+ ),
+ description: "Button with outlined variant",
+ },
+ {
+ name: "Connect Wallet",
+ component: ,
+ description: "Button with left icon",
+ },
+ {
+ name: "Button Sizes",
+ component: (
+
+
+
+
+
+ ),
+ description: "Different button sizes",
+ },
+ ],
+ },
+ {
+ name: "Navigation",
+ examples: [
+ {
+ name: "Basic Tabs",
+ component: (
+
+ Overview content },
+ { label: "Settings", content: Settings content },
+ { label: "Resources", content: Resources content },
+ ]}
+ />
+
+ ),
+ description: "Basic tabbed navigation",
+ },
+ ],
+ },
+ {
+ name: "Data Display",
+ examples: [
+ {
+ name: "Avatar Sizes",
+ component: (
+
+
+
+
+
+ ),
+ description: "Avatar component in different sizes",
+ },
+ {
+ name: "Avatar with Image",
+ component: (
+
+
+
+
+ ),
+ description: "Avatar with image fallback to initials",
+ },
+ {
+ name: "Tooltip Variants",
+ component: (
+
+
+
+
+
+
+
+
+ ),
+ description: "Tooltip variations",
+ },
+ ],
+ },
+ {
+ name: "Feedback",
+ examples: [
+ {
+ name: "Spinner Sizes",
+ component: (
+
+
+
+
+
+ ),
+ description: "Loading spinners in different sizes",
+ },
+ ],
+ },
+ {
+ name: "Icons",
+ examples: [
+ {
+ name: "Common Icons",
+ component: (
+
+
+
+
+
+
+
+
+
+ ),
+ description: "Commonly used icons",
+ },
+ {
+ name: "Icon Sizes",
+ component: (
+
+
+
+
+
+ ),
+ description: "Icons in different sizes",
+ },
+ ],
+ },
+ {
+ name: "Inputs",
+ examples: [
+ {
+ name: "Text Field Variants",
+ component: (
+
+
+
+ ),
+ description: "Text field variations",
+ },
+ ],
+ },
+];
+
+// Client Components
+function ComponentGrid({ category }: { category: ComponentCategory }) {
+ const { theme } = useTheme();
+
+ return (
+
+ {category.examples.map((example, index) => (
+
+
+ {example.name}
+
+ {example.description && (
+
+ {example.description}
+
+ )}
+
+ {example.component}
+
+
+ ))}
+
+ );
+}
+
+// Main Component
+export default function HomeScreen() {
+ const [activeCategory, setActiveCategory] = useState(0);
+ const { theme } = useTheme();
+
+ return (
+
+
+
+
+ Interchain UI Components
+
+
+ Explore our component library and documentation
+
+
+
+ ({
+ label: category.name,
+ content: (
+
+
+ {category.name} Components
+
+
+
+
+
+
+ ),
+ }))}
+ activeTab={activeCategory}
+ onTabChange={setActiveCategory}
+ />
+
+
+ );
+}
diff --git a/apps/react-nextjs-app-router/src/components/ui/vertical-tabs.tsx b/apps/react-nextjs-app-router/src/components/ui/vertical-tabs.tsx
new file mode 100644
index 00000000..7ca19bb1
--- /dev/null
+++ b/apps/react-nextjs-app-router/src/components/ui/vertical-tabs.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { useState } from "react";
+import clsx from "clsx";
+import { useTheme } from "@interchain-ui/react";
+
+interface Tab {
+ label: string;
+ content: React.ReactNode;
+}
+
+interface VerticalTabsProps {
+ tabs: Tab[];
+ activeTab?: number;
+ onTabChange?: (index: number) => void;
+}
+
+export function VerticalTabs({
+ tabs,
+ activeTab: controlledActiveTab,
+ onTabChange,
+}: VerticalTabsProps) {
+ const [internalActiveTab, setInternalActiveTab] = useState(0);
+ const { themeClass } = useTheme();
+
+ // Use controlled or uncontrolled active tab
+ const activeTab =
+ typeof controlledActiveTab !== "undefined"
+ ? controlledActiveTab
+ : internalActiveTab;
+
+ const handleTabClick = (index: number) => {
+ if (onTabChange) {
+ onTabChange(index);
+ } else {
+ setInternalActiveTab(index);
+ }
+ };
+
+ return (
+
+ {/* Tab List */}
+
+
+ {tabs.map((tab, index) => (
+
+ ))}
+
+
+
+ {/* Tab Content */}
+
+
+ {tabs[activeTab]?.content}
+
+
+
+ );
+}
diff --git a/apps/react-nextjs-app-router/tsconfig.json b/apps/react-nextjs-app-router/tsconfig.json
new file mode 100644
index 00000000..c1334095
--- /dev/null
+++ b/apps/react-nextjs-app-router/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/apps/react-nextjs-pages-router/src/pages/index.tsx b/apps/react-nextjs-pages-router/src/pages/index.tsx
index 8a0f8314..900efcdc 100644
--- a/apps/react-nextjs-pages-router/src/pages/index.tsx
+++ b/apps/react-nextjs-pages-router/src/pages/index.tsx
@@ -1,6 +1,21 @@
-import Image from "next/image";
+import { useState } from "react";
import localFont from "next/font/local";
-import { Text, Button, Box, Stack, TextField } from "@interchain-ui/react";
+import Image from "next/image";
+import {
+ Text,
+ Box,
+ Stack,
+ Container,
+ Tabs,
+ Button,
+ Avatar,
+ Tooltip,
+ IconButton,
+ TextField,
+ Spinner,
+ Icon,
+ Divider,
+} from "@interchain-ui/react";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
@@ -13,55 +28,343 @@ const geistMono = localFont({
weight: "100 900",
});
+// Define the structure for our component examples
+interface ComponentExample {
+ name: string;
+ component: React.ReactNode;
+ description?: string;
+}
+
+// Define the structure for our component categories
+interface ComponentCategory {
+ name: string;
+ examples: ComponentExample[];
+}
+
+// Create our component categories with manually defined examples
+const componentCategories: ComponentCategory[] = [
+ {
+ name: "Buttons",
+ examples: [
+ {
+ name: "Primary Button",
+ component: ,
+ description: "Primary action button with default styling",
+ },
+ {
+ name: "Secondary Button",
+ component: ,
+ description: "Secondary action button with muted styling",
+ },
+ {
+ name: "Success Button",
+ component: ,
+ description: "Button indicating a successful action",
+ },
+ {
+ name: "Warning Button",
+ component: ,
+ description: "Button indicating a warning or caution",
+ },
+ {
+ name: "Danger Button",
+ component: ,
+ description: "Button indicating a destructive action",
+ },
+ {
+ name: "Icon Button",
+ component: ,
+ description: "Button with only an icon",
+ },
+ ],
+ },
+ {
+ name: "Layout",
+ examples: [
+ {
+ name: "Accordion Example",
+ component: (
+
+
+ Section 1
+
+
+
+ Content for section 1
+
+
+
+ Section 2
+
+
+
+ Content for section 2
+
+
+ ),
+ description: "Collapsible content sections",
+ },
+ {
+ name: "Stack - Horizontal",
+ component: (
+
+
+ Item 1
+
+
+ Item 2
+
+
+ Item 3
+
+
+ ),
+ description: "Horizontal stack layout with spacing",
+ },
+ {
+ name: "Stack - Vertical",
+ component: (
+
+
+ Item 1
+
+
+ Item 2
+
+
+ Item 3
+
+
+ ),
+ description: "Vertical stack layout with spacing",
+ },
+ ],
+ },
+ {
+ name: "Data Display",
+ examples: [
+ {
+ name: "Avatar",
+ component: (
+
+
+
+
+
+ ),
+ description: "User avatar with initials",
+ },
+ {
+ name: "Tooltip",
+ component: (
+
+
+
+ ),
+ description: "Informational tooltip on hover",
+ },
+ ],
+ },
+ {
+ name: "Feedback",
+ examples: [
+ {
+ name: "Spinner",
+ component: ,
+ description: "Loading indicator",
+ },
+ ],
+ },
+ {
+ name: "Navigation",
+ examples: [
+ {
+ name: "Tabs",
+ component: (
+ Content for Tab 1 },
+ { label: "Tab 2", content: Content for Tab 2 },
+ { label: "Tab 3", content: Content for Tab 3 },
+ ]}
+ />
+ ),
+ description: "Tabbed navigation",
+ },
+ ],
+ },
+ {
+ name: "Icons",
+ examples: [
+ {
+ name: "Basic Icons",
+ component: (
+
+
+
+
+
+
+
+ ),
+ description: "Basic icon set",
+ },
+ ],
+ },
+ {
+ name: "Inputs",
+ examples: [
+ {
+ name: "Text Field",
+ component: (
+
+ ),
+ description: "Basic text input field",
+ },
+ {
+ name: "Text Field with Label",
+ component: (
+
+ ),
+ description: "Text input with label",
+ },
+ ],
+ },
+];
+
export default function Home() {
+ const [activeCategory, setActiveCategory] = useState(
+ componentCategories[0].name,
+ );
+
+ // Find the active category
+ const currentCategory =
+ componentCategories.find((cat) => cat.name === activeCategory) ||
+ componentCategories[0];
+
return (
-
-
-
+
+
+
+
+
+
+ Interchain UI Playground
+
+
+
+
+
-
- Interchain UI React + Next.js Pages Router
-
-
+
+
+ ({
+ label: category.name,
+ content: null, // We're handling content display separately
+ }))}
+ activeTab={componentCategories.findIndex(
+ (cat) => cat.name === activeCategory,
+ )}
+ onActiveTabChange={(index) =>
+ setActiveCategory(componentCategories[index].name)
+ }
+ />
+
-
+
- Component playground
+ {currentCategory.name} Components
+
+
-
-
-
-
-
-
- Custom theme
+
+ {currentCategory.examples.map((example, index) => (
+
+
+ {example.name}
+
+ {example.description && (
+
+ {example.description}
+
+ )}
+
+ {example.component}
+
-
+ ))}
-
+
);
}
diff --git a/package.json b/package.json
index 2167559f..be17a8e6 100644
--- a/package.json
+++ b/package.json
@@ -40,36 +40,36 @@
"test:coverage": "jest --coverage"
},
"devDependencies": {
- "@babel/core": "^7.25.8",
- "@babel/generator": "^7.25.7",
- "@babel/parser": "^7.25.8",
- "@babel/plugin-proposal-decorators": "^7.25.7",
- "@babel/preset-env": "^7.25.8",
- "@babel/preset-react": "^7.25.7",
- "@babel/preset-typescript": "^7.25.7",
- "@babel/traverse": "^7.25.7",
- "@babel/types": "^7.25.8",
+ "@babel/core": "^7.26.9",
+ "@babel/generator": "^7.26.9",
+ "@babel/parser": "^7.26.9",
+ "@babel/plugin-proposal-decorators": "^7.25.9",
+ "@babel/preset-env": "^7.26.9",
+ "@babel/preset-react": "^7.26.3",
+ "@babel/preset-typescript": "^7.26.0",
+ "@babel/traverse": "^7.26.9",
+ "@babel/types": "^7.26.9",
"@builder.io/eslint-plugin-mitosis": "^0.0.16",
- "@builder.io/mitosis": "0.4.3",
- "@builder.io/mitosis-cli": "0.4.3",
+ "@builder.io/mitosis": "^0.4.7",
+ "@builder.io/mitosis-cli": "^0.4.7",
"@formkit/auto-animate": "^0.8.2",
- "@parcel/packager-ts": "^2.12.0",
- "@parcel/transformer-typescript-types": "^2.12.0",
- "@parcel/transformer-vue": "^2.12.0",
- "@parcel/watcher": "^2.4.1",
- "@react-aria/i18n": "^3.12.3",
- "@react-aria/numberfield": "^3.11.8",
- "@react-aria/utils": "^3.25.3",
- "@react-stately/numberfield": "^3.9.7",
- "@swc/helpers": "^0.5.13",
- "@types/animejs": "^3.1.12",
- "@types/node": "^20.16.13",
+ "@parcel/packager-ts": "^2.13.3",
+ "@parcel/transformer-typescript-types": "^2.13.3",
+ "@parcel/transformer-vue": "^2.13.3",
+ "@parcel/watcher": "^2.5.1",
+ "@react-aria/i18n": "^3.12.6",
+ "@react-aria/numberfield": "^3.11.11",
+ "@react-aria/utils": "^3.28.0",
+ "@react-stately/numberfield": "^3.9.10",
+ "@swc/helpers": "^0.5.15",
+ "@types/animejs": "^3.1.13",
+ "@types/node": "^20.17.23",
"@types/react": "latest",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
- "@vitejs/plugin-vue": "^5.1.4",
+ "@vitejs/plugin-vue": "^5.2.1",
"@vue/babel-preset-app": "^5.0.8",
- "@vue/compiler-sfc": "^3.5.12",
+ "@vue/compiler-sfc": "^3.5.13",
"autoprefixer": "^10.4.20",
"cli-color": "^2.0.4",
"command-line-args": "^5.2.1",
@@ -77,12 +77,12 @@
"cross-env": "^7.0.3",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-prettier": "^5.2.1",
+ "eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-storybook": "^0.8.0",
- "fs-extra": "^11.2.0",
- "glob": "^11.0.0",
+ "fs-extra": "^11.3.0",
+ "glob": "^11.0.1",
"gluegun": "^5.2.0",
- "husky": "^9.1.6",
+ "husky": "^9.1.7",
"lerna": "8.1.3",
"lerna-changelog": "^2.2.0",
"listr2": "^5.0.8",
@@ -92,24 +92,24 @@
"ora": "^5.4.1",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
- "prettier": "^3.3.3",
+ "prettier": "^3.5.3",
"process": "^0.11.10",
"rainbow-sprinkles": "^0.17.3",
"replace": "^1.2.2",
"replace-in-file": "^6.3.5",
"rimraf": "^5.0.10",
- "tsx": "^4.19.1",
- "typescript": "^5.6.3",
- "vite": "^5.4.9",
+ "tsx": "^4.19.3",
+ "typescript": "^5.8.2",
+ "vite": "^5.4.14",
"vue": "^3.4.21",
- "vue-metamorph": "^3.2.0",
- "vue-tsc": "^2.1.6"
+ "vue-metamorph": "^3.3.3",
+ "vue-tsc": "^2.2.8"
},
"dependencies": {
- "@floating-ui/core": "^1.6.8",
- "@floating-ui/dom": "^1.6.11",
- "@types/lodash": "^4.17.12",
- "@vanilla-extract/css": "^1.16.0",
+ "@floating-ui/core": "^1.6.9",
+ "@floating-ui/dom": "^1.6.13",
+ "@types/lodash": "^4.17.16",
+ "@vanilla-extract/css": "^1.17.1",
"@vanilla-extract/css-utils": "^0.1.4",
"@vanilla-extract/dynamic": "^2.1.2",
"@vanilla-extract/private": "^1.0.6",
@@ -118,8 +118,8 @@
"bignumber.js": "^9.1.2",
"clsx": "^2.1.1",
"immer": "^10.1.1",
- "vue": "^3.5.12",
- "zustand": "^4.5.5"
+ "vue": "^3.5.13",
+ "zustand": "^4.5.6"
},
"changelog": {
"labels": {
@@ -132,4 +132,4 @@
"chore": "Chore"
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/compiler/log.example.txt b/packages/compiler/log.example.txt
new file mode 100644
index 00000000..4a84fbf5
--- /dev/null
+++ b/packages/compiler/log.example.txt
@@ -0,0 +1,797 @@
+> interchain-ui@0.1.0 t:react /Users/phathag/workspace/cosmology/interchain-ui
+> rm -rf packages/react/src && tsx packages/compiler/src/frameworks/react.compile.ts
+
+- >> Compiling [react]
+packages/react/src/ui/validator-list/validator-token-amount-cell.tsx
+
+[Group: React Component: ValidatorTokenAmountCell]
+Compiling validator-token-amount-cell [ValidatorTokenAmountCell] for React...
+Compiled ValidatorTokenAmountCell successfully
+packages/react/src/ui/validator-list/validator-name-cell.tsx
+packages/react/src/ui/validator-list/validator-list.tsx
+packages/react/src/ui/transfer-item/transfer-item.tsx
+packages/react/src/ui/tooltip/tooltip.tsx
+packages/react/src/ui/token-input/token-input.tsx
+packages/react/src/ui/toast/toaster.tsx
+packages/react/src/ui/toast/toast.tsx
+packages/react/src/ui/timeline/timeline.tsx
+packages/react/src/ui/theme-provider/theme-provider.tsx
+packages/react/src/ui/text-field-addon/text-field-addon.tsx
+packages/react/src/ui/text-field/text-field.tsx
+packages/react/src/ui/text/text.tsx
+packages/react/src/ui/tabs/tabs.tsx
+packages/react/src/ui/table/table.tsx
+packages/react/src/ui/table/table-row.tsx
+packages/react/src/ui/table/table-row-header-cell.tsx
+packages/react/src/ui/table/table-head.tsx
+packages/react/src/ui/table/table-column-header-cell.tsx
+packages/react/src/ui/table/table-cell.tsx
+packages/react/src/ui/table/table-body.tsx
+packages/react/src/ui/swap-token/swap-token.tsx
+packages/react/src/ui/swap-price/swap-price.tsx
+packages/react/src/ui/star-text/star-text.tsx
+packages/react/src/ui/staking-delegate/staking-delegate.tsx
+packages/react/src/ui/staking-delegate/staking-delegate-input.tsx
+packages/react/src/ui/staking-delegate/staking-delegate-card.tsx
+packages/react/src/ui/staking-claim-header/staking-claim-header.tsx
+packages/react/src/ui/staking-asset-header/staking-asset-header.tsx
+packages/react/src/ui/stack/stack.tsx
+packages/react/src/ui/spinner/spinner.tsx
+packages/react/src/ui/skeleton/skeleton.tsx
+packages/react/src/ui/single-chain/single-chain.tsx
+packages/react/src/ui/select-button/select-button.tsx
+packages/react/src/ui/scroll-indicator/scroll-indicator.tsx
+packages/react/src/ui/reveal/reveal.tsx
+packages/react/src/ui/remove-liquidity/remove-liquidity.tsx
+packages/react/src/ui/qrcode/qrcode.tsx
+packages/react/src/ui/progress-bar/progress-bar.tsx
+packages/react/src/ui/pools-header/pools-header.tsx
+packages/react/src/ui/pool-list-item/pool-list-item.tsx
+packages/react/src/ui/pool-list-item/cell-with-title.tsx
+packages/react/src/ui/pool-list-item/apr.tsx
+packages/react/src/ui/pool-list/pool-list.tsx
+packages/react/src/ui/pool-info-header/pool-info-header.tsx
+packages/react/src/ui/pool-card-list/pool-card-list.tsx
+packages/react/src/ui/pool-card/pool-card.tsx
+packages/react/src/ui/pool/components/pool-name/pool-name.tsx
+packages/react/src/ui/overview-transfer/overview-transfer.tsx
+packages/react/src/ui/overlays-manager/overlays-manager.tsx
+packages/react/src/ui/noble/noble-tx-step-item.tsx
+packages/react/src/ui/noble/noble-tx-progress-bar.tsx
+packages/react/src/ui/noble/noble-tx-history-overview-item.tsx
+packages/react/src/ui/noble/noble-tx-estimate.tsx
+packages/react/src/ui/noble/noble-tx-direction-card.tsx
+packages/react/src/ui/noble/noble-tx-chain-route.tsx
+packages/react/src/ui/noble/noble-token-avatar.tsx
+packages/react/src/ui/noble/noble-select-wallet-button.tsx
+packages/react/src/ui/noble/noble-select-token-button.tsx
+packages/react/src/ui/noble/noble-select-network-button.tsx
+packages/react/src/ui/noble/noble-provider.tsx
+packages/react/src/ui/noble/noble-page-title-bar.tsx
+packages/react/src/ui/noble/noble-input.tsx
+packages/react/src/ui/noble/noble-button.tsx
+packages/react/src/ui/nft-transfer/nft-transfer.tsx
+packages/react/src/ui/nft-trait-list-item/nft-trait-list-item.tsx
+packages/react/src/ui/nft-trait-list/nft-trait-list.tsx
+packages/react/src/ui/nft-sell-now/nft-sell-now.tsx
+packages/react/src/ui/nft-profile-card-list/nft-profile-card-list.tsx
+packages/react/src/ui/nft-profile-card/nft-profile-card.tsx
+packages/react/src/ui/nft-profile/nft-profile.tsx
+packages/react/src/ui/nft-mint/nft-mint.tsx
+packages/react/src/ui/nft-minimum-offer/nft-minimum-offer.tsx
+packages/react/src/ui/nft-make-offer/nft-make-offer.tsx
+packages/react/src/ui/nft-fixed-price/nft-fixed-price.tsx
+packages/react/src/ui/nft-fees/nft-fees.tsx
+packages/react/src/ui/nft-detail-top-offers/nft-detail-top-offers.tsx
+packages/react/src/ui/nft-detail-info/nft-detail-info.tsx
+packages/react/src/ui/nft-detail-activity-list-item/nft-detail-activity-list-item.tsx
+packages/react/src/ui/nft-detail-activity-list/nft-detail-activity-list.tsx
+packages/react/src/ui/nft-detail/nft-detail.tsx
+packages/react/src/ui/nft-auction/nft-auction.tsx
+packages/react/src/ui/mesh-staking/mesh-validator-squad-empty.tsx
+packages/react/src/ui/mesh-staking/mesh-tag-button.tsx
+packages/react/src/ui/mesh-staking/mesh-table.tsx
+packages/react/src/ui/mesh-staking/mesh-table-validators-cell.tsx
+packages/react/src/ui/mesh-staking/mesh-table-header-action.tsx
+packages/react/src/ui/mesh-staking/mesh-table-chain-cell.tsx
+packages/react/src/ui/mesh-staking/mesh-table-apr-cell.tsx
+packages/react/src/ui/mesh-staking/mesh-tab.tsx
+packages/react/src/ui/mesh-staking/mesh-staking-slider-info.tsx
+packages/react/src/ui/mesh-staking/mesh-provider.tsx
+packages/react/src/ui/mesh-staking/mesh-footer-info-item.tsx
+packages/react/src/ui/mesh-staking/mesh-button.tsx
+packages/react/src/ui/mesh-modal/mesh-modal.tsx
+packages/react/src/ui/manage-liquidity-card/manage-liquidity-card.tsx
+packages/react/src/ui/list-item/list-item.tsx
+packages/react/src/ui/list-for-sale/list-for-sale.tsx
+packages/react/src/ui/liquid-staking/liquid-staking.tsx
+packages/react/src/ui/link/link.tsx
+packages/react/src/ui/interchain-ui-provider/interchain-ui-provider.tsx
+packages/react/src/ui/icon-button/icon-button.tsx
+packages/react/src/ui/icon/icon.tsx
+packages/react/src/ui/i18n-provider/i18n-provider.tsx
+packages/react/src/ui/governance/governance-vote-form.tsx
+packages/react/src/ui/governance/governance-vote-breakdown.tsx
+packages/react/src/ui/governance/governance-result-card.tsx
+packages/react/src/ui/governance/governance-proposal-list.tsx
+packages/react/src/ui/governance/governance-proposal-item.tsx
+packages/react/src/ui/field-label/field-label.tsx
+packages/react/src/ui/fade-in/fade-in.tsx
+packages/react/src/ui/divider/divider.tsx
+packages/react/src/ui/cross-chain/cross-chain.tsx
+packages/react/src/ui/container/container.tsx
+packages/react/src/ui/connected-wallet/connected-wallet.tsx
+packages/react/src/ui/connect-modal-wallet-list/connect-modal-wallet-list.tsx
+packages/react/src/ui/connect-modal-wallet-button/connect-modal-wallet-button.tsx
+packages/react/src/ui/connect-modal-status/connect-modal-status.tsx
+packages/react/src/ui/connect-modal-qrcode-skeleton/connect-modal-qrcode-skeleton.tsx
+packages/react/src/ui/connect-modal-qrcode-error/connect-modal-qrcode-error.tsx
+packages/react/src/ui/connect-modal-qrcode/connect-modal-qrcode.tsx
+packages/react/src/ui/connect-modal-install-button/connect-modal-install-button.tsx
+packages/react/src/ui/connect-modal-head/connect-modal-head.tsx
+packages/react/src/ui/connect-modal/connect-modal.tsx
+packages/react/src/ui/clipboard-copy-text/clipboard-copy-text.tsx
+packages/react/src/ui/circular-progress-bar/cicular-progress-bar.tsx
+packages/react/src/ui/change-chain-list-item/change-chain-list-item.tsx
+packages/react/src/ui/change-chain-input/change-chain-input.tsx
+packages/react/src/ui/change-chain-input/change-chain-input-bold.tsx
+packages/react/src/ui/chain-swap-input/chain-swap-input.tsx
+packages/react/src/ui/chain-list-item/chain-list-item.tsx
+packages/react/src/ui/center/center.tsx
+packages/react/src/ui/carousel/carousel.tsx
+packages/react/src/ui/callout/callout.tsx
+packages/react/src/ui/button/button.tsx
+packages/react/src/ui/breadcrumb/breadcrumb.tsx
+packages/react/src/ui/breadcrumb/breadcrumb-item.tsx
+packages/react/src/ui/box/box.tsx
+packages/react/src/ui/bonding-more/bonding-more.tsx
+packages/react/src/ui/bonding-list-sm/bonding-list-sm.tsx
+packages/react/src/ui/bonding-list-item-sm/bonding-list-item-sm.tsx
+packages/react/src/ui/bonding-list-item/bonding-list-item.tsx
+packages/react/src/ui/bonding-list/bonding-list.tsx
+packages/react/src/ui/bonding-card-list/bonding-card-list.tsx
+packages/react/src/ui/bonding-card/bonding-card.tsx
+packages/react/src/ui/basic-modal/basic-modal.tsx
+packages/react/src/ui/avatar-name/avatar-name.tsx
+packages/react/src/ui/avatar-image/avatar-image.tsx
+packages/react/src/ui/avatar-badge/avatar-badge.tsx
+packages/react/src/ui/avatar/avatar.tsx
+packages/react/src/ui/asset-withdraw-tokens/asset-withdraw-tokens.tsx
+packages/react/src/ui/asset-list-item/asset-list-item.tsx
+packages/react/src/ui/asset-list-header/asset-list-header.tsx
+packages/react/src/ui/asset-list/asset-list.tsx
+packages/react/src/ui/animate-layout/animate-layout.tsx
+packages/react/src/ui/add-liquidity/add-liquidity.tsx
+packages/react/src/ui/accordion/accordion.tsx
+
+[Group: React Component: ValidatorNameCell]
+Compiling validator-name-cell [ValidatorNameCell] for React...
+Compiled ValidatorNameCell successfully
+
+[Group: React Component: ValidatorList]
+Compiling validator-list [ValidatorList] for React...
+Compiled ValidatorList successfully
+
+[Group: React Component: TransferItem]
+Compiling transfer-item [TransferItem] for React...
+Compiled TransferItem successfully
+
+[Group: React Component: Tooltip]
+Compiling tooltip [Tooltip] for React...
+Compiled Tooltip successfully
+
+[Group: React Component: TokenInput]
+Compiling token-input [TokenInput] for React...
+Compiled TokenInput successfully
+✔ [Done] src/ui/accordion/accordion.lite.tsx
+
+[Group: React Component: Toaster]
+Compiling toaster [Toaster] for React...
+Compiled Toaster successfully
+
+[Group: React Component: Toast]
+Compiling toast [Toast] for React...
+Compiled Toast successfully
+
+[Group: React Component: Timeline]
+Compiling timeline [Timeline] for React...
+Compiled Timeline successfully
+
+[Group: React Component: ThemeProvider]
+Compiling theme-provider [ThemeProvider] for React...
+Compiled ThemeProvider successfully
+
+[Group: React Component: TextFieldAddon]
+Compiling text-field-addon [TextFieldAddon] for React...
+Compiled TextFieldAddon successfully
+
+[Group: React Component: TextField]
+Compiling text-field [TextField] for React...
+Compiled TextField successfully
+
+[Group: React Component: Text]
+Compiling text [Text] for React...
+Compiled Text successfully
+
+[Group: React Component: Tabs]
+Compiling tabs [Tabs] for React...
+Compiled Tabs successfully
+
+[Group: React Component: Table]
+Compiling table [Table] for React...
+Compiled Table successfully
+
+[Group: React Component: TableRow]
+Compiling table-row [TableRow] for React...
+Compiled TableRow successfully
+
+[Group: React Component: TableRowHeaderCell]
+Compiling table-row-header-cell [TableRowHeaderCell] for React...
+Compiled TableRowHeaderCell successfully
+
+[Group: React Component: TableHead]
+Compiling table-head [TableHead] for React...
+Compiled TableHead successfully
+
+[Group: React Component: TableColumnHeaderCell]
+Compiling table-column-header-cell [TableColumnHeaderCell] for React...
+Compiled TableColumnHeaderCell successfully
+
+[Group: React Component: TableCell]
+Compiling table-cell [TableCell] for React...
+Compiled TableCell successfully
+
+[Group: React Component: TableBody]
+Compiling table-body [TableBody] for React...
+Compiled TableBody successfully
+
+[Group: React Component: SwapToken]
+Compiling swap-token [SwapToken] for React...
+Compiled SwapToken successfully
+
+[Group: React Component: SwapPrice]
+Compiling swap-price [SwapPrice] for React...
+Compiled SwapPrice successfully
+
+[Group: React Component: StarText]
+Compiling star-text [StarText] for React...
+Compiled StarText successfully
+
+[Group: React Component: StakingDelegate]
+Compiling staking-delegate [StakingDelegate] for React...
+Compiled StakingDelegate successfully
+
+[Group: React Component: StakingDelegateInput]
+Compiling staking-delegate-input [StakingDelegateInput] for React...
+Compiled StakingDelegateInput successfully
+
+[Group: React Component: StakingDelegateCard]
+Compiling staking-delegate-card [StakingDelegateCard] for React...
+Compiled StakingDelegateCard successfully
+
+[Group: React Component: StakingClaimHeader]
+Compiling staking-claim-header [StakingClaimHeader] for React...
+Compiled StakingClaimHeader successfully
+
+[Group: React Component: StakingAssetHeader]
+Compiling staking-asset-header [StakingAssetHeader] for React...
+Compiled StakingAssetHeader successfully
+
+[Group: React Component: Stack]
+Compiling stack [Stack] for React...
+Compiled Stack successfully
+
+[Group: React Component: Spinner]
+Compiling spinner [Spinner] for React...
+Compiled Spinner successfully
+
+[Group: React Component: Skeleton]
+Compiling skeleton [Skeleton] for React...
+Compiled Skeleton successfully
+
+[Group: React Component: SingleChain]
+Compiling single-chain [SingleChain] for React...
+Compiled SingleChain successfully
+
+[Group: React Component: SelectButton]
+Compiling select-button [SelectButton] for React...
+Compiled SelectButton successfully
+
+[Group: React Component: ScrollIndicator]
+Compiling scroll-indicator [ScrollIndicator] for React...
+Compiled ScrollIndicator successfully
+
+[Group: React Component: Reveal]
+Compiling reveal [Reveal] for React...
+Compiled Reveal successfully
+
+[Group: React Component: RemoveLiquidity]
+Compiling remove-liquidity [RemoveLiquidity] for React...
+Compiled RemoveLiquidity successfully
+
+[Group: React Component: Qrcode]
+Compiling qrcode [Qrcode] for React...
+Compiled Qrcode successfully
+
+[Group: React Component: ProgressBar]
+Compiling progress-bar [ProgressBar] for React...
+Compiled ProgressBar successfully
+
+[Group: React Component: PoolsHeader]
+Compiling pools-header [PoolsHeader] for React...
+Compiled PoolsHeader successfully
+
+[Group: React Component: PoolListItem]
+Compiling pool-list-item [PoolListItem] for React...
+Compiled PoolListItem successfully
+
+[Group: React Component: CellWithTitle]
+Compiling cell-with-title [CellWithTitle] for React...
+Compiled CellWithTitle successfully
+
+[Group: React Component: Apr]
+Compiling apr [Apr] for React...
+Compiled Apr successfully
+
+[Group: React Component: PoolList]
+Compiling pool-list [PoolList] for React...
+Compiled PoolList successfully
+
+[Group: React Component: PoolInfoHeader]
+Compiling pool-info-header [PoolInfoHeader] for React...
+Compiled PoolInfoHeader successfully
+
+[Group: React Component: PoolCardList]
+Compiling pool-card-list [PoolCardList] for React...
+Compiled PoolCardList successfully
+
+[Group: React Component: PoolCard]
+Compiling pool-card [PoolCard] for React...
+Compiled PoolCard successfully
+
+[Group: React Component: PoolName]
+Compiling pool-name [PoolName] for React...
+Compiled PoolName successfully
+
+[Group: React Component: OverviewTransfer]
+Compiling overview-transfer [OverviewTransfer] for React...
+Compiled OverviewTransfer successfully
+
+[Group: React Component: OverlaysManager]
+Compiling overlays-manager [OverlaysManager] for React...
+Compiled OverlaysManager successfully
+
+[Group: React Component: NobleTxStepItem]
+Compiling noble-tx-step-item [NobleTxStepItem] for React...
+Compiled NobleTxStepItem successfully
+
+[Group: React Component: NobleTxProgressBar]
+Compiling noble-tx-progress-bar [NobleTxProgressBar] for React...
+Compiled NobleTxProgressBar successfully
+
+[Group: React Component: NobleTxHistoryOverviewItem]
+Compiling noble-tx-history-overview-item [NobleTxHistoryOverviewItem] for React...
+Compiled NobleTxHistoryOverviewItem successfully
+
+[Group: React Component: NobleTxEstimate]
+Compiling noble-tx-estimate [NobleTxEstimate] for React...
+Compiled NobleTxEstimate successfully
+
+[Group: React Component: NobleTxDirectionCard]
+Compiling noble-tx-direction-card [NobleTxDirectionCard] for React...
+Compiled NobleTxDirectionCard successfully
+
+[Group: React Component: NobleTxChainRoute]
+Compiling noble-tx-chain-route [NobleTxChainRoute] for React...
+Compiled NobleTxChainRoute successfully
+
+[Group: React Component: NobleTokenAvatar]
+Compiling noble-token-avatar [NobleTokenAvatar] for React...
+Compiled NobleTokenAvatar successfully
+
+[Group: React Component: NobleSelectWalletButton]
+Compiling noble-select-wallet-button [NobleSelectWalletButton] for React...
+Compiled NobleSelectWalletButton successfully
+
+[Group: React Component: NobleSelectTokenButton]
+Compiling noble-select-token-button [NobleSelectTokenButton] for React...
+Compiled NobleSelectTokenButton successfully
+
+[Group: React Component: NobleSelectNetworkButton]
+Compiling noble-select-network-button [NobleSelectNetworkButton] for React...
+Compiled NobleSelectNetworkButton successfully
+
+[Group: React Component: NobleProvider]
+Compiling noble-provider [NobleProvider] for React...
+Compiled NobleProvider successfully
+
+[Group: React Component: NoblePageTitleBar]
+Compiling noble-page-title-bar [NoblePageTitleBar] for React...
+Compiled NoblePageTitleBar successfully
+
+[Group: React Component: NobleInput]
+Compiling noble-input [NobleInput] for React...
+Compiled NobleInput successfully
+
+[Group: React Component: NobleButton]
+Compiling noble-button [NobleButton] for React...
+Compiled NobleButton successfully
+
+[Group: React Component: NftTransfer]
+Compiling nft-transfer [NftTransfer] for React...
+Compiled NftTransfer successfully
+
+[Group: React Component: NftTraitListItem]
+Compiling nft-trait-list-item [NftTraitListItem] for React...
+Compiled NftTraitListItem successfully
+
+[Group: React Component: NftTraitList]
+Compiling nft-trait-list [NftTraitList] for React...
+Compiled NftTraitList successfully
+
+[Group: React Component: NftSellNow]
+Compiling nft-sell-now [NftSellNow] for React...
+Compiled NftSellNow successfully
+
+[Group: React Component: NftProfileCardList]
+Compiling nft-profile-card-list [NftProfileCardList] for React...
+Compiled NftProfileCardList successfully
+
+[Group: React Component: NftProfileCard]
+Compiling nft-profile-card [NftProfileCard] for React...
+Compiled NftProfileCard successfully
+
+[Group: React Component: NftProfile]
+Compiling nft-profile [NftProfile] for React...
+Compiled NftProfile successfully
+
+[Group: React Component: NftMint]
+Compiling nft-mint [NftMint] for React...
+Compiled NftMint successfully
+
+[Group: React Component: NftMinimumOffer]
+Compiling nft-minimum-offer [NftMinimumOffer] for React...
+Compiled NftMinimumOffer successfully
+
+[Group: React Component: NftMakeOffer]
+Compiling nft-make-offer [NftMakeOffer] for React...
+Compiled NftMakeOffer successfully
+
+[Group: React Component: NftFixedPrice]
+Compiling nft-fixed-price [NftFixedPrice] for React...
+Compiled NftFixedPrice successfully
+
+[Group: React Component: NftFees]
+Compiling nft-fees [NftFees] for React...
+Compiled NftFees successfully
+
+[Group: React Component: NftDetailTopOffers]
+Compiling nft-detail-top-offers [NftDetailTopOffers] for React...
+Compiled NftDetailTopOffers successfully
+
+[Group: React Component: NftDetailInfo]
+Compiling nft-detail-info [NftDetailInfo] for React...
+Compiled NftDetailInfo successfully
+
+[Group: React Component: NftDetailActivityListItem]
+Compiling nft-detail-activity-list-item [NftDetailActivityListItem] for React...
+Compiled NftDetailActivityListItem successfully
+
+[Group: React Component: NftDetailActivityList]
+Compiling nft-detail-activity-list [NftDetailActivityList] for React...
+Compiled NftDetailActivityList successfully
+
+[Group: React Component: NftDetail]
+Compiling nft-detail [NftDetail] for React...
+Compiled NftDetail successfully
+
+[Group: React Component: NftAuction]
+Compiling nft-auction [NftAuction] for React...
+Compiled NftAuction successfully
+
+[Group: React Component: MeshValidatorSquadEmpty]
+Compiling mesh-validator-squad-empty [MeshValidatorSquadEmpty] for React...
+Compiled MeshValidatorSquadEmpty successfully
+
+[Group: React Component: MeshTagButton]
+Compiling mesh-tag-button [MeshTagButton] for React...
+Compiled MeshTagButton successfully
+
+[Group: React Component: MeshTable]
+Compiling mesh-table [MeshTable] for React...
+Compiled MeshTable successfully
+
+[Group: React Component: MeshTableValidatorsCell]
+Compiling mesh-table-validators-cell [MeshTableValidatorsCell] for React...
+Compiled MeshTableValidatorsCell successfully
+
+[Group: React Component: MeshTableHeaderAction]
+Compiling mesh-table-header-action [MeshTableHeaderAction] for React...
+Compiled MeshTableHeaderAction successfully
+
+[Group: React Component: MeshTableChainCell]
+Compiling mesh-table-chain-cell [MeshTableChainCell] for React...
+Compiled MeshTableChainCell successfully
+
+[Group: React Component: MeshTableAprCell]
+Compiling mesh-table-apr-cell [MeshTableAprCell] for React...
+Compiled MeshTableAprCell successfully
+
+[Group: React Component: MeshTab]
+Compiling mesh-tab [MeshTab] for React...
+Compiled MeshTab successfully
+
+[Group: React Component: MeshStakingSliderInfo]
+Compiling mesh-staking-slider-info [MeshStakingSliderInfo] for React...
+Compiled MeshStakingSliderInfo successfully
+
+[Group: React Component: MeshProvider]
+Compiling mesh-provider [MeshProvider] for React...
+Compiled MeshProvider successfully
+
+[Group: React Component: MeshFooterInfoItem]
+Compiling mesh-footer-info-item [MeshFooterInfoItem] for React...
+Compiled MeshFooterInfoItem successfully
+
+[Group: React Component: MeshButton]
+Compiling mesh-button [MeshButton] for React...
+Compiled MeshButton successfully
+
+[Group: React Component: MeshModal]
+Compiling mesh-modal [MeshModal] for React...
+Compiled MeshModal successfully
+
+[Group: React Component: ManageLiquidityCard]
+Compiling manage-liquidity-card [ManageLiquidityCard] for React...
+Compiled ManageLiquidityCard successfully
+
+[Group: React Component: ListItem]
+Compiling list-item [ListItem] for React...
+Compiled ListItem successfully
+
+[Group: React Component: ListForSale]
+Compiling list-for-sale [ListForSale] for React...
+Compiled ListForSale successfully
+
+[Group: React Component: LiquidStaking]
+Compiling liquid-staking [LiquidStaking] for React...
+Compiled LiquidStaking successfully
+
+[Group: React Component: Link]
+Compiling link [Link] for React...
+Compiled Link successfully
+
+[Group: React Component: InterchainUiProvider]
+Compiling interchain-ui-provider [InterchainUiProvider] for React...
+Compiled InterchainUiProvider successfully
+
+[Group: React Component: IconButton]
+Compiling icon-button [IconButton] for React...
+Compiled IconButton successfully
+
+[Group: React Component: Icon]
+Compiling icon [Icon] for React...
+Compiled Icon successfully
+
+[Group: React Component: I18NProvider]
+Compiling i18n-provider [I18NProvider] for React...
+Compiled I18NProvider successfully
+
+[Group: React Component: GovernanceVoteForm]
+Compiling governance-vote-form [GovernanceVoteForm] for React...
+Compiled GovernanceVoteForm successfully
+
+[Group: React Component: GovernanceVoteBreakdown]
+Compiling governance-vote-breakdown [GovernanceVoteBreakdown] for React...
+Compiled GovernanceVoteBreakdown successfully
+
+[Group: React Component: GovernanceResultCard]
+Compiling governance-result-card [GovernanceResultCard] for React...
+Compiled GovernanceResultCard successfully
+
+[Group: React Component: GovernanceProposalList]
+Compiling governance-proposal-list [GovernanceProposalList] for React...
+Compiled GovernanceProposalList successfully
+
+[Group: React Component: GovernanceProposalItem]
+Compiling governance-proposal-item [GovernanceProposalItem] for React...
+Compiled GovernanceProposalItem successfully
+
+[Group: React Component: FieldLabel]
+Compiling field-label [FieldLabel] for React...
+Compiled FieldLabel successfully
+
+[Group: React Component: FadeIn]
+Compiling fade-in [FadeIn] for React...
+Compiled FadeIn successfully
+
+[Group: React Component: Divider]
+Compiling divider [Divider] for React...
+Compiled Divider successfully
+
+[Group: React Component: CrossChain]
+Compiling cross-chain [CrossChain] for React...
+Compiled CrossChain successfully
+
+[Group: React Component: Container]
+Compiling container [Container] for React...
+Compiled Container successfully
+
+[Group: React Component: ConnectedWallet]
+Compiling connected-wallet [ConnectedWallet] for React...
+Compiled ConnectedWallet successfully
+
+[Group: React Component: ConnectModalWalletList]
+Compiling connect-modal-wallet-list [ConnectModalWalletList] for React...
+Compiled ConnectModalWalletList successfully
+
+[Group: React Component: ConnectModalWalletButton]
+Compiling connect-modal-wallet-button [ConnectModalWalletButton] for React...
+Compiled ConnectModalWalletButton successfully
+
+[Group: React Component: ConnectModalStatus]
+Compiling connect-modal-status [ConnectModalStatus] for React...
+Compiled ConnectModalStatus successfully
+
+[Group: React Component: ConnectModalQrcodeSkeleton]
+Compiling connect-modal-qrcode-skeleton [ConnectModalQrcodeSkeleton] for React...
+Compiled ConnectModalQrcodeSkeleton successfully
+
+[Group: React Component: ConnectModalQrcodeError]
+Compiling connect-modal-qrcode-error [ConnectModalQrcodeError] for React...
+Compiled ConnectModalQrcodeError successfully
+
+[Group: React Component: ConnectModalQrcode]
+Compiling connect-modal-qrcode [ConnectModalQrcode] for React...
+Compiled ConnectModalQrcode successfully
+
+[Group: React Component: ConnectModalInstallButton]
+Compiling connect-modal-install-button [ConnectModalInstallButton] for React...
+Compiled ConnectModalInstallButton successfully
+
+[Group: React Component: ConnectModalHead]
+Compiling connect-modal-head [ConnectModalHead] for React...
+Compiled ConnectModalHead successfully
+
+[Group: React Component: ConnectModal]
+Compiling connect-modal [ConnectModal] for React...
+Compiled ConnectModal successfully
+
+[Group: React Component: ClipboardCopyText]
+Compiling clipboard-copy-text [ClipboardCopyText] for React...
+Compiled ClipboardCopyText successfully
+
+[Group: React Component: CicularProgressBar]
+Compiling cicular-progress-bar [CicularProgressBar] for React...
+Compiled CicularProgressBar successfully
+
+[Group: React Component: ChangeChainListItem]
+Compiling change-chain-list-item [ChangeChainListItem] for React...
+Compiled ChangeChainListItem successfully
+
+[Group: React Component: ChangeChainInput]
+Compiling change-chain-input [ChangeChainInput] for React...
+Compiled ChangeChainInput successfully
+
+[Group: React Component: ChangeChainInputBold]
+Compiling change-chain-input-bold [ChangeChainInputBold] for React...
+Compiled ChangeChainInputBold successfully
+
+[Group: React Component: ChainSwapInput]
+Compiling chain-swap-input [ChainSwapInput] for React...
+Compiled ChainSwapInput successfully
+
+[Group: React Component: ChainListItem]
+Compiling chain-list-item [ChainListItem] for React...
+Compiled ChainListItem successfully
+
+[Group: React Component: Center]
+Compiling center [Center] for React...
+Compiled Center successfully
+
+[Group: React Component: Carousel]
+Compiling carousel [Carousel] for React...
+Compiled Carousel successfully
+
+[Group: React Component: Callout]
+Compiling callout [Callout] for React...
+Compiled Callout successfully
+
+[Group: React Component: Button]
+Compiling button [Button] for React...
+Compiled Button successfully
+
+[Group: React Component: Breadcrumb]
+Compiling breadcrumb [Breadcrumb] for React...
+Compiled Breadcrumb successfully
+
+[Group: React Component: BreadcrumbItem]
+Compiling breadcrumb-item [BreadcrumbItem] for React...
+Compiled BreadcrumbItem successfully
+
+[Group: React Component: Box]
+Compiling box [Box] for React...
+Compiled Box successfully
+
+[Group: React Component: BondingMore]
+Compiling bonding-more [BondingMore] for React...
+Compiled BondingMore successfully
+
+[Group: React Component: BondingListSm]
+Compiling bonding-list-sm [BondingListSm] for React...
+Compiled BondingListSm successfully
+
+[Group: React Component: BondingListItemSm]
+Compiling bonding-list-item-sm [BondingListItemSm] for React...
+Compiled BondingListItemSm successfully
+
+[Group: React Component: BondingListItem]
+Compiling bonding-list-item [BondingListItem] for React...
+Compiled BondingListItem successfully
+
+[Group: React Component: BondingList]
+Compiling bonding-list [BondingList] for React...
+Compiled BondingList successfully
+
+[Group: React Component: BondingCardList]
+Compiling bonding-card-list [BondingCardList] for React...
+Compiled BondingCardList successfully
+
+[Group: React Component: BondingCard]
+Compiling bonding-card [BondingCard] for React...
+Compiled BondingCard successfully
+
+[Group: React Component: BasicModal]
+Compiling basic-modal [BasicModal] for React...
+Compiled BasicModal successfully
+
+[Group: React Component: AvatarName]
+Compiling avatar-name [AvatarName] for React...
+Compiled AvatarName successfully
+
+[Group: React Component: AvatarImage]
+Compiling avatar-image [AvatarImage] for React...
+Compiled AvatarImage successfully
+
+[Group: React Component: AvatarBadge]
+Compiling avatar-badge [AvatarBadge] for React...
+Compiled AvatarBadge successfully
+
+[Group: React Component: Avatar]
+Compiling avatar [Avatar] for React...
+Compiled Avatar successfully
+
+[Group: React Component: AssetWithdrawTokens]
+Compiling asset-withdraw-tokens [AssetWithdrawTokens] for React...
+Compiled AssetWithdrawTokens successfully
+
+[Group: React Component: AssetListItem]
+Compiling asset-list-item [AssetListItem] for React...
+Compiled AssetListItem successfully
+
+[Group: React Component: AssetListHeader]
+Compiling asset-list-header [AssetListHeader] for React...
+Compiled AssetListHeader successfully
+
+[Group: React Component: AssetList]
+Compiling asset-list [AssetList] for React...
+Compiled AssetList successfully
+
+[Group: React Component: AnimateLayout]
+Compiling animate-layout [AnimateLayout] for React...
+Compiled AnimateLayout successfully
+
+[Group: React Component: AddLiquidity]
+Compiling add-liquidity [AddLiquidity] for React...
+Compiled AddLiquidity successfully
+
+[Group: React Component: Accordion]
+Compiling accordion [Accordion] for React...
+Compiled Accordion successfully
+
+[Group: REACT Compilation]
+Starting compilation for REACT
+Compilation for REACT completed successfully
+
+[Group: React Compilation]
+React compilation completed successfully
diff --git a/packages/compiler/src/base.ts b/packages/compiler/src/base.ts
index df260ed9..894bc097 100644
--- a/packages/compiler/src/base.ts
+++ b/packages/compiler/src/base.ts
@@ -17,9 +17,13 @@ import {
} from "./scaffold.config.js";
import { Cache } from "./cache";
import { fixReactTypeIssues } from "./utils/react.utils";
+import log from "./log";
const { print, filesystem, strings } = require("gluegun");
+// Add fs alias to prevent linter errors when accessing fs methods
+const fsSync = fs;
+
const cache = new Cache();
type ValidTarget = "react" | "vue";
@@ -87,6 +91,11 @@ interface CompileParams {
[key: string]: unknown;
}
+interface CliConfig {
+ elements?: string[];
+ dev?: boolean;
+}
+
export async function compile(rawOptions: CompileParams): Promise {
const { watcherEvents, ...defaultOptions } = rawOptions;
@@ -95,7 +104,7 @@ export async function compile(rawOptions: CompileParams): Promise {
...(defaultOptions as Partial),
};
- const cliConfig = commandLineArgs(optionDefinitions);
+ const cliConfig = commandLineArgs(optionDefinitions) as CliConfig;
// String or array of strings of glob patterns
const elementsFilter = cliConfig.elements
@@ -103,13 +112,14 @@ export async function compile(rawOptions: CompileParams): Promise {
: options.elements;
options.elements = elementsFilter;
- options.isDev = !!cliConfig.dev;
+ options.isDev = Boolean(cliConfig.dev);
const files = cliConfig.elements
? options.elements
: globSync(options.elements);
- const targetAllowList = compileAllowList[options.target as ValidTarget];
+ const targetAllowList =
+ compileAllowList[options.target as ValidTarget] || undefined;
const filteredGlobbedFiles = targetAllowList
? (files as string[]).filter((file: string) => {
@@ -122,415 +132,562 @@ export async function compile(rawOptions: CompileParams): Promise {
})
: files;
- // console.log("[base.ts] filteredGlobbedFiles", filteredGlobbedFiles);
-
const outPath = `${options.dest}/${options.target}`;
- function copyNonMitosisLiteFiles(
- isFirstRun = false,
- scaffoldsExist = false,
- ): void {
- if (!isFirstRun) {
- return;
+ // Create a silent spinner that doesn't output to console
+ const spinner = ora({
+ text: `>> Compiling [${options.target}]`,
+ // Use a null stream to prevent output
+ stream: process.stderr,
+ // Disable the spinner to prevent output
+ isEnabled: false,
+ }).start();
+
+ // Create a compilation group to keep all logs together
+ const compileGroup = log.group(`${options.target.toUpperCase()} Compilation`);
+
+ // Log compilation start
+ compileGroup.info(`Starting compilation for ${options.target.toUpperCase()}`);
+
+ // Save original stdout.write
+ const originalStdoutWrite = process.stdout.write;
+ const originalStderrWrite = process.stderr.write;
+
+ // Create a filter function to prevent file path logging
+ const filterOutput = (
+ chunk: Buffer | string,
+ encoding: BufferEncoding,
+ callback?: (err?: Error) => void,
+ ): boolean => {
+ const str = chunk.toString();
+ // Only allow our log messages through
+ if (
+ str.includes("[Group:") ||
+ str.includes("Compiling") ||
+ str.includes("Compiled")
+ ) {
+ return originalStdoutWrite.call(
+ process.stdout,
+ chunk,
+ encoding,
+ callback,
+ );
}
+ return true;
+ };
- // Move src to all the package folder
- const srcFiles = globSync("src/**/*");
- const allowList = compileAllowList[options.target as ValidTarget];
- const doesTargetHaveAllowList = allowList != null;
-
- srcFiles.forEach((file: string) => {
- const relativePath = path.relative("src", file);
- let destPath = path.join(outPath, "src", relativePath);
-
- if (doesTargetHaveAllowList && !file.startsWith("src/ui/shared")) {
- const isAllowed = allowList.some(
- (allowed: string) =>
- file.includes(`src/ui/${allowed}/`) || !file.startsWith("src/ui/"),
- );
- if (!isAllowed) return;
- }
+ try {
+ // Redirect stdout to filter out file paths
+ process.stdout.write = filterOutput as any;
+ process.stderr.write = filterOutput as any;
+
+ for (const fileName of filteredGlobbedFiles) {
+ const isFirstCompilation =
+ !fs.existsSync(`${outPath}/src`) || options.isDev;
+ const file = path.parse(fileName);
+ const name = file.name.replace(".lite", "");
+
+ // Copying files
+ const { inDir, outDir } = getScaffoldsDirs(outPath);
+ const scaffoldsExist = fs.existsSync(inDir);
+
+ copyNonMitosisLiteFiles(
+ options,
+ outPath,
+ cliConfig,
+ targetAllowList,
+ isFirstCompilation,
+ scaffoldsExist,
+ );
+
+ if (scaffoldsExist) {
+ fs.copySync(inDir, outDir);
- if (fs.lstatSync(file).isDirectory()) {
- fs.ensureDirSync(destPath);
- } else {
+ // For Vue, we need to rename .tsx files to .vue and copy the hooks
if (options.target === "vue") {
- destPath = stripVueJsxExtension(destPath);
+ const scaffoldFiles = globSync(`${outDir}/**/*.tsx`);
+ scaffoldFiles.forEach((file) => {
+ const newFile = file.replace(/\.tsx$/, ".vue");
+ fs.renameSync(file, newFile);
+ });
+
+ // Copy hooks to the correct location
+ const hooksDir = path.join(inDir, "hooks");
+ const vueHooksDir = path.join(outPath, "src", "ui", "hooks");
+ if (fs.existsSync(hooksDir)) {
+ fs.copySync(hooksDir, vueHooksDir);
+ }
}
- fs.copySync(file, destPath);
}
- });
- // For Vue, we need to add .vue to the export statement
- if (options.target === "vue") {
- const reExportIndexFiles = globSync(`${outPath}/src/ui/**/index.ts`);
+ const changed = await cache.isChanged(fileName);
- reExportIndexFiles.forEach((indexFile: string) => {
- const data = fs.readFileSync(indexFile, "utf8");
- const result = addVueExtension(data);
- fs.writeFileSync(indexFile, result, "utf8");
- });
+ if (!changed) {
+ continue;
+ }
- copyFiles(`${outPath}/typings`, `${outPath}/src`);
- }
+ // Compile using Mitosis CLI
+ const { outFile } = await compileMitosisComponent(
+ fileName,
+ options,
+ outPath,
+ );
- // Remove unnecessary files moved
- const unnecessaryFiles = globSync(`${outPath}/src/**/*.lite.tsx`);
- unnecessaryFiles.forEach((element: string) => fs.removeSync(element));
+ replacePropertiesFromCompiledFiles(outFile);
- // Output file to /src
- const targetSrcFiles = globSync(`${outPath}/src/**/*.{ts,tsx}`);
+ if (typeof options.customReplace === "function") {
+ options.customReplace({
+ outFile,
+ isFirstCompilation,
+ name,
+ pascalName: toPascalName(name),
+ outPath,
+ });
+ }
- targetSrcFiles.forEach((element: string) => {
- const data = fs.readFileSync(element, "utf8");
+ // Update spinner text but don't log it directly
+ spinner.text = `[Done] ${fileName}`;
+ }
- let result = removeLiteExtension(
- // Fix alias
- data.replace(/\~\//g, "../../"),
+ if (watcherEvents) {
+ await handleWatcherEvents(
+ watcherEvents,
+ options,
+ outPath,
+ cliConfig,
+ targetAllowList,
);
+ }
- result = applyFixReactTypeIssues(result, element, options.target);
-
- fs.writeFileSync(element, result, "utf8");
- });
-
- let fileExports = "$2";
-
- // Export only the elements we want with matching filters:
- // - CLI flag --elements
- // - allowList
- if (cliConfig.elements || doesTargetHaveAllowList) {
- const filterWithAllowList = (elements: string | string[]): string[] => {
- const elementsToFilter = doesTargetHaveAllowList
- ? (targetAllowList ?? []).map(
- (allowedElement: string) =>
- `src/ui/${allowedElement}/${allowedElement}.lite.tsx`,
- )
- : toArray(elements);
-
- return elementsToFilter;
- };
-
- fileExports = filterWithAllowList(options.elements)
- .map((fileName: string) => {
- const file = path.parse(fileName);
- const name = file.name.replace(".lite", "");
- return `export { default as ${toPascalName(
- name,
- )} } from './${file.dir.replace("src/", "")}';`;
- })
- .join("\n");
+ if (!cache.isPopulated && files) {
+ await cache.build(Array.isArray(files) ? files : [files]);
}
- const indexData = fs.readFileSync(`${outPath}/src/index.ts`, "utf8");
-
- let indexResult = indexData
- // Export only needed components
- .replace(
- /(\/\/ Init Components)(.+?)(\/\/ End Components)/s,
- `$1\n${fileExports}\n$3`,
- )
- .replace(/Platform.Default/g, `Platform.${toPascalName(options.target)}`);
-
- // Adding scaffolds imports to index.ts
- if (scaffoldsExist) {
- const scaffoldConfig =
- options.target === "vue" ? vueScaffoldConfig : reactScaffoldConfig;
- const scaffoldNames = Object.keys(scaffoldConfig).map((name) => ({
- name,
- Comp: toPascalName(name),
- }));
-
- const scaffoldImports = scaffoldNames
- .map((item) => {
- const importPath =
- options.target === "vue"
- ? `./ui/${item.name}/${item.name}.vue`
- : `./ui/${item.name}`;
- return `export { default as ${item.Comp} } from '${importPath}';`;
- })
- .join("\n");
+ // Call the new function for both React and Vue
+ await addFrameworkSpecificPatches(options);
- indexResult = indexResult.replace(
- /(\/\/ Init Components)(.+?)(\/\/ End Components)/s,
- `$1$2${scaffoldImports}\n$3`,
- );
- }
+ spinner.succeed();
+ compileGroup.success(
+ `Compilation for ${options.target.toUpperCase()} completed successfully`,
+ );
+ } catch (error) {
+ compileGroup.error(
+ `Compilation error: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ } finally {
+ // Restore original stdout.write
+ process.stdout.write = originalStdoutWrite;
+ process.stderr.write = originalStderrWrite;
- fs.writeFileSync(`${outPath}/src/index.ts`, indexResult, "utf8");
+ spinner.stop();
+ compileGroup.end();
}
+}
- async function handleWatcherEvents(watcherEvents: Event[]): Promise {
- // Watcher event has shape Array<{ path: , type: 'update' | 'create' | 'delete' }>
- const event = watcherEvents[0];
+function toPascalName(str: string): string {
+ return startCase(str).replace(/\s/g, "");
+}
- const parsedPath = path.parse(event.path);
+function getScaffoldsDirs(rootPath: string): { inDir: string; outDir: string } {
+ return {
+ inDir: `${rootPath}/scaffolds`,
+ outDir: `${rootPath}/src/ui`,
+ };
+}
- const isLiteJSXComponent =
- parsedPath.ext === ".tsx" && parsedPath.name.includes(".lite");
- const isScaffold = parsedPath.dir.includes("scaffolds");
+function removeLiteExtension(fileContent: string): string {
+ return fileContent.replace(/\.lite/g, "");
+}
- let targetPath = path.join(
- outPath,
- parsedPath.dir.slice(parsedPath.dir.indexOf("src")),
- parsedPath.base,
- );
+function toArray(maybeArray: T | T[]): T[] {
+ return Array.isArray(maybeArray) ? maybeArray : [maybeArray];
+}
- if (options.target === "vue") {
- targetPath = stripVueJsxExtension(targetPath);
- }
+function addVueExtension(inputString: string): string {
+ return inputString.replace(/(\.[^"';\s]+)("|')/g, "$1.vue$2");
+}
- if (event.type === "create" || event.type === "update") {
- // Only process non lite jsx files in this handler
- if (isLiteJSXComponent || isScaffold) return;
+async function copyFiles(srcDir: string, destDir: string): Promise {
+ try {
+ // Ensure the destination directory exists, if not create it
+ await fs.mkdir(destDir, { recursive: true });
- try {
- let fileContent = await fsPromise.readFile(event.path, "utf-8");
- fileContent = removeLiteExtension(fileContent);
+ // Read all the files from the source directory
+ const files = await fs.readdir(srcDir);
- fileContent = applyFixReactTypeIssues(
- fileContent,
- event.path,
- options.target,
- );
+ for (const file of files) {
+ // Construct full file paths for both the source and destination
+ const srcFile = path.join(srcDir, file);
+ const destFile = path.join(destDir, file);
- await fsPromise.writeFile(targetPath, fileContent);
- } catch (err) {
- console.log(`handleWatcherEvents() [${event.type}] event error `, err);
+ // Check if the source is indeed a file and not a directory
+ const stat = await fs.stat(srcFile);
+ if (stat.isFile()) {
+ // Copy each file to the destination directory
+ await fs.copyFile(srcFile, destFile);
}
}
+ } catch (error) {
+ log.error(
+ `Error copying files: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+}
+
+function copyNonMitosisLiteFiles(
+ options: CompileOptions,
+ outPath: string,
+ cliConfig: CliConfig,
+ targetAllowList: string[] | undefined,
+ isFirstRun = false,
+ scaffoldsExist = false,
+): void {
+ if (!isFirstRun) {
+ return;
+ }
+
+ // Move src to all the package folder
+ const srcFiles = globSync("src/**/*");
+ const allowList = compileAllowList[options.target as ValidTarget];
+ const doesTargetHaveAllowList = allowList != null;
+
+ // Use a silent copy process that doesn't log file paths
+ srcFiles.forEach((file: string) => {
+ const relativePath = path.relative("src", file);
+ let destPath = path.join(outPath, "src", relativePath);
- if (event.type === "delete") {
- try {
- await fsPromise.unlink(targetPath);
- } catch (err) {
- console.log("handleWatcherEvents() [delete] event error ", err);
+ if (doesTargetHaveAllowList && !file.startsWith("src/ui/shared")) {
+ const isAllowed = allowList.some(
+ (allowed: string) =>
+ file.includes(`src/ui/${allowed}/`) || !file.startsWith("src/ui/"),
+ );
+ if (!isAllowed) return;
+ }
+
+ if (fs.lstatSync(file).isDirectory()) {
+ fs.ensureDirSync(destPath);
+ } else {
+ if (options.target === "vue") {
+ destPath = stripVueJsxExtension(destPath);
}
+ fs.copySync(file, destPath);
}
- }
+ });
- async function compileMitosisComponent(
- filepath: string,
- ): Promise<{ outFile: string }> {
- const file = path.parse(filepath);
- const outFile = `${outPath}/${file.dir}/${file.name.replace(".lite", "")}.${
- options.extension
- }`;
-
- const to =
- options.target === "webcomponents" ? "webcomponent" : options.target;
-
- const configPath = path.resolve(__dirname, "./mitosis.config.js");
-
- await compileCommand.run({
- parameters: {
- options: {
- from: "mitosis",
- to: to,
- out: outFile,
- force: true,
- state: options.state,
- styles: options.styles,
- api: options.api,
- outFile: outPath,
- config: configPath,
- },
- array: [filepath],
- },
- strings: strings,
- filesystem: filesystem,
- print: print,
+ // For Vue, we need to add .vue to the export statement
+ if (options.target === "vue") {
+ const reExportIndexFiles = globSync(`${outPath}/src/ui/**/index.ts`);
+
+ reExportIndexFiles.forEach((indexFile: string) => {
+ const data = fs.readFileSync(indexFile, "utf8");
+ const result = addVueExtension(data);
+ fs.writeFileSync(indexFile, result, "utf8");
});
- return {
- outFile,
- };
+ copyFiles(`${outPath}/typings`, `${outPath}/src`);
}
- async function addFrameworkSpecificPatches(): Promise {
- const targetRootPath = path.resolve(
- cwd(),
- `packages/${options.target}/src`,
- );
- const indexPath = path.resolve(targetRootPath, "index.ts");
- const hooksPath = path.resolve(targetRootPath, "ui", "hooks");
-
- return fsPromise
- .readdir(hooksPath)
- .then((hookFolders) => {
- const hookNamesByFolder = hookFolders.reduce(
- (arr: { folder: string; hookName: string }[], folder: string) => {
- arr.push({ folder, hookName: camelCase(folder) });
- return arr;
- },
- [],
- );
-
- const indexData = fs.readFileSync(indexPath, "utf8");
-
- const hooksExports = hookNamesByFolder
- .map((item) => {
- if (options.target === "vue") {
- // Due to SFC compiler not understanding the barrel file, we need to import the hooks manually
- return `export { default as ${item.hookName} } from './ui/hooks/${item.folder}/${item.folder}';`;
- } else {
- return `export { default as ${item.hookName} } from './ui/hooks/${item.folder}';`;
- }
- })
- .filter((exportLine) => {
- // Don't include exports if it's already there
- return indexData.indexOf(exportLine) === -1;
- })
- .join("\n");
-
- let indexResult = indexData;
-
- if (options.target === "react") {
- const clientOnlyMarker = `import "client-only";`;
- indexResult = `${indexData}\n${clientOnlyMarker}\n${hooksExports}`;
- } else if (options.target === "vue") {
- // For Vue, we don't need the client-only import
- indexResult = `${indexData}\n${hooksExports}`;
- }
+ // Remove unnecessary files moved
+ const unnecessaryFiles = globSync(`${outPath}/src/**/*.lite.tsx`);
+ unnecessaryFiles.forEach((element: string) => fs.removeSync(element));
- // Skip if the result is the same as the original
- if (indexResult === indexData) {
- return;
- }
+ // Output file to /src
+ const targetSrcFiles = globSync(`${outPath}/src/**/*.{ts,tsx}`);
- fs.writeFileSync(indexPath, indexResult, "utf8");
- })
- .catch((err) => {
- console.log(`Failed to add ${options.target} specific patches:`, err);
- });
- }
+ targetSrcFiles.forEach((element: string) => {
+ const data = fs.readFileSync(element, "utf8");
- function replacePropertiesFromCompiledFiles(outFile: string): void {
- const data = fs.readFileSync(outFile, "utf8");
- const result = data
+ let result = removeLiteExtension(
// Fix alias
- .replace(/\~\//g, "../../");
+ data.replace(/\~\//g, "../../"),
+ );
- fs.writeFileSync(outFile, result, "utf8");
- }
+ result = applyFixReactTypeIssues(result, element, options.target);
- const spinner = ora(`>> Compiling [${options.target}]`).start();
+ fs.writeFileSync(element, result, "utf8");
+ });
- for (const fileName of filteredGlobbedFiles) {
- const isFirstCompilation =
- !fs.existsSync(`${outPath}/src`) || options.isDev;
- const file = path.parse(fileName);
- const name = file.name.replace(".lite", "");
+ let fileExports = "$2";
- // Copying files
- const { inDir, outDir } = getScaffoldsDirs(outPath);
- const scaffoldsExist = fs.existsSync(inDir);
+ // Export only the elements we want with matching filters:
+ // - CLI flag --elements
+ // - allowList
+ if (cliConfig.elements || doesTargetHaveAllowList) {
+ const filterWithAllowList = (elements: string | string[]): string[] => {
+ const elementsToFilter = doesTargetHaveAllowList
+ ? (targetAllowList ?? []).map(
+ (allowedElement: string) =>
+ `src/ui/${allowedElement}/${allowedElement}.lite.tsx`,
+ )
+ : toArray(elements);
- copyNonMitosisLiteFiles(isFirstCompilation, scaffoldsExist);
+ return elementsToFilter;
+ };
- if (scaffoldsExist) {
- fs.copySync(inDir, outDir);
+ fileExports = filterWithAllowList(options.elements)
+ .map((fileName: string) => {
+ const file = path.parse(fileName);
+ const name = file.name.replace(".lite", "");
+ return `export { default as ${toPascalName(
+ name,
+ )} } from './${file.dir.replace("src/", "")}';`;
+ })
+ .join("\n");
+ }
- // For Vue, we need to rename .tsx files to .vue and copy the hooks
- if (options.target === "vue") {
- const scaffoldFiles = globSync(`${outDir}/**/*.tsx`);
- scaffoldFiles.forEach((file) => {
- const newFile = file.replace(/\.tsx$/, ".vue");
- fs.renameSync(file, newFile);
- });
+ const indexData = fs.readFileSync(`${outPath}/src/index.ts`, "utf8");
+
+ let indexResult = indexData
+ // Export only needed components
+ .replace(
+ /(\/\/ Init Components)(.+?)(\/\/ End Components)/s,
+ `$1\n${fileExports}\n$3`,
+ )
+ .replace(/Platform.Default/g, `Platform.${toPascalName(options.target)}`);
+
+ // Adding scaffolds imports to index.ts
+ if (scaffoldsExist) {
+ const scaffoldConfig =
+ options.target === "vue" ? vueScaffoldConfig : reactScaffoldConfig;
+ const scaffoldNames = Object.keys(scaffoldConfig).map((name) => ({
+ name,
+ Comp: toPascalName(name),
+ }));
+
+ const scaffoldImports = scaffoldNames
+ .map((item) => {
+ const importPath =
+ options.target === "vue"
+ ? `./ui/${item.name}/${item.name}.vue`
+ : `./ui/${item.name}`;
+ return `export { default as ${item.Comp} } from '${importPath}';`;
+ })
+ .join("\n");
- // Copy hooks to the correct location
- const hooksDir = path.join(inDir, "hooks");
- const vueHooksDir = path.join(outPath, "src", "ui", "hooks");
- if (fs.existsSync(hooksDir)) {
- fs.copySync(hooksDir, vueHooksDir);
- }
- }
- }
+ indexResult = indexResult.replace(
+ /(\/\/ Init Components)(.+?)(\/\/ End Components)/s,
+ `$1$2${scaffoldImports}\n$3`,
+ );
+ }
- const changed = await cache.isChanged(fileName);
+ fs.writeFileSync(`${outPath}/src/index.ts`, indexResult, "utf8");
+}
- if (!changed) {
- continue;
- }
+async function handleWatcherEvents(
+ watcherEvents: Event[],
+ options: CompileOptions,
+ outPath: string,
+ cliConfig: CliConfig,
+ targetAllowList: string[] | undefined,
+): Promise {
+ if (!watcherEvents || watcherEvents.length === 0) return;
+
+ // Process events in batches to reduce I/O operations
+ const eventsByType = {
+ liteTsxComponents: [] as Event[],
+ scaffolds: [] as Event[],
+ otherFiles: [] as Event[],
+ };
- // Compile using Mitosis CLI
- const { outFile } = await compileMitosisComponent(fileName);
+ // Group events by type for batch processing
+ for (const event of watcherEvents) {
+ const parsedPath = path.parse(event.path);
+ const isLiteJSXComponent =
+ parsedPath.ext === ".tsx" && parsedPath.name.includes(".lite");
+ const isScaffold = parsedPath.dir.includes("scaffolds");
+
+ if (isLiteJSXComponent) {
+ eventsByType.liteTsxComponents.push(event);
+ } else if (isScaffold) {
+ eventsByType.scaffolds.push(event);
+ } else {
+ eventsByType.otherFiles.push(event);
+ }
+ }
- replacePropertiesFromCompiledFiles(outFile);
+ // Process regular files (non-lite, non-scaffold)
+ if (eventsByType.otherFiles.length > 0) {
+ const fileOperations = eventsByType.otherFiles.map(async (event) => {
+ const parsedPath = path.parse(event.path);
- if (typeof options.customReplace === "function") {
- options.customReplace({
- outFile,
- isFirstCompilation,
- name,
- pascalName: toPascalName(name),
+ let targetPath = path.join(
outPath,
- });
- }
+ parsedPath.dir.slice(parsedPath.dir.indexOf("src")),
+ parsedPath.base,
+ );
- spinner.text = `[Done] ${fileName}`;
- }
+ if (options.target === "vue") {
+ targetPath = stripVueJsxExtension(targetPath);
+ }
- if (watcherEvents) {
- await handleWatcherEvents(watcherEvents);
- }
+ if (event.type === "create" || event.type === "update") {
+ try {
+ const fileContent = await fsPromise.readFile(event.path, "utf-8");
+
+ // Apply transformations once
+ const transformedContent = applyFixReactTypeIssues(
+ removeLiteExtension(fileContent),
+ event.path,
+ options.target,
+ );
+
+ await fsPromise.mkdir(path.dirname(targetPath), { recursive: true });
+ await fsPromise.writeFile(targetPath, transformedContent);
+ } catch (err) {
+ log.error(
+ `Error processing ${path.basename(event.path)}: ${err instanceof Error ? err.message : String(err)}`,
+ );
+ }
+ } else if (event.type === "delete") {
+ try {
+ // Check if file exists before attempting to delete
+ if (fsSync.existsSync(targetPath)) {
+ await fsPromise.unlink(targetPath);
+ }
+ } catch (err) {
+ log.error(
+ `Error deleting ${path.basename(targetPath)}: ${err instanceof Error ? err.message : String(err)}`,
+ );
+ }
+ }
+ });
- if (!cache.isPopulated && files) {
- await cache.build(Array.isArray(files) ? files : [files]);
+ // Process all file operations in parallel
+ await Promise.all(fileOperations);
}
- // Call the new function for both React and Vue
- await addFrameworkSpecificPatches();
+ // Handle lite components with Mitosis compilation
+ if (eventsByType.liteTsxComponents.length > 0) {
+ for (const event of eventsByType.liteTsxComponents) {
+ if (event.type === "create" || event.type === "update") {
+ try {
+ await compileMitosisComponent(event.path, options, outPath);
+ } catch (err) {
+ log.error(
+ `Error compiling ${path.basename(event.path)}: ${err instanceof Error ? err.message : String(err)}`,
+ );
+ }
+ }
+ }
+ }
- spinner.succeed();
- spinner.stop();
+ // Handle scaffolds if needed
+ if (eventsByType.scaffolds.length > 0) {
+ copyNonMitosisLiteFiles(
+ options,
+ outPath,
+ cliConfig,
+ targetAllowList,
+ false,
+ true,
+ );
+ }
}
-function toPascalName(str: string): string {
- return startCase(str).replace(/\s/g, "");
-}
+async function compileMitosisComponent(
+ filepath: string,
+ options: CompileOptions,
+ outPath: string,
+): Promise<{ outFile: string }> {
+ const file = path.parse(filepath);
+ const outFile = `${outPath}/${file.dir}/${file.name.replace(".lite", "")}.${
+ options.extension
+ }`;
+
+ const to =
+ options.target === "webcomponents" ? "webcomponent" : options.target;
+
+ const configPath = path.resolve(__dirname, "./mitosis.config.js");
+
+ await compileCommand.run({
+ parameters: {
+ options: {
+ from: "mitosis",
+ to: to,
+ out: outFile,
+ force: true,
+ state: options.state,
+ styles: options.styles,
+ api: options.api,
+ outFile: outPath,
+ config: configPath,
+ },
+ array: [filepath],
+ },
+ strings: strings,
+ filesystem: filesystem,
+ print: print,
+ });
-function getScaffoldsDirs(rootPath: string): { inDir: string; outDir: string } {
return {
- inDir: `${rootPath}/scaffolds`,
- outDir: `${rootPath}/src/ui`,
+ outFile,
};
}
-function removeLiteExtension(fileContent: string): string {
- return fileContent.replace(/\.lite/g, "");
-}
-
-function toArray(maybeArray: T | T[]): T[] {
- return Array.isArray(maybeArray) ? maybeArray : [maybeArray];
-}
+async function addFrameworkSpecificPatches(
+ options: CompileOptions,
+): Promise {
+ const targetRootPath = path.resolve(cwd(), `packages/${options.target}/src`);
+ const indexPath = path.resolve(targetRootPath, "index.ts");
+ const hooksPath = path.resolve(targetRootPath, "ui", "hooks");
+
+ return fsPromise
+ .readdir(hooksPath)
+ .then((hookFolders) => {
+ const hookNamesByFolder = hookFolders.reduce(
+ (arr: { folder: string; hookName: string }[], folder: string) => {
+ arr.push({ folder, hookName: camelCase(folder) });
+ return arr;
+ },
+ [],
+ );
-function addVueExtension(inputString: string): string {
- return inputString.replace(/(\.[^"';\s]+)("|')/g, "$1.vue$2");
-}
+ const indexData = fs.readFileSync(indexPath, "utf8");
-async function copyFiles(srcDir: string, destDir: string): Promise {
- try {
- // Ensure the destination directory exists, if not create it
- await fs.mkdir(destDir, { recursive: true });
+ const hooksExports = hookNamesByFolder
+ .map((item) => {
+ if (options.target === "vue") {
+ // Due to SFC compiler not understanding the barrel file, we need to import the hooks manually
+ return `export { default as ${item.hookName} } from './ui/hooks/${item.folder}/${item.folder}';`;
+ } else {
+ return `export { default as ${item.hookName} } from './ui/hooks/${item.folder}';`;
+ }
+ })
+ .filter((exportLine) => {
+ // Don't include exports if it's already there
+ return indexData.indexOf(exportLine) === -1;
+ })
+ .join("\n");
- // Read all the files from the source directory
- const files = await fs.readdir(srcDir);
+ let indexResult = indexData;
- for (const file of files) {
- // Construct full file paths for both the source and destination
- const srcFile = path.join(srcDir, file);
- const destFile = path.join(destDir, file);
+ if (options.target === "react") {
+ const clientOnlyMarker = `import "client-only";`;
+ indexResult = `${indexData}\n${clientOnlyMarker}\n${hooksExports}`;
+ } else if (options.target === "vue") {
+ // For Vue, we don't need the client-only import
+ indexResult = `${indexData}\n${hooksExports}`;
+ }
- // Check if the source is indeed a file and not a directory
- const stat = await fs.stat(srcFile);
- if (stat.isFile()) {
- // Copy each file to the destination directory
- await fs.copyFile(srcFile, destFile);
+ // Skip if the result is the same as the original
+ if (indexResult === indexData) {
+ return;
}
- }
- } catch (error) {
- console.error("Error copying files:", error);
- }
+
+ fs.writeFileSync(indexPath, indexResult, "utf8");
+ })
+ .catch((err) => {
+ log.error(
+ `Failed to add ${options.target} specific patches: ${err instanceof Error ? err.message : String(err)}`,
+ );
+ });
+}
+
+function replacePropertiesFromCompiledFiles(outFile: string): void {
+ const data = fs.readFileSync(outFile, "utf8");
+ const result = data
+ // Fix alias
+ .replace(/\~\//g, "../../");
+
+ fs.writeFileSync(outFile, result, "utf8");
}
diff --git a/packages/compiler/src/cache.ts b/packages/compiler/src/cache.ts
index 2515f703..ec34b88e 100644
--- a/packages/compiler/src/cache.ts
+++ b/packages/compiler/src/cache.ts
@@ -1,6 +1,7 @@
import fs from "fs/promises";
import fsSync from "fs";
import crypto from "crypto";
+import log from "./log";
function replacer(_key: string, value: unknown): unknown {
if (value instanceof Map) {
@@ -15,16 +16,18 @@ function replacer(_key: string, value: unknown): unknown {
class Cache {
private cache: Map;
+ private fileContentCache: Map;
public isPopulated: boolean;
constructor() {
// { : }
this.cache = new Map();
+ this.fileContentCache = new Map();
this.isPopulated = false;
}
async build(filePaths: string[] | undefined = []): Promise {
- if (!filePaths) {
+ if (!filePaths || filePaths.length === 0) {
return;
}
@@ -32,16 +35,11 @@ class Cache {
.filter((filePath) => fsSync.existsSync(filePath))
.map(async (filePath) => {
try {
- const fileContent = await fs.readFile(filePath, "utf8");
+ const fileContent = await this.readFile(filePath);
const hash = this.hash(fileContent);
- // Check if the file still exists and its content hasn't changed
- // before setting the cache to avoid race conditions
- const currentContent = await fs.readFile(filePath, "utf8");
- if (currentContent === fileContent) {
- this.cache.set(filePath, hash);
- }
+ this.cache.set(filePath, hash);
} catch (err) {
- console.error("Cannot build cache, error reading file ", filePath);
+ log.error(`Cannot build cache, error reading file ${filePath}`);
// Don't throw here, allow other files to be processed
}
});
@@ -50,7 +48,9 @@ class Cache {
await Promise.all(promises);
this.isPopulated = true;
} catch (err) {
- console.error("Cannot build cache ", err);
+ log.error(
+ `Cannot build cache: ${err instanceof Error ? err.message : String(err)}`,
+ );
}
}
@@ -59,7 +59,8 @@ class Cache {
}
private hash(source: string): string {
- return crypto.createHash("sha1").update(source).digest("base64");
+ // Using base64url for smaller hash strings (no padding needed)
+ return crypto.createHash("sha1").update(source).digest("base64url");
}
has(filePath: string): boolean {
@@ -73,6 +74,18 @@ class Cache {
set(filePath: string, source: string): void {
const hash = this.hash(source);
this.cache.set(filePath, hash);
+ this.fileContentCache.set(filePath, source);
+ }
+
+ // Read file with caching
+ private async readFile(filePath: string): Promise {
+ if (this.fileContentCache.has(filePath)) {
+ return this.fileContentCache.get(filePath)!;
+ }
+
+ const content = await fs.readFile(filePath, "utf8");
+ this.fileContentCache.set(filePath, content);
+ return content;
}
// Check if file content changed
@@ -82,15 +95,27 @@ class Cache {
}
try {
- const fileContent = await fs.readFile(filePath, "utf8");
+ const fileContent = await this.readFile(filePath);
const currentHash = this.hash(fileContent);
const prevHash = this.cache.get(filePath);
return prevHash !== currentHash;
} catch (err) {
- console.error("isDiff error ", err);
+ log.error(
+ `isDiff error: ${err instanceof Error ? err.message : String(err)}`,
+ );
return true;
}
}
+
+ // Clear file content cache for a specific path
+ clearFileCache(filePath: string): void {
+ this.fileContentCache.delete(filePath);
+ }
+
+ // Clear all file content caches
+ clearAllFileCaches(): void {
+ this.fileContentCache.clear();
+ }
}
export { Cache };
diff --git a/packages/compiler/src/dev.ts b/packages/compiler/src/dev.ts
index fb32afa8..66d8d731 100644
--- a/packages/compiler/src/dev.ts
+++ b/packages/compiler/src/dev.ts
@@ -6,6 +6,12 @@ import lodash from "lodash";
import { compileReact } from "./frameworks/react.compile";
import { command as execa } from "execa";
+// Track pending compilation operations
+const pendingOperations = {
+ isCompiling: false,
+ events: [] as Event[],
+};
+
(async () => {
let unsub: (() => void) | undefined;
@@ -61,59 +67,87 @@ import { command as execa } from "execa";
"packages/react/scaffolds",
);
- const onChange = lodash.debounce(
- (err: Error | null, _events: Array) => {
- const spinner = ora(`Watching src/ for changes...`).start();
- spinner.text = `src/ changed, compiling...`;
-
- const t = +Date.now();
- const timingLabel = `[t:${t}] Recompile took`;
- console.time(timingLabel);
-
- const compilationPromise = compile(_events, {
- cancel: () => {},
- })
- .then(() => {
- spinner.succeed("Compiled successfully.");
- console.timeEnd(timingLabel);
- })
- .catch((e: Error) => {
- spinner.fail(`Error compiling mitosis ${e.message}.`);
- });
+ // Enhanced event handler with batching
+ const handleEvents = async (events: Event[]): Promise => {
+ // Add new events to pending batch
+ pendingOperations.events.push(...events);
+
+ // If already compiling, just return - the events have been queued
+ if (pendingOperations.isCompiling) {
+ return;
+ }
+
+ pendingOperations.isCompiling = true;
+ const spinner = ora(`Watching src/ for changes...`).start();
+ spinner.text = `src/ changed, compiling...`;
+
+ const t = +Date.now();
+ const timingLabel = `[t:${t}] Recompile took`;
+ console.time(timingLabel);
+
+ try {
+ // Process all accumulated events
+ const eventsToProcess = [...pendingOperations.events];
+ pendingOperations.events = []; // Clear the queue
+
+ await compile(eventsToProcess, { cancel: () => {} });
+ spinner.succeed("Compiled successfully.");
+ console.timeEnd(timingLabel);
+ } catch (e) {
+ const error = e as Error;
+ spinner.fail(`Error compiling mitosis ${error.message}.`);
+ } finally {
+ pendingOperations.isCompiling = false;
+
+ // If more events accumulated while we were compiling, process them now
+ if (pendingOperations.events.length > 0) {
+ // Small delay to prevent potential rapid consecutive compilations
+ setTimeout(() => handleEvents([]), 100);
+ }
+ spinner.stop();
+ }
+ };
+
+ // Smart debounce with shorter delay for initial changes and longer for subsequent changes
+ const onChange = lodash.debounce(
+ (err: Error | null, events: Array) => {
if (err) {
- spinner.fail(
- `Error watching src/ for changes ${err?.message}.`,
+ console.error(
+ `Error watching for changes: ${err?.message}`,
);
+ return;
}
- return () => {
- if (compilationPromise) {
- compilationPromise.then(() => {
- spinner.stop();
- });
- }
- };
+ // Filter out irrelevant events (like .DS_Store files)
+ const relevantEvents = events.filter(
+ (event) =>
+ !event.path.includes(".DS_Store") &&
+ !event.path.includes(".git/"),
+ );
+
+ if (relevantEvents.length === 0) return;
+
+ handleEvents(relevantEvents);
+ },
+ 300, // Shorter debounce time for better responsiveness
+ {
+ leading: false,
+ trailing: true,
+ maxWait: 1000, // Maximum wait time for batching
},
- 500,
);
- const watchSrc = watcher.subscribe(srcDir, (err, _events) => {
- const cleanup = onChange(err, _events);
-
- return () => {
- cleanup?.();
- };
+ const watchSrc = watcher.subscribe(srcDir, (err, events) => {
+ onChange(err, events);
+ return () => {};
});
const watchScaffold = watcher.subscribe(
scaffoldDir,
- (err, _events) => {
- const cleanup = onChange(err, _events);
-
- return () => {
- cleanup?.();
- };
+ (err, events) => {
+ onChange(err, events);
+ return () => {};
},
);
diff --git a/packages/compiler/src/frameworks/react.compile.ts b/packages/compiler/src/frameworks/react.compile.ts
index ae35f425..cb2b7527 100644
--- a/packages/compiler/src/frameworks/react.compile.ts
+++ b/packages/compiler/src/frameworks/react.compile.ts
@@ -12,23 +12,60 @@ const DEFAULT_OPTIONS = {
styles: "style-tag",
};
+// Keep track of processed components to avoid duplicate logs
+const processedComponents = new Set();
+
function customReplaceReact(props: CustomReplaceProps): void {
const { name, pascalName, outFile } = props;
- log.info(`\nCompiling ${name} [${pascalName}] for React...`);
- const data = fs.readFileSync(outFile, "utf8");
+ // Skip if we've already processed this component
+ if (processedComponents.has(outFile)) {
+ return;
+ }
+
+ // Mark as processed
+ processedComponents.add(outFile);
+
+ // Use the group logging feature to ensure all logs for this component stay together
+ const componentLogger = log.group(`React Component: ${pascalName}`);
- const result = fixReactTypeIssues(data);
+ try {
+ componentLogger.info(`Compiling ${name} [${pascalName}] for React...`);
- fs.writeFileSync(outFile, result, "utf8");
+ const data = fs.readFileSync(outFile, "utf8");
+ const result = fixReactTypeIssues(data);
+ fs.writeFileSync(outFile, result, "utf8");
+
+ componentLogger.success(`Compiled ${pascalName} successfully`);
+ } catch (error) {
+ componentLogger.error(
+ `Error processing ${pascalName}: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ } finally {
+ componentLogger.end();
+ }
}
async function compileReact(watcherEvents?: Event[]): Promise {
- await compiler.compile({
- ...DEFAULT_OPTIONS,
- watcherEvents: watcherEvents as Event[],
- customReplace: customReplaceReact,
- });
+ // Clear the processed components set for a fresh compilation
+ processedComponents.clear();
+
+ const compileLogger = log.group("React Compilation");
+
+ try {
+ await compiler.compile({
+ ...DEFAULT_OPTIONS,
+ watcherEvents: watcherEvents as Event[],
+ customReplace: customReplaceReact,
+ });
+ compileLogger.success("React compilation completed successfully");
+ } catch (error) {
+ compileLogger.error(
+ `React compilation failed: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ } finally {
+ compileLogger.end();
+ }
}
export { compileReact };
diff --git a/packages/compiler/src/frameworks/vue.compile.ts b/packages/compiler/src/frameworks/vue.compile.ts
index 34b3c1e9..4ca3f964 100644
--- a/packages/compiler/src/frameworks/vue.compile.ts
+++ b/packages/compiler/src/frameworks/vue.compile.ts
@@ -13,84 +13,102 @@ const DEFAULT_OPTIONS = {
styles: "",
};
+// Keep track of processed components to avoid duplicate logs
+const processedComponents = new Set();
+
function customReplaceVue(props: CustomReplaceProps): void {
const { name, pascalName, outFile, outPath, isFirstCompilation } = props;
- log.info(`\nCompiling ${name} [${pascalName}] for Vue...`);
-
- if (isFirstCompilation) {
- const data = fs.readFileSync(`${outPath}/src/index.ts`, "utf8");
-
- log.info("\n ============== before edit index.ts =========== \n" + data);
+ // Skip if we've already processed this component
+ if (processedComponents.has(outFile)) {
+ return;
+ }
- const result = data
- // Add .vue to index and filter by compileAllowList
- .replace(
- /(export)(.*)\/ui\/(?!.*(\.css|\.css\.ts)")(.+)";/g,
- (match, p1, p2, p3, p4) => {
- const componentName = p4.split("/").pop();
- return compileAllowList["vue"]?.includes(componentName)
- ? `${p1}${p2}/ui/${componentName}/${componentName}.vue";`
- : "";
- },
- )
- .replace(/(extensions)\/(.*)\.vue/g, "$1/$2")
- .replace(/\/helpers\.vue/g, "")
- // Add .vue and a subpath to each export, and filter by compileAllowList
- .replace(
- /(export { default as (\w+) } from '\.\/ui\/)([^';]+)';/g,
- (match, p1, p2, p3) => {
- return compileAllowList["vue"]?.includes(p3)
- ? `${p1}${p3}/${p3}.vue';`
- : "";
- },
- )
- // Remove empty lines created by filtered out exports
- .replace(/^\s*[\r\n]/gm, "");
+ // Mark as processed
+ processedComponents.add(outFile);
+
+ // Use the group logging feature to ensure all logs for this component stay together
+ const componentLogger = log.group(`Vue Component: ${pascalName}`);
+
+ try {
+ componentLogger.info(`Compiling ${name} [${pascalName}] for Vue...`);
+
+ if (isFirstCompilation) {
+ const data = fs.readFileSync(`${outPath}/src/index.ts`, "utf8");
+
+ if (data) {
+ componentLogger.info("Processing index.ts");
+
+ const result = data
+ // Add .vue to index and filter by compileAllowList
+ .replace(
+ /(export)(.*)\/ui\/(?!.*(\.css|\.css\.ts)")(.+)";/g,
+ (match, p1, p2, p3, p4) => {
+ const componentName = p4.split("/").pop();
+ return compileAllowList["vue"]?.includes(componentName)
+ ? `${p1}${p2}/ui/${componentName}/${componentName}.vue";`
+ : "";
+ },
+ )
+ .replace(/(extensions)\/(.*)\.vue/g, "$1/$2")
+ .replace(/\/helpers\.vue/g, "")
+ // Add .vue and a subpath to each export, and filter by compileAllowList
+ .replace(
+ /(export { default as (\w+) } from '\.\/ui\/)([^';]+)';/g,
+ (match, p1, p2, p3) => {
+ return compileAllowList["vue"]?.includes(p3)
+ ? `${p1}${p3}/${p3}.vue';`
+ : "";
+ },
+ )
+ // Remove empty lines created by filtered out exports
+ .replace(/^\s*[\r\n]/gm, "");
+
+ fs.writeFileSync(`${outPath}/src/index.ts`, result, "utf8");
+
+ // Add .vue extension to all the indexes in src folder
+ globSync(`${outPath}/src/ui/**/index.ts`).forEach((src: string) => {
+ const data = fs
+ .readFileSync(src, "utf8")
+ // add vue to index
+ .replace(/(export { default } from)(.*)(';)/g, "$1$2.vue$3")
+ // but remove from hooks
+ .replace(/\.hook\.vue/g, ".hook");
+
+ fs.writeFileSync(src, data, "utf8");
+ });
+ }
+ }
- log.warn("\n ============== after edit index.ts =========== \n" + result);
+ componentLogger.success(`Compiled ${pascalName} successfully`);
+ } catch (error) {
+ componentLogger.error(
+ `Error processing ${pascalName}: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ } finally {
+ componentLogger.end();
+ }
+}
- fs.writeFileSync(`${outPath}/src/index.ts`, result, "utf8");
+const compileVue = async (): Promise => {
+ // Clear the processed components set for a fresh compilation
+ processedComponents.clear();
- // Add .vue extension to all the indexes in src folder
- globSync(`${outPath}/src/ui/**/index.ts`).forEach((src: string) => {
- const data = fs
- .readFileSync(src, "utf8")
- // add vue to index
- .replace(/(export { default } from)(.*)(';)/g, "$1$2.vue$3")
- // but remove from hooks
- .replace(/\.hook\.vue/g, ".hook");
+ const compileLogger = log.group("Vue Compilation");
- fs.writeFileSync(src, data, "utf8");
+ try {
+ await compiler.compile({
+ ...DEFAULT_OPTIONS,
+ customReplace: customReplaceVue,
});
+ compileLogger.success("Vue compilation completed successfully");
+ } catch (error) {
+ compileLogger.error(
+ `Vue compilation failed: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ } finally {
+ compileLogger.end();
}
-
- const data = fs.readFileSync(outFile, "utf8");
-
- let result = data;
-
- const transforms = [
- patchPropsDestructuring,
- // TODO: work on TS output later
- // Need to parse + follow imports and resolve + inline types in the .vue files
- // addPathAliasToHelperTypeImports,
- // addPathAliasToRelativeTypeImports,
- // (fileData) => inlineTypes(fileData, name, pascalName),
- ];
-
- result = transforms.reduce((acc, transform) => {
- acc = transform(acc);
- return acc;
- }, result);
-
- fs.writeFileSync(outFile, result, "utf8");
-}
-
-const compileVue = async (): Promise => {
- await compiler.compile({
- ...DEFAULT_OPTIONS,
- customReplace: customReplaceVue,
- });
};
if (require.main === module) {
diff --git a/packages/compiler/src/log.ts b/packages/compiler/src/log.ts
index 4adb95f0..2303ccdb 100644
--- a/packages/compiler/src/log.ts
+++ b/packages/compiler/src/log.ts
@@ -3,19 +3,132 @@ import clc from "cli-color";
const error = clc.red.bold;
const warn = clc.yellow;
const info = clc.blue;
+const success = clc.green;
interface LogFunction {
(...args: unknown[]): void;
}
+// Queue for log operations to prevent interleaving in concurrent processes
+class LogQueue {
+ private queue: Array<() => void> = [];
+ private processing = false;
+ private groupStack: string[] = [];
+
+ enqueue(operation: () => void): void {
+ this.queue.push(operation);
+ if (!this.processing) {
+ this.processQueue();
+ }
+ }
+
+ // Add batch operations to ensure related logs stay together
+ enqueueBatch(operations: Array<() => void>): void {
+ if (operations.length === 0) return;
+
+ // Combine all operations into a single atomic operation
+ this.enqueue(() => {
+ operations.forEach((op) => op());
+ });
+ }
+
+ // Get current group depth for indentation
+ private getCurrentIndent(): string {
+ return " ".repeat(this.groupStack.length);
+ }
+
+ // Push a group onto the stack
+ pushGroup(groupName: string): void {
+ this.groupStack.push(groupName);
+ }
+
+ // Pop a group from the stack
+ popGroup(): void {
+ this.groupStack.pop();
+ }
+
+ private processQueue(): void {
+ if (this.queue.length === 0) {
+ this.processing = false;
+ return;
+ }
+
+ this.processing = true;
+ const operation = this.queue.shift();
+
+ if (operation) {
+ operation();
+ }
+
+ // Use setImmediate for better performance than setTimeout
+ setImmediate(() => this.processQueue());
+ }
+}
+
+const logQueue = new LogQueue();
+
+interface LogGroup {
+ info: LogFunction;
+ error: LogFunction;
+ warn: LogFunction;
+ success: LogFunction;
+ end: () => void;
+}
+
const log: {
error: LogFunction;
warn: LogFunction;
info: LogFunction;
+ success: LogFunction;
+ group: (groupName?: string) => LogGroup;
} = {
- error: (...args: unknown[]) => console.log(error(...args)),
- warn: (...args: unknown[]) => console.log(warn(...args)),
- info: (...args: unknown[]) => console.log(info(...args)),
+ error: (...args: unknown[]) =>
+ logQueue.enqueue(() => console.log(error(...args))),
+ warn: (...args: unknown[]) =>
+ logQueue.enqueue(() => console.log(warn(...args))),
+ info: (...args: unknown[]) =>
+ logQueue.enqueue(() => console.log(info(...args))),
+ success: (...args: unknown[]) =>
+ logQueue.enqueue(() => console.log(success(...args))),
+
+ // Add a group function to batch related logs
+ group: (groupName?: string) => {
+ const pendingLogs: Array<() => void> = [];
+
+ if (groupName) {
+ pendingLogs.push(() => {
+ logQueue.pushGroup(groupName);
+ console.log(info(`\n[Group: ${groupName}]`));
+ });
+ }
+
+ const groupLog = {
+ info: (...args: unknown[]) => {
+ pendingLogs.push(() => console.log(info(...args)));
+ return groupLog;
+ },
+ error: (...args: unknown[]) => {
+ pendingLogs.push(() => console.log(error(...args)));
+ return groupLog;
+ },
+ warn: (...args: unknown[]) => {
+ pendingLogs.push(() => console.log(warn(...args)));
+ return groupLog;
+ },
+ success: (...args: unknown[]) => {
+ pendingLogs.push(() => console.log(success(...args)));
+ return groupLog;
+ },
+ end: () => {
+ if (pendingLogs.length > 0) {
+ logQueue.enqueueBatch(pendingLogs);
+ }
+ logQueue.enqueue(() => logQueue.popGroup());
+ },
+ };
+
+ return groupLog;
+ },
};
export default log;
diff --git a/packages/compiler/src/tasks.ts b/packages/compiler/src/tasks.ts
index 9644678c..ffc80359 100644
--- a/packages/compiler/src/tasks.ts
+++ b/packages/compiler/src/tasks.ts
@@ -3,6 +3,7 @@ import commandLineArgs from "command-line-args";
import util from "util";
import { exec as execCallback } from "child_process";
import { command as execa } from "execa";
+import ora from "ora";
const exec = util.promisify(execCallback);
@@ -11,6 +12,7 @@ interface CliConfig {
platforms: string[];
lint: boolean;
"no-lint"?: boolean;
+ concurrency?: number;
}
const optionDefinitions: commandLineArgs.OptionDefinition[] = [
@@ -25,15 +27,36 @@ const optionDefinitions: commandLineArgs.OptionDefinition[] = [
},
{ name: "lint", type: Boolean, defaultValue: true },
{ name: "no-lint", type: Boolean },
+ {
+ name: "concurrency",
+ alias: "c",
+ type: Number,
+ defaultValue: 4, // Default to 4 concurrent processes
+ },
];
+// Add a comment for clarity even though we removed the description property
+// concurrency: Number of concurrent compilation processes (1-8)
+
const shouldMinify = process.env.MINIFY === "true";
const shouldSkipBundling = process.env.NO_BUILD === "true";
(async () => {
+ const startTime = Date.now();
const cliConfig = commandLineArgs(optionDefinitions) as CliConfig;
cliConfig.lint = cliConfig.lint && !cliConfig["no-lint"]; // TODO: add linting
+ // Limit concurrency to reasonable values
+ if (
+ cliConfig.concurrency &&
+ (cliConfig.concurrency < 1 || cliConfig.concurrency > 8)
+ ) {
+ console.warn(
+ `Concurrency value ${cliConfig.concurrency} is out of range (1-8), defaulting to 4`,
+ );
+ cliConfig.concurrency = 4;
+ }
+
const tasks = new Listr([
{
title: "Pretasks",
@@ -54,7 +77,11 @@ const shouldSkipBundling = process.env.NO_BUILD === "true";
const cleanCmd = `rimraf packages/${platformPkgRoot}/{src,dist,lib,types,stats.html}`;
task.output = `Cleaning dir: ${cleanCmd}`;
- return exec(cleanCmd);
+ return exec(cleanCmd).catch((error) => {
+ console.error(`Error cleaning directories: ${error.message}`);
+ // Continue even if cleaning fails
+ return Promise.resolve();
+ });
},
},
],
@@ -67,28 +94,45 @@ const shouldSkipBundling = process.env.NO_BUILD === "true";
cliConfig.elements?.join(", ") || "all"
}`,
task: () => {
+ const spinner = ora("Preparing compilation...").start();
+
return new Listr(
cliConfig.platforms.map((platform) => ({
title: `Compile ${platform}`,
- task: () =>
- execa(
+ task: () => {
+ spinner.text = `Compiling ${platform}...`;
+ return execa(
`tsx packages/compiler/src/frameworks/${platform}.compile.ts ${
cliConfig.elements
? `--elements ${cliConfig.elements.join(" ")}`
: ""
}`,
- ).catch((error: Error) => {
- throw new Error(`Error compiling ${platform} ${error.message}`);
- }),
+ )
+ .catch((error: Error) => {
+ spinner.fail(`Error compiling ${platform}`);
+ throw new Error(
+ `Error compiling ${platform} ${error.message}`,
+ );
+ })
+ .finally(() => {
+ spinner.succeed(`Compiled ${platform}`);
+ });
+ },
})),
- { concurrent: true },
+ {
+ concurrent: cliConfig.concurrency || true,
+ exitOnError: false, // Continue with other platforms if one fails
+ },
);
},
},
{
title: `Bundle Packages: ${cliConfig.platforms?.join(", ") || ""}`,
- task: async () => {
- if (shouldSkipBundling) return true;
+ task: async (_, task) => {
+ if (shouldSkipBundling) {
+ task.skip("Skipping bundling (NO_BUILD=true)");
+ return;
+ }
const platforms = Array.isArray(cliConfig.platforms)
? cliConfig.platforms
@@ -102,12 +146,14 @@ const shouldSkipBundling = process.env.NO_BUILD === "true";
const filters = `--filter "@interchain-ui/${platformGlob}"`;
const buildCmd = `pnpm run --stream ${filters} build`;
+ task.output = `Running: ${buildCmd}`;
try {
await exec(buildCmd);
if (shouldMinify) {
const minifyCssCmd = `pnpm run --stream ${filters} minifyCss`;
+ task.output = `Running: ${minifyCssCmd}`;
await exec(minifyCssCmd);
}
} catch (error) {
@@ -117,7 +163,15 @@ const shouldSkipBundling = process.env.NO_BUILD === "true";
},
]);
- tasks.run().catch((err: Error) => {
- console.error(err);
- });
+ tasks
+ .run()
+ .then(() => {
+ const endTime = Date.now();
+ const duration = (endTime - startTime) / 1000;
+ console.log(`🚀 Compilation completed in ${duration.toFixed(2)}s`);
+ })
+ .catch((err: Error) => {
+ console.error(err);
+ process.exit(1);
+ });
})();
diff --git a/packages/compiler/src/utils/performance.ts b/packages/compiler/src/utils/performance.ts
new file mode 100644
index 00000000..33d7717f
--- /dev/null
+++ b/packages/compiler/src/utils/performance.ts
@@ -0,0 +1,238 @@
+import clc from "cli-color";
+
+/**
+ * Performance timer utility for detailed profiling
+ */
+class Timer {
+ private timers: Map = new Map();
+ private results: Map = new Map();
+ private isEnabled: boolean = true;
+
+ /**
+ * Start timing an operation
+ */
+ start(label: string): void {
+ if (!this.isEnabled) return;
+ this.timers.set(label, performance.now());
+ }
+
+ /**
+ * End timing an operation and record the duration
+ */
+ end(label: string): number {
+ if (!this.isEnabled) return 0;
+
+ const startTime = this.timers.get(label);
+ if (startTime === undefined) {
+ console.warn(`Timer '${label}' was never started`);
+ return 0;
+ }
+
+ const duration = performance.now() - startTime;
+ this.results.set(label, duration);
+ return duration;
+ }
+
+ /**
+ * Get the duration of a completed operation
+ */
+ get(label: string): number {
+ return this.results.get(label) || 0;
+ }
+
+ /**
+ * Format a duration in milliseconds to a human-readable string
+ */
+ formatDuration(ms: number): string {
+ if (ms < 1000) {
+ return `${ms.toFixed(2)}ms`;
+ }
+ return `${(ms / 1000).toFixed(2)}s`;
+ }
+
+ /**
+ * Enable or disable timing
+ */
+ setEnabled(enabled: boolean): void {
+ this.isEnabled = enabled;
+ }
+
+ /**
+ * Reset all timers and results
+ */
+ reset(): void {
+ this.timers.clear();
+ this.results.clear();
+ }
+
+ /**
+ * Log the recorded timings to the console
+ */
+ logTimings(): void {
+ if (!this.isEnabled || this.results.size === 0) return;
+
+ console.log(clc.cyan("\n📊 Build Performance Metrics:"));
+
+ // Sort timings by duration (descending)
+ const sortedEntries = [...this.results.entries()].sort(
+ (a, b) => b[1] - a[1],
+ );
+
+ const totalTime =
+ this.get("Total Compilation") ||
+ this.get("compileReact") ||
+ this.get("compileVue") ||
+ this.get("React Compilation (CLI)") ||
+ this.get("Vue Compilation (CLI)");
+
+ // Group by category
+ const categories = new Map>();
+
+ for (const [label, duration] of sortedEntries) {
+ const category = label.includes("(") ? label.split("(")[0] : "Other";
+ if (!categories.has(category)) {
+ categories.set(category, []);
+ }
+ categories.get(category)!.push([label, duration]);
+ }
+
+ // Print by category
+ for (const [category, items] of categories) {
+ if (category === "Total" || category === "Other") continue; // Handle these separately
+
+ console.log(clc.bold(`\n🔷 ${category}:`));
+ for (const [label, duration] of items) {
+ const percentage = totalTime
+ ? ((duration / totalTime) * 100).toFixed(1)
+ : "0";
+ const durationStr = this.formatDuration(duration);
+
+ let colorFn = clc.green;
+ if (duration > 5000) {
+ colorFn = clc.red;
+ } else if (duration > 2000) {
+ colorFn = clc.yellow;
+ }
+
+ console.log(
+ ` ${label}: ${colorFn(durationStr)} ${clc.blackBright(`(${percentage}% of total)`)}`,
+ );
+ }
+ }
+
+ // Print 'Other' category
+ if (categories.has("Other")) {
+ console.log(clc.bold(`\n🔹 Other Operations:`));
+ for (const [label, duration] of categories.get("Other")!) {
+ if (label.startsWith("Total")) continue;
+
+ const percentage = totalTime
+ ? ((duration / totalTime) * 100).toFixed(1)
+ : "0";
+ const durationStr = this.formatDuration(duration);
+
+ let colorFn = clc.green;
+ if (duration > 5000) {
+ colorFn = clc.red;
+ } else if (duration > 2000) {
+ colorFn = clc.yellow;
+ }
+
+ console.log(
+ ` ${label}: ${colorFn(durationStr)} ${clc.blackBright(`(${percentage}% of total)`)}`,
+ );
+ }
+ }
+
+ if (totalTime) {
+ console.log(
+ clc.bold(
+ `\n⌛ Total Compilation Time: ${clc.cyan(this.formatDuration(totalTime))}`,
+ ),
+ );
+ }
+
+ console.log(
+ clc.blackBright("\nℹ️ Add the '--no-timing' flag to disable this output"),
+ );
+ }
+
+ // Type-safe wrapper with more explicit types
+ wrap(
+ fn: (...args: Args) => Return,
+ label: string,
+ ): (...args: Args) => Return {
+ return (...args: Args): Return => {
+ this.start(label);
+ try {
+ const result = fn(...args);
+
+ // Handle promises
+ if (result instanceof Promise) {
+ // TypeScript doesn't know that Return === Promise
+ // We use a type assertion here
+ return result
+ .then((value) => {
+ this.end(label);
+ return value;
+ })
+ .catch((error) => {
+ this.end(label);
+ throw error;
+ }) as Return;
+ }
+
+ this.end(label);
+ return result;
+ } catch (error) {
+ this.end(label);
+ throw error;
+ }
+ };
+ }
+}
+
+// Create and export singleton instance immediately
+// This ensures it's available as soon as the module is imported
+const timerInstance = new Timer();
+
+// Helper function to time async operations
+async function timeAsync(label: string, fn: () => Promise): Promise {
+ timerInstance.start(label);
+ try {
+ const result = await fn();
+ timerInstance.end(label);
+ return result;
+ } catch (error) {
+ timerInstance.end(label);
+ throw error;
+ }
+}
+
+// Helper function to time synchronous operations
+function timeSync(label: string, fn: () => T): T {
+ timerInstance.start(label);
+ try {
+ const result = fn();
+ timerInstance.end(label);
+ return result;
+ } catch (error) {
+ timerInstance.end(label);
+ throw error;
+ }
+}
+
+// Export timer interface for TypeScript
+export interface TimerInterface {
+ start: (label: string) => void;
+ end: (label: string) => number;
+ get: (label: string) => number;
+ reset: () => void;
+ formatDuration: (ms: number) => string;
+ setEnabled: (enabled: boolean) => void;
+ logTimings: () => void;
+}
+
+// Export the timer instance and helper functions
+export const timer: TimerInterface = timerInstance;
+export { timeAsync, timeSync };
diff --git a/packages/react-codemod/package.json b/packages/react-codemod/package.json
index 16538b4f..41b942cd 100644
--- a/packages/react-codemod/package.json
+++ b/packages/react-codemod/package.json
@@ -44,14 +44,14 @@
"@babel/plugin-syntax-typescript": "^7.x"
},
"devDependencies": {
- "@parcel/config-default": "^2.12.0",
- "@parcel/transformer-typescript-tsc": "^2.12.0",
+ "@parcel/config-default": "^2.13.3",
+ "@parcel/transformer-typescript-tsc": "^2.13.3",
"@types/node": "^20.16.5",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"acorn": "^8.10.0",
"eslint": "^9.10.0",
- "parcel": "^2.12.0",
+ "parcel": "^2.13.3",
"typescript": "^5.5.4",
"vite": "^5.4.3",
"vitest": "^1.6.0"
@@ -73,4 +73,4 @@
"bugs": {
"url": "https://github.com/hyperweb-io/interchain-ui/issues"
}
-}
\ No newline at end of file
+}
diff --git a/packages/react-no-ssr/package.json b/packages/react-no-ssr/package.json
index 35190237..0d4fe5ff 100644
--- a/packages/react-no-ssr/package.json
+++ b/packages/react-no-ssr/package.json
@@ -39,11 +39,11 @@
"devDependencies": {
"@eslint/compat": "^1.1.0",
"@eslint/js": "^9.5.0",
- "@parcel/config-default": "^2.12.0",
- "@parcel/core": "^2.12.0",
- "@parcel/transformer-js": "^2.12.0",
- "@parcel/transformer-react-refresh-wrap": "^2.12.0",
- "@parcel/transformer-typescript-tsc": "^2.12.0",
+ "@parcel/config-default": "^2.13.3",
+ "@parcel/core": "^2.13.3",
+ "@parcel/transformer-js": "^2.13.3",
+ "@parcel/transformer-react-refresh-wrap": "^2.13.3",
+ "@parcel/transformer-typescript-tsc": "^2.13.3",
"@testing-library/dom": "^10.1.0",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
@@ -53,7 +53,7 @@
"eslint-plugin-react": "^7.34.3",
"globals": "^15.6.0",
"jsdom": "^24.1.0",
- "parcel": "^2.12.0",
+ "parcel": "^2.13.3",
"tshy": "^1.15.1",
"typescript": "^5.4.5",
"typescript-eslint": "^7.13.1",
diff --git a/packages/react/package.json b/packages/react/package.json
index 2233a026..1c648c98 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -89,8 +89,8 @@
"zustand": "^4.5.5"
},
"peerDependencies": {
- "react": "^16.14.0 || ^17.0.0 || ^18.0.0",
- "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
},
"directories": {
"lib": "lib",
@@ -128,28 +128,28 @@
"@chain-registry/osmosis": "^1.62.59",
"@chain-registry/types": "^0.18.19",
"@chain-registry/utils": "^1.46.59",
- "@chromatic-com/storybook": "^3.1.0",
- "@parcel/config-default": "^2.12.0",
- "@parcel/core": "^2.12.0",
- "@parcel/optimizer-swc": "^2.12.0",
- "@parcel/optimizer-terser": "^2.12.0",
- "@parcel/reporter-bundle-analyzer": "^2.12.0",
- "@parcel/resolver-default": "^2.12.0",
- "@parcel/transformer-css": "^2.12.0",
- "@parcel/transformer-js": "^2.12.0",
- "@parcel/transformer-react-refresh-wrap": "^2.12.0",
- "@parcel/transformer-typescript-tsc": "^2.12.0",
- "@parcel/transformer-typescript-types": "^2.12.0",
+ "@chromatic-com/storybook": "^3.2.6",
+ "@parcel/config-default": "^2.13.3",
+ "@parcel/core": "^2.13.3",
+ "@parcel/optimizer-swc": "^2.13.3",
+ "@parcel/optimizer-terser": "^2.13.3",
+ "@parcel/reporter-bundle-analyzer": "^2.13.3",
+ "@parcel/resolver-default": "^2.13.3",
+ "@parcel/transformer-css": "^2.13.3",
+ "@parcel/transformer-js": "^2.13.3",
+ "@parcel/transformer-react-refresh-wrap": "^2.13.3",
+ "@parcel/transformer-typescript-tsc": "^2.13.3",
+ "@parcel/transformer-typescript-types": "^2.13.3",
"@react-types/combobox": "^3.12.1",
"@react-types/shared": "^3.25.0",
- "@storybook/addon-essentials": "^8.3.6",
- "@storybook/addon-interactions": "^8.3.6",
- "@storybook/addon-links": "^8.3.6",
- "@storybook/addon-viewport": "^8.3.6",
- "@storybook/blocks": "^8.3.6",
- "@storybook/react": "^8.3.6",
- "@storybook/react-vite": "^8.3.6",
- "@storybook/test": "^8.3.6",
+ "@storybook/addon-essentials": "^8.6.7",
+ "@storybook/addon-interactions": "^8.6.7",
+ "@storybook/addon-links": "^8.6.7",
+ "@storybook/addon-viewport": "^8.6.7",
+ "@storybook/blocks": "^8.6.7",
+ "@storybook/react": "^8.6.7",
+ "@storybook/react-vite": "^8.6.7",
+ "@storybook/test": "^8.6.7",
"@types/react": "latest",
"@vanilla-extract/parcel-transformer": "^1.0.9",
"@vanilla-extract/vite-plugin": "^4.0.15",
@@ -157,12 +157,12 @@
"@vitejs/plugin-react-swc": "^3.7.0",
"match-sorter": "^6.3.4",
"mkdirp": "^3.0.1",
- "parcel": "^2.12.0",
+ "parcel": "^2.13.3",
"parcel-optimizer-unlink-css": "workspace:*",
"parcel-resolver-ts-base-url": "^1.3.1",
"prop-types": "^15.8.1",
- "storybook": "^8.3.6",
- "storybook-react-rsbuild": "^0.1.2",
+ "storybook": "^8.6.7",
+ "storybook-react-rsbuild": "^1.0.0",
"vite": "^5.4.2",
"vite-plugin-replace": "^0.1.1"
},
diff --git a/packages/react/scaffolds/number-field/number-field.tsx b/packages/react/scaffolds/number-field/number-field.tsx
index 99b0782f..b227f038 100644
--- a/packages/react/scaffolds/number-field/number-field.tsx
+++ b/packages/react/scaffolds/number-field/number-field.tsx
@@ -20,7 +20,7 @@ import * as styles from "./number-field.css";
import type { NumberInputProps } from "./number-field.types";
function usePrevious(value: T): T {
- const ref = React.useRef();
+ const ref = React.useRef(undefined);
useEffect(() => {
ref.current = value;
});
diff --git a/packages/react/src/app-env.d.ts b/packages/react/src/app-env.d.ts
new file mode 100644
index 00000000..dad4fed1
--- /dev/null
+++ b/packages/react/src/app-env.d.ts
@@ -0,0 +1,17 @@
+// Workaround for importing images
+declare module "*.svg" {
+ const svgContent: string;
+ export default svgContent;
+}
+
+declare module "*.png" {
+ const pngContent: string;
+ export default pngContent;
+}
+
+// Workaround for Vue SFC compiler: Failed to resolve index type into finite keys
+import { createRainbowSprinkles } from "rainbow-sprinkles";
+
+export const rainbowSprinkles: ReturnType;
+
+export type Sprinkles = Parameters[0];
diff --git a/packages/react/src/helpers/debug.ts b/packages/react/src/helpers/debug.ts
new file mode 100644
index 00000000..076e2bae
--- /dev/null
+++ b/packages/react/src/helpers/debug.ts
@@ -0,0 +1,24 @@
+export enum DebugLevel {
+ None = 0,
+ Log = 1,
+ Breakpoint = 2,
+}
+
+let debugLevel = DebugLevel.None;
+
+const debugStrategies = {
+ [DebugLevel.None]: () => null,
+ [DebugLevel.Log]: (message: string) => console.log(message),
+ [DebugLevel.Breakpoint]: (message: string) => {
+ console.log(message);
+ debugger;
+ },
+};
+
+export function setDebugLevel(level: DebugLevel) {
+ debugLevel = level;
+}
+
+export function debug(message: string) {
+ debugStrategies[debugLevel](message);
+}
diff --git a/packages/react/src/helpers/index.ts b/packages/react/src/helpers/index.ts
new file mode 100644
index 00000000..0d70c6ca
--- /dev/null
+++ b/packages/react/src/helpers/index.ts
@@ -0,0 +1,29 @@
+export * from "./debug";
+export * from "./platform";
+export * from "./style";
+export * from "./number";
+
+export type Args = T extends (...args: infer R) => any
+ ? R
+ : never;
+
+export type AnyFunction = (...args: T[]) => any;
+
+export function callAllHandlers void>(
+ ...fns: (T | undefined)[]
+) {
+ return function func(event: Args[0]) {
+ fns.some((fn) => {
+ fn?.(event);
+ return event?.defaultPrevented;
+ });
+ };
+}
+
+export function callAll(...fns: (T | undefined)[]) {
+ return function mergedFn(arg: Args[0]) {
+ fns.forEach((fn) => {
+ fn?.(arg);
+ });
+ };
+}
diff --git a/packages/react/src/helpers/number.ts b/packages/react/src/helpers/number.ts
new file mode 100644
index 00000000..4617f209
--- /dev/null
+++ b/packages/react/src/helpers/number.ts
@@ -0,0 +1,85 @@
+import BigNumber from "bignumber.js";
+
+export function getCurrencyFormatter(
+ locale?: string,
+ options?: Intl.NumberFormatOptions
+): Intl.NumberFormat {
+ return new Intl.NumberFormat(locale ?? "en-US", options);
+}
+
+export function formatIntlNumber(
+ value: number,
+ locale?: string,
+ options?: Intl.NumberFormatOptions
+) {
+ const formatter = new Intl.NumberFormat(locale ?? "en-US", {
+ ...options,
+ style: options?.style ?? "decimal",
+ currency: options?.currency ?? "USD",
+ minimumFractionDigits: options?.minimumFractionDigits ?? 0,
+ maximumFractionDigits: options?.maximumFractionDigits ?? 4,
+ });
+ return formatter.format(value);
+}
+
+export function formatCurrency(
+ value: number,
+ locale?: string,
+ options?: Intl.NumberFormatOptions
+) {
+ return formatIntlNumber(value, locale, {
+ ...options,
+ style: "currency",
+ });
+}
+
+export function safelyFormatNumberWithFallback(
+ formatter: Intl.NumberFormat,
+ value: BigNumber
+): string {
+ // First, attempt to format the BigNumber as a number primitive
+ try {
+ return formatter.format(value.toNumber());
+ } catch {}
+
+ // As a fallback, simply return the ugly string value
+ return value.toString();
+}
+
+/**
+ * Function for lamp value
+ * @param min
+ * @param max
+ * @param value
+ * @returns
+ */
+export function clampBigNumber(
+ min: string | number,
+ max: string | number,
+ value: string
+): string {
+ if (value === "") {
+ return "";
+ }
+ if (new BigNumber(value).gt(max)) {
+ return new BigNumber(max).toString();
+ }
+ if (new BigNumber(value).lt(min)) {
+ return new BigNumber(min).toString();
+ }
+ return value?.toString();
+}
+
+export function toNumber(value?: string | number, fallbackValue?: number) {
+ if (value == null) return fallbackValue ?? 0;
+ if (typeof value === "number") return value;
+ if (isNaN(Number(value))) return fallbackValue ?? 0;
+ return Number(value);
+}
+
+export function formatNumeric(
+ value: string | number,
+ maximumFractionDigits = 6
+) {
+ return new BigNumber(value).decimalPlaces(maximumFractionDigits).toString();
+}
diff --git a/packages/react/src/helpers/platform.ts b/packages/react/src/helpers/platform.ts
new file mode 100644
index 00000000..80b7283a
--- /dev/null
+++ b/packages/react/src/helpers/platform.ts
@@ -0,0 +1,48 @@
+import { debug } from "./debug";
+
+export enum Platform {
+ Default = "default",
+ Angular = "angular",
+ Preact = "preact",
+ Qwik = "qwik",
+ React = "react",
+ Solid = "solid",
+ Svelte = "svelte",
+ Vue = "vue",
+ Webcomponents = "webcomponent",
+}
+
+let platform = Platform.Default;
+
+export function setPlatform(newPlatform: Platform) {
+ debug(`Setting new platform ${newPlatform}`);
+
+ platform = newPlatform;
+}
+
+export function getPlatform(): Platform {
+ return platform;
+}
+
+export function isSSR(): boolean {
+ try {
+ return typeof window === undefined;
+ } catch (error) {}
+
+ return true;
+}
+
+export function closestBodyElement(node: HTMLElement) {
+ let parent = node.parentElement;
+ while (parent) {
+ if (parent.tagName === "BODY") {
+ return parent as HTMLIFrameElement;
+ }
+ parent = parent.parentElement;
+ }
+ return null;
+}
+
+export function getOwnerDocument(node: HTMLElement) {
+ return node.ownerDocument;
+}
diff --git a/packages/react/src/helpers/string.ts b/packages/react/src/helpers/string.ts
new file mode 100644
index 00000000..fad54b6f
--- /dev/null
+++ b/packages/react/src/helpers/string.ts
@@ -0,0 +1,14 @@
+export function truncateTextMiddle(addr: string, maxLength: number) {
+ const midChar = "…";
+ if (!addr) addr = "";
+
+ if (addr.length <= maxLength) return addr;
+
+ // length of beginning part
+ const left = Math.ceil(maxLength / 2);
+
+ // start index of ending part
+ const right = addr.length - Math.floor(maxLength / 2) + 1;
+
+ return addr.substring(0, left) + midChar + addr.substring(right);
+}
diff --git a/packages/react/src/helpers/style.ts b/packages/react/src/helpers/style.ts
new file mode 100644
index 00000000..025d590f
--- /dev/null
+++ b/packages/react/src/helpers/style.ts
@@ -0,0 +1,111 @@
+import { globalStyle, GlobalStyleRule } from "@vanilla-extract/css";
+import type { Writable } from "../helpers/types";
+import { store } from "../models/store";
+import {
+ Accent,
+ DEFAULT_ACCENTS,
+ accents,
+ accentsForeground,
+} from "../styles/tokens";
+import {
+ ModePreference,
+ ModePreferences,
+ ThemeVariant,
+} from "../models/system.model";
+import { isSSR } from "./platform";
+
+const hexToRgb = (hex: string) => {
+ const channels = hex
+ .replace(
+ /^#?([a-f\d])([a-f\d])([a-f\d])$/i,
+ (m, r, g, b) => "#" + r + r + g + g + b + b,
+ )
+ .substring(1)
+ .match(/.{2}/g)
+ .map((x) => parseInt(x, 16));
+ return `rgb(${channels[0]}, ${channels[1]}, ${channels[2]})`;
+};
+
+export const rgb = (color: string, alpha?: string) => {
+ const rgbColor = color.includes("rgb") ? color : hexToRgb(color);
+ // 'rgb(x, y, z)' -> 'x, y, z'
+ const partial = rgbColor.replace("rgb(", "").replace(")", "");
+ return alpha ? `rgba(${partial}, ${alpha})` : `rgb(${partial})`;
+};
+
+export const isDefaultAccent = (accent: Accent) => {
+ return (DEFAULT_ACCENTS as unknown as Writable).includes(accent);
+};
+
+// Get accent color, if not default provided then it's a color string
+export const getAccent = (accent: Accent, colorMode: ThemeVariant | null) => {
+ return isDefaultAccent(accent)
+ ? accents[colorMode ?? "light"][accent]
+ : accent;
+};
+
+export const getAccentText = (colorMode: ThemeVariant | null) => {
+ return accentsForeground[colorMode ?? "light"];
+};
+
+export const getAccentHover = (color: string) => {
+ return rgb(color, "0.075");
+};
+
+export const mediaQueryColorScheme = (mode: string) =>
+ `(prefers-color-scheme: ${mode})`;
+
+const isValidThemeMode = (mode: ModePreference) => {
+ return ModePreferences.includes(mode);
+};
+
+// Resolve theme mode by priority:
+// props.defaultProps > saved theme > system theme
+export const resolveThemeMode = (
+ defaultThemeMode?: ModePreference,
+): ModePreference => {
+ const hasHydrated = store.getState()._hasHydrated;
+
+ // While in SSR, return a default theme
+ if (isSSR() || !hasHydrated) {
+ return "light";
+ }
+
+ const savedTheme = store.getState().themeMode;
+
+ if (isValidThemeMode(defaultThemeMode)) {
+ // Only set the theme if it's different from the saved theme
+ if (defaultThemeMode !== savedTheme) {
+ store.getState().setThemeMode(defaultThemeMode);
+ }
+ return defaultThemeMode;
+ }
+
+ // props.defaultThemeMode is not provided or invalid, rely on persisted theme
+ if (isValidThemeMode(savedTheme)) {
+ return savedTheme;
+ }
+
+ // persisted value not a valid theme mode, fallback to 'system'
+ store.getState().setThemeMode("system");
+ console.log("[resolveThemeMode] using system theme mode");
+ return "system";
+};
+
+export const isPreferLightMode = () =>
+ window.matchMedia &&
+ window.matchMedia("(prefers-color-scheme: light)").matches;
+
+export const isPreferDarkMode = () =>
+ window.matchMedia &&
+ window.matchMedia("(prefers-color-scheme: dark)").matches;
+
+export function childSelectors(
+ styleVariantsSelector: string,
+ selector: string,
+ rule: GlobalStyleRule,
+) {
+ const allSelectors = Array.from(new Set(styleVariantsSelector.split(" ")));
+ const targetSelector = allSelectors[allSelectors.length - 1];
+ globalStyle(`${targetSelector} ${selector}`, rule);
+}
diff --git a/packages/react/src/helpers/types.ts b/packages/react/src/helpers/types.ts
new file mode 100644
index 00000000..aa94970d
--- /dev/null
+++ b/packages/react/src/helpers/types.ts
@@ -0,0 +1,40 @@
+// Help "resolve" to the final type value of a type
+export type Resolve = {
+ [Key in keyof T]: T[Key];
+} & {};
+
+// Utility type to convert string literal types to String constructor
+export type StringifyLeaf = T extends string ? String : T;
+
+// Recursive type to walk through the object tree
+export type DeepStringConstructor = {
+ [P in keyof T]: T[P] extends object
+ ? DeepStringConstructor
+ : StringifyLeaf;
+};
+
+export type Primitive =
+ | string
+ | number
+ | boolean
+ | bigint
+ | symbol
+ | null
+ | undefined;
+
+export type LiteralUnion<
+ LiteralType extends Primitive,
+ BaseType extends Primitive,
+> = LiteralType | (BaseType & { _?: never });
+
+export type PartialDeep = T extends object
+ ? {
+ [P in keyof T]?: PartialDeep;
+ }
+ : T;
+
+export type UnknownRecord = Record;
+
+export type Writable = {
+ -readonly [P in keyof T]: T[P];
+};
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
new file mode 100644
index 00000000..fa71f359
--- /dev/null
+++ b/packages/react/src/index.ts
@@ -0,0 +1,307 @@
+///
+export type { UIStore, UIState, UIAction } from "./models/store";
+export type {
+ Intent,
+ ThemeVariant,
+ ModePreference,
+ NumberFormatProps,
+ NumberFormatter,
+} from "./models/system.model";
+export type { GridColumn } from "./models/components.model";
+export type { Sprinkles } from "./styles/rainbow-sprinkles.css";
+export { store } from "./models/store";
+export { skeleton as skeletonClass } from "./ui/shared/shared.css";
+
+// Init Components
+
+export { default as Box } from "./ui/box";
+export { default as Container } from "./ui/container";
+export { default as Link } from "./ui/link";
+export { default as Stack } from "./ui/stack";
+export { default as Center } from "./ui/center";
+export { default as Icon } from "./ui/icon";
+export { default as Callout } from "./ui/callout";
+export type { IconProps, IconName } from "./ui/icon/icon.types";
+export { default as Text } from "./ui/text";
+export type { TextProps } from "./ui/text/text.types";
+export { default as Button } from "./ui/button";
+export type { ButtonProps } from "./ui/button/button.types";
+export { default as Skeleton } from "./ui/skeleton";
+export { default as ClipboardCopyText } from "./ui/clipboard-copy-text";
+export type { ClipboardCopyTextProps } from "./ui/clipboard-copy-text/clipboard-copy-text.types";
+export { default as ThemeProvider } from "./ui/theme-provider";
+export type { ThemeProviderProps } from "./ui/theme-provider/theme-provider.types";
+export { default as OverlaysManager } from "./ui/overlays-manager";
+export type { OverlaysManagerProps } from "./ui/overlays-manager/overlays-manager.types";
+export { default as InterchainUIProvider } from "./ui/interchain-ui-provider";
+export { default as FadeIn } from "./ui/fade-in";
+export { default as PoolsHeader } from "./ui/pools-header";
+export type { PoolsHeaderProps } from "./ui/pools-header/pools-header.types";
+export { default as PoolList } from "./ui/pool-list";
+export type { PoolListProps } from "./ui/pool-list/pool-list.types";
+export { default as PoolListItem } from "./ui/pool-list-item";
+export type { PoolListItemProps } from "./ui/pool-list-item/pool-list-item.types";
+export { default as PoolCard } from "./ui/pool-card";
+export type { PoolCardProps } from "./ui/pool-card/pool-card.types";
+export { default as PoolCardList } from "./ui/pool-card-list";
+export type { PoolCardListProps } from "./ui/pool-card-list/pool-card-list.types";
+export { default as ConnectModal } from "./ui/connect-modal";
+export type { ConnectModalProps } from "./ui/connect-modal/connect-modal.types";
+export { default as ConnectModalHead } from "./ui/connect-modal-head";
+export type { ConnectModalHeadProps } from "./ui/connect-modal-head/connect-modal-head.types";
+export { default as ConnectModalQRCode } from "./ui/connect-modal-qrcode";
+export type { QRCodeStatus } from "./ui/connect-modal-qrcode/connect-modal-qrcode.types";
+export { default as ConnectModalQRCodeError } from "./ui/connect-modal-qrcode-error";
+export type { ConnectModalQRCodeErrorProps } from "./ui/connect-modal-qrcode-error/connect-modal-qrcode-error.types";
+export { default as ConnectModalQRCodeSkeleton } from "./ui/connect-modal-qrcode-skeleton";
+export { default as ConnectModalStatus } from "./ui/connect-modal-status";
+export type { ConnectModalStatusProps } from "./ui/connect-modal-status/connect-modal-status.types";
+export { default as ConnectModalWalletButton } from "./ui/connect-modal-wallet-button";
+export type { ConnectModalWalletButtonProps } from "./ui/connect-modal-wallet-button/connect-modal-wallet-button.types";
+export { default as ConnectModalWalletList } from "./ui/connect-modal-wallet-list";
+export type { ConnectModalWalletListProps } from "./ui/connect-modal-wallet-list/connect-modal-wallet-list.types";
+export { default as Reveal } from "./ui/reveal";
+export { default as I18nProvider } from "./ui/i18n-provider";
+export { default as PoolInfoHeader } from "./ui/pool-info-header";
+export type { PoolInfoHeaderProps } from "./ui/pool-info-header/pool-info-header.types";
+export { default as ManageLiquidityCard } from "./ui/manage-liquidity-card";
+export type { ManageLiquidityCardProps } from "./ui/manage-liquidity-card/manage-liquidity-card.types";
+export { default as BondingCard } from "./ui/bonding-card";
+export type { BondingCardProps } from "./ui/bonding-card/bonding-card.types";
+export { default as BondingCardList } from "./ui/bonding-card-list";
+export type { BondingCardListProps } from "./ui/bonding-card-list/bonding-card-list.types";
+export { default as BondingListItem } from "./ui/bonding-list-item";
+export type { BondingListItemProps } from "./ui/bonding-list-item/bonding-list-item.types";
+export { default as BondingList } from "./ui/bonding-list";
+export type { BondingListProps } from "./ui/bonding-list/bonding-list.types";
+export { default as BondingListItemSm } from "./ui/bonding-list-item-sm";
+export type { BondingListItemSmProps } from "./ui/bonding-list-item-sm/bonding-list-item-sm.types";
+export { default as BondingListSm } from "./ui/bonding-list-sm";
+export type { BondingListSmProps } from "./ui/bonding-list-sm/bonding-list-sm.types";
+export { default as QRCode } from "./ui/qrcode";
+export type { QRProps } from "./ui/qrcode/qrcode.types";
+export { default as IconButton } from "./ui/icon-button";
+export type { IconButtonProps } from "./ui/icon-button/icon-button.types";
+export { default as ProgressBar } from "./ui/progress-bar";
+export type { ProgressBarProps } from "./ui/progress-bar/progress-bar.types";
+export { default as CircularProgressBar } from "./ui/circular-progress-bar";
+export type { CircularProgressBarProps } from "./ui/circular-progress-bar/circular-progress-bar.types";
+export { default as TokenInput } from "./ui/token-input";
+export type { TokenInputProps } from "./ui/token-input/token-input.types";
+export { default as AddLiquidity } from "./ui/add-liquidity";
+export type { AddLiquidityProps } from "./ui/add-liquidity/add-liquidity.types";
+export { default as RemoveLiquidity } from "./ui/remove-liquidity";
+export type { RemoveLiquidityProps } from "./ui/remove-liquidity/remove-liquidity.types";
+export { default as BondingMore } from "./ui/bonding-more";
+export type { BondingMoreProps } from "./ui/bonding-more/bonding-more.types";
+export { default as AssetListHeader } from "./ui/asset-list-header";
+export type { AssetListHeaderProps } from "./ui/asset-list-header/asset-list-header.types";
+export { default as AssetListItem } from "./ui/asset-list-item";
+export type { AssetListItemProps } from "./ui/asset-list-item/asset-list-item.types";
+export { default as AssetList } from "./ui/asset-list";
+export type { AssetListProps } from "./ui/asset-list/asset-list.types";
+export { default as CrossChain } from "./ui/cross-chain";
+export type {
+ CrossChainProps,
+ CrossChainListItemProps,
+} from "./ui/cross-chain/cross-chain.types";
+export { default as SingleChain } from "./ui/single-chain";
+export type {
+ SingleChainProps,
+ SingleChainListItemProps,
+} from "./ui/single-chain/single-chain.types";
+export { default as OverviewTransfer } from "./ui/overview-transfer";
+export type { OverviewTransferProps } from "./ui/overview-transfer/overview-transfer.types";
+export { default as AssetWithdrawTokens } from "./ui/asset-withdraw-tokens";
+export type { AssetWithdrawTokensProps } from "./ui/asset-withdraw-tokens/asset-withdraw-tokens.types";
+export { default as NftMint } from "./ui/nft-mint";
+export type { NftMintProps } from "./ui/nft-mint/nft-mint.types";
+export { default as NftProfileCard } from "./ui/nft-profile-card";
+export type { NftProfileCardProps } from "./ui/nft-profile-card/nft-profile-card.types";
+export { default as NftProfileCardList } from "./ui/nft-profile-card-list";
+export { default as NftProfile } from "./ui/nft-profile";
+export type { NftProfileProps } from "./ui/nft-profile/nft-profile.types";
+export { default as NftDetail } from "./ui/nft-detail";
+export type { NftDetailProps } from "./ui/nft-detail/nft-detail.types";
+export { default as NftTraitListItem } from "./ui/nft-trait-list-item";
+export type { NftTraitListItemProps } from "./ui/nft-trait-list-item/nft-trait-list-item.types";
+export { default as NftTraitList } from "./ui/nft-trait-list";
+export type { NftTraitListProps } from "./ui/nft-trait-list/nft-trait-list.types";
+export { default as NftDetailInfo } from "./ui/nft-detail-info";
+export { default as NftDetailTopOffers } from "./ui/nft-detail-top-offers";
+export type { NftDetailTopOfferProps } from "./ui/nft-detail-top-offers/nft-detail-top-offers.types";
+export { default as NftDetailActivityListItem } from "./ui/nft-detail-activity-list-item";
+export type { NftDetailActivityListItemProps } from "./ui/nft-detail-activity-list-item/nft-detail-activity-list-item.types";
+export { default as NftDetailActivityList } from "./ui/nft-detail-activity-list";
+export type { NftDetailActivityListProps } from "./ui/nft-detail-activity-list/nft-detail-activity-list.types";
+export { default as Tooltip } from "./ui/tooltip";
+export type { TooltipProps } from "./ui/tooltip/tooltip.types";
+export { default as Tabs } from "./ui/tabs";
+export type { TabProps, TabsProps } from "./ui/tabs/tabs.types";
+export { default as StarText } from "./ui/star-text";
+export type { StarTextProps } from "./ui/star-text/star-text.types";
+export { default as ListForSale } from "./ui/list-for-sale";
+export { default as NftFees } from "./ui/nft-fees";
+export type {
+ NftFeeItemProps,
+ NftFeesProps,
+} from "./ui/nft-fees/nft-fees.types";
+export { default as TransferItem } from "./ui/transfer-item";
+export type { TransferItemProps } from "./ui/transfer-item/transfer-item.types";
+export { default as SwapToken } from "./ui/swap-token";
+export type {
+ SwapInfo,
+ SwapItemProps,
+ SwapTokenProps,
+} from "./ui/swap-token/swap-token.types";
+export { default as SwapPrice } from "./ui/swap-price";
+export type { SwapPriceProps } from "./ui/swap-price/swap-price.types";
+export { default as BasicModal } from "./ui/basic-modal";
+export type { BasicModalProps } from "./ui/basic-modal/basic-modal.types";
+export { default as NftMakeOffer } from "./ui/nft-make-offer";
+export type { NftMakeOfferProps } from "./ui/nft-make-offer/nft-make-offer.types";
+export { default as FieldLabel } from "./ui/field-label";
+export type { FieldLabelProps } from "./ui/field-label/field-label.types";
+export { default as TextField } from "./ui/text-field";
+export type { TextFieldProps } from "./ui/text-field/text-field.types";
+export { default as TextFieldAddon } from "./ui/text-field-addon";
+export type { TextFieldAddonProps } from "./ui/text-field-addon/text-field-addon.types";
+export { default as AnimateLayout } from "./ui/animate-layout";
+export type { AnimateLayoutProps } from "./ui/animate-layout/animate-layout.types";
+export { default as ListItem } from "./ui/list-item";
+export type { ListItemProps } from "./ui/list-item/list-item.types";
+export { default as ChainListItem } from "./ui/chain-list-item";
+export type { ChainListItemProps } from "./ui/chain-list-item/chain-list-item.types";
+export { default as ChainSwapInput } from "./ui/chain-swap-input";
+export type { ChainSwapInputProps } from "./ui/chain-swap-input/chain-swap-input.types";
+export { default as SelectButton } from "./ui/select-button";
+export type { SelectButtonProps } from "./ui/select-button/select-button.types";
+export { default as ConnectedWallet } from "./ui/connected-wallet";
+export type { ConnectedWalletProps } from "./ui/connected-wallet/connected-wallet.types";
+export { default as Spinner } from "./ui/spinner";
+export { default as StakingAssetHeader } from "./ui/staking-asset-header";
+export { default as StakingClaimHeader } from "./ui/staking-claim-header";
+
+// ==== Validators
+export { default as ValidatorList } from "./ui/validator-list";
+export { default as ValidatorNameCell } from "./ui/validator-list/validator-name-cell";
+export { default as ValidatorTokenAmountCell } from "./ui/validator-list/validator-token-amount-cell";
+
+export { default as LiquidStaking } from "./ui/liquid-staking";
+export type { LiquidStakingProps } from "./ui/liquid-staking/liquid-staking.types";
+export type { SpinnerProps } from "./ui/spinner/spinner.types";
+export { default as Divider } from "./ui/divider";
+export type { DividerProps } from "./ui/divider/divider.types";
+export { default as Carousel } from "./ui/carousel";
+export type { CarouselProps } from "./ui/carousel/carousel.types";
+export { default as Accordion } from "./ui/accordion";
+export type { AccordionProps } from "./ui/accordion/accordion.types";
+export { default as Breadcrumb } from "./ui/breadcrumb";
+export { default as BreadcrumbItem } from "./ui/breadcrumb/breadcrumb-item";
+export type {
+ BreadcrumbProps,
+ BreadcrumbLink,
+} from "./ui/breadcrumb/breadcrumb.types";
+export { default as ScrollIndicator } from "./ui/scroll-indicator";
+export type { ScrollIndicatorProps } from "./ui/scroll-indicator/scroll-indicator.types";
+export { default as NftMinimumOffer } from "./ui/nft-minimum-offer";
+export type { NftMinimumOfferProps } from "./ui/nft-minimum-offer/nft-minimum-offer.types";
+export { default as NftSellNow } from "./ui/nft-sell-now";
+export type { NftSellNowProps } from "./ui/nft-sell-now/nft-sell-now.types";
+export { default as NftTransfer } from "./ui/nft-transfer";
+export type { NftTransferProps } from "./ui/nft-transfer/nft-transfer.types";
+export { default as Toast } from "./ui/toast";
+export { default as Toaster } from "./ui/toast/toaster";
+export { toast } from "./ui/toast/toast.state";
+export type {
+ ToastProps,
+ ToasterProps,
+ ToastType,
+ Toast as ToastShape,
+ ToastPosition,
+} from "./ui/toast/toast.types";
+export { default as Avatar } from "./ui/avatar";
+export { default as AvatarBadge } from "./ui/avatar-badge";
+export { default as AvatarImage } from "./ui/avatar-image";
+export { default as AvatarName } from "./ui/avatar-name";
+export type { AvatarProps, AvatarBadgeProps } from "./ui/avatar/avatar.types";
+export { default as ChangeChainListItem } from "./ui/change-chain-list-item";
+export type { ChangeChainListItemProps } from "./ui/change-chain-list-item/change-chain-list-item.types";
+export { default as ChangeChainInput } from "./ui/change-chain-input";
+export type { ChangeChainInputProps } from "./ui/change-chain-input/change-chain-input.types";
+
+// Staking delegate
+export { default as StakingDelegate } from "./ui/staking-delegate";
+export { default as StakingDelegateCard } from "./ui/staking-delegate/staking-delegate-card";
+export { default as StakingDelegateInput } from "./ui/staking-delegate/staking-delegate-input";
+
+// Table
+export { default as Table } from "./ui/table";
+export { default as TableHead } from "./ui/table/table-head";
+export { default as TableBody } from "./ui/table/table-body";
+export { default as TableRow } from "./ui/table/table-row";
+export { default as TableCell } from "./ui/table/table-cell";
+export { default as TableRowHeaderCell } from "./ui/table/table-row-header-cell";
+export { default as TableColumnHeaderCell } from "./ui/table/table-column-header-cell";
+
+// Marketing components
+export { default as Timeline } from "./ui/timeline";
+
+// Governance
+export { default as GovernanceProposalItem } from "./ui/governance/governance-proposal-item";
+export { default as GovernanceVoteBreakdown } from "./ui/governance/governance-vote-breakdown";
+export { default as GovernanceVoteForm } from "./ui/governance/governance-vote-form";
+export { default as GovernanceResultCard } from "./ui/governance/governance-result-card";
+
+// Mesh security
+export { default as MeshProvider } from "./ui/mesh-staking/mesh-provider";
+export { default as MeshStakingSliderInfo } from "./ui/mesh-staking/mesh-staking-slider-info";
+export { default as MeshButton } from "./ui/mesh-staking/mesh-button";
+export { default as MeshTagButton } from "./ui/mesh-staking/mesh-tag-button";
+export { default as MeshTab } from "./ui/mesh-staking/mesh-tab";
+export { default as MeshFooterInfoItem } from "./ui/mesh-staking/mesh-footer-info-item";
+export { default as MeshValidatorSquadEmpty } from "./ui/mesh-staking/mesh-validator-squad-empty";
+export { default as MeshModal } from "./ui/mesh-modal";
+export { default as MeshTable } from "./ui/mesh-staking/mesh-table";
+export { default as MeshTableChainCell } from "./ui/mesh-staking/mesh-table-chain-cell";
+export { default as MeshTableAPRCell } from "./ui/mesh-staking/mesh-table-apr-cell";
+export { default as MeshTableHeaderAction } from "./ui/mesh-staking/mesh-table-header-action";
+export { default as MeshTableValidatorsCell } from "./ui/mesh-staking/mesh-table-validators-cell";
+
+// Noble
+export { default as NobleProvider } from "./ui/noble/noble-provider";
+export { default as NobleTxDirectionCard } from "./ui/noble/noble-tx-direction-card";
+export { default as NobleTxProgressBar } from "./ui/noble/noble-tx-progress-bar";
+export { default as NobleTxStepItem } from "./ui/noble/noble-tx-step-item";
+export { default as NobleButton } from "./ui/noble/noble-button";
+export { default as NobleInput } from "./ui/noble/noble-input";
+export { default as NobleSelectNetworkButton } from "./ui/noble/noble-select-network-button";
+export { default as NobleSelectTokenButton } from "./ui/noble/noble-select-token-button";
+export { default as NobleSelectWalletButton } from "./ui/noble/noble-select-wallet-button";
+export { default as NobleTokenAvatar } from "./ui/noble/noble-token-avatar";
+export { default as NobleTxChainRoute } from "./ui/noble/noble-tx-chain-route";
+export { default as NobleTxEstimate } from "./ui/noble/noble-tx-estimate";
+export { default as NoblePageTitleBar } from "./ui/noble/noble-page-title-bar";
+export { default as NobleTxHistoryOverviewItem } from "./ui/noble/noble-tx-history-overview-item";
+
+
+export { default as Modal } from './ui/modal';
+export { default as Select } from './ui/select';
+export { default as Combobox } from './ui/combobox';
+export { default as Slider } from './ui/slider';
+export { default as SelectOption } from './ui/select-option';
+export { default as Popover } from './ui/popover';
+export { default as PopoverTrigger } from './ui/popover-trigger';
+export { default as PopoverContent } from './ui/popover-content';
+export { default as ChainSwapCombobox } from './ui/chain-swap-combobox';
+export { default as ChangeChainCombobox } from './ui/change-chain-combobox';
+export { default as NumberField } from './ui/number-field';
+export { default as GovernanceCheckbox } from './ui/governance-checkbox';
+export { default as GovernanceRadio } from './ui/governance-radio';
+export { default as GovernanceRadioGroup } from './ui/governance-radio-group';
+export { default as NobleChainCombobox } from './ui/noble-chain-combobox';
+// End Components
+
+import "client-only";
+export { default as useColorModeValue } from './ui/hooks/use-color-mode-value';
+export { default as useTheme } from './ui/hooks/use-theme';
\ No newline at end of file
diff --git a/packages/react/src/models/components.model.ts b/packages/react/src/models/components.model.ts
new file mode 100644
index 00000000..c0618b9f
--- /dev/null
+++ b/packages/react/src/models/components.model.ts
@@ -0,0 +1,35 @@
+import type { Sprinkles } from "../styles/rainbow-sprinkles.css";
+
+export type ComponentRef = T & ((el: T) => void);
+export type Children = React.ReactNode; // TODO
+
+export interface BaseComponentProps {
+ className?: string;
+ class?: string; // Fallback className
+ classList?: string; // Fallback class
+ children?: Children;
+ forwardedRef?: any;
+}
+
+export type BaseState = {
+ loaded: boolean;
+};
+
+export type CSS = Partial & {
+ [key: string]: Partial | string;
+};
+
+export type NumberFormatOptions = {
+ minimumFractionDigits: number;
+ maximumFractionDigits: number;
+};
+
+export type GridColumn = {
+ id: string;
+ width?: Sprinkles["width"];
+ label?: string;
+ align?: "left" | "center" | "right";
+ color?: Sprinkles["color"];
+ textTransform?: Sprinkles["textTransform"];
+ render?: (value: any, column: GridColumn, isPinned?: boolean) => Children;
+};
diff --git a/packages/react/src/models/store.ts b/packages/react/src/models/store.ts
new file mode 100644
index 00000000..c8abd974
--- /dev/null
+++ b/packages/react/src/models/store.ts
@@ -0,0 +1,170 @@
+import { createStore } from "zustand/vanilla";
+import { persist } from "zustand/middleware";
+import { immer } from "zustand/middleware/immer";
+import { current } from "immer";
+
+import BigNumber from "bignumber.js";
+import {
+ ModePreference,
+ ThemeVariant,
+ NumberFormatter,
+ NumberFormatProps,
+} from "./system.model";
+import { isPreferDarkMode, isPreferLightMode } from "../helpers/style";
+import {
+ getCurrencyFormatter,
+ safelyFormatNumberWithFallback,
+} from "../helpers/number";
+import { darkThemeClass, lightThemeClass } from "../styles/themes.css";
+import { Accent } from "../styles/tokens";
+import {
+ OverrideStyleManager,
+ assignThemeVars,
+} from "../styles/override/override";
+import type { ThemeDef } from "../styles/override/override.types";
+
+export const STORAGE_NAME = "interchain-ui-store";
+
+export interface UIState {
+ // This is the value persisted in localstorage
+ themeMode: ModePreference;
+ // This is the value that our theme system uses for styling .ie styleVariants
+ // which is derived from themeMode
+ theme: ThemeVariant;
+ themeClass: string;
+ themeClasses: [string, string];
+ themeAccent: Accent;
+ customTheme: string | null;
+ themeDefs: Array;
+ overrideStyleManager: OverrideStyleManager;
+ // Useful for use in SSR frameworks to check if the store has hydrated
+ // and merged state with localstorage yet
+ _hasHydrated: boolean;
+}
+
+export interface UIAction {
+ setThemeMode: (mode: ModePreference) => void;
+ setTheme: (theme: ThemeVariant, themeClass: string) => void;
+ setThemeDefs: (defs: Array, defaultTheme?: string) => void;
+ setCustomTheme: (customTheme: string) => void;
+ setThemeAccent: (accent: Accent) => void;
+ setHasHydrated: (hasHydrated: boolean) => void;
+}
+
+export interface I18nState {
+ formatNumber: NumberFormatter;
+}
+
+export interface I18nAction {
+ setFormatNumberFn: (fn: NumberFormatter) => void;
+}
+
+export interface UIStore extends UIState, UIAction, I18nState, I18nAction {}
+
+export const store = createStore(
+ persist(
+ immer((set) => ({
+ themeMode: null,
+ theme: null,
+ themeClass: "",
+ themeClasses: [lightThemeClass, darkThemeClass],
+ themeAccent: "blue",
+ // Custom theme contract
+ themeDefs: [],
+ customTheme: null,
+ overrideStyleManager: new OverrideStyleManager("light"),
+ _hasHydrated: false,
+ setTheme: (newTheme: ThemeVariant, themeClass: string) =>
+ set((state) => {
+ state.theme = newTheme;
+ state.themeClass = themeClass;
+ }),
+ setThemeMode: (newThemeMode: ModePreference) =>
+ set((state) => {
+ const resolveSystemMode = (
+ themeMode: ModePreference,
+ ): [ThemeVariant, string] => {
+ if (themeMode === "system") {
+ if (isPreferDarkMode()) {
+ return ["dark", darkThemeClass];
+ }
+ if (isPreferLightMode()) {
+ return ["light", lightThemeClass];
+ }
+ } else {
+ const themeClass = {
+ dark: darkThemeClass,
+ light: lightThemeClass,
+ }[themeMode];
+
+ return [themeMode, themeClass];
+ }
+ };
+
+ const [resolvedTheme, resolvedClass] =
+ resolveSystemMode(newThemeMode);
+
+ state.overrideStyleManager?.update(null, resolvedTheme);
+ state.themeMode = newThemeMode;
+ state.theme = resolvedTheme;
+ state.themeClass = resolvedClass;
+ }),
+ setThemeDefs: (defs: Array) => {
+ set((state) => {
+ state.themeDefs = defs;
+ });
+ },
+ setCustomTheme: (customTheme: string) =>
+ set((state) => {
+ state.customTheme = customTheme;
+ const currentState = current(state);
+
+ if (customTheme !== null) {
+ const customThemeObj = currentState.themeDefs.find(
+ (item) => item.name === customTheme,
+ );
+
+ if (customThemeObj) {
+ assignThemeVars(customThemeObj.vars, state.theme);
+ }
+ }
+ }),
+ setThemeAccent: (accent: Accent) =>
+ set((state) => {
+ state.themeAccent = accent;
+ }),
+ formatNumber: (props: NumberFormatProps): string => {
+ const formatter = getCurrencyFormatter("en-US", {
+ currency: "USD",
+ style: props.style,
+ });
+ return safelyFormatNumberWithFallback(
+ formatter,
+ new BigNumber(props.value),
+ );
+ },
+ setFormatNumberFn: (fn: NumberFormatter) =>
+ set((state) => {
+ state.formatNumber = fn;
+ }),
+ setHasHydrated: (hasHydrated) => {
+ set({
+ _hasHydrated: hasHydrated,
+ });
+ },
+ })),
+ {
+ name: STORAGE_NAME,
+ onRehydrateStorage: (state) => {
+ return (persistedState) => {
+ state.setHasHydrated(true);
+ state.setThemeMode(persistedState.themeMode);
+ };
+ },
+ // Only choose to persist theme preference, ignore other state
+ partialize: (state) => ({
+ themeMode: state.themeMode,
+ }),
+ },
+ ),
+);
diff --git a/packages/react/src/models/system.model.ts b/packages/react/src/models/system.model.ts
new file mode 100644
index 00000000..70060f5a
--- /dev/null
+++ b/packages/react/src/models/system.model.ts
@@ -0,0 +1,31 @@
+export enum Intent {
+ None = "none",
+ Info = "info",
+ Success = "warning",
+ Warning = "success",
+ Error = "error",
+}
+
+export const intents = Object.entries(Intent).map(
+ ([key, value]: [string, string]) => ({ key, value }),
+);
+
+export type IntentValues = "none" | "info" | "warning" | "success" | "error";
+
+export type ThemeVariant = "light" | "dark";
+
+export type ModePreference = ThemeVariant | "system";
+
+export const ModePreferences: ModePreference[] = ["light", "dark", "system"];
+
+export interface NumberFormatProps {
+ value: number | string;
+ style?: keyof Intl.NumberFormatOptionsStyleRegistry;
+}
+export type NumberFormatter = (props: NumberFormatProps) => string;
+
+export type Token = {
+ iconSrc: string;
+ name: string;
+ symbol: string;
+};
diff --git a/packages/react/src/styles/global.css.ts b/packages/react/src/styles/global.css.ts
new file mode 100644
index 00000000..e3e8fe58
--- /dev/null
+++ b/packages/react/src/styles/global.css.ts
@@ -0,0 +1,39 @@
+import { globalStyle } from "@vanilla-extract/css";
+import { SYSTEM_FONT_STACK } from "../styles/tokens/typography";
+
+globalStyle(`*, *::before, *::after`, {
+ boxSizing: `border-box`,
+});
+
+globalStyle(`*`, {
+ margin: 0,
+});
+
+globalStyle(`html, body`, {
+ height: `100%`,
+ fontFamily: `Inter, ${SYSTEM_FONT_STACK}`,
+});
+
+globalStyle(`body`, {
+ lineHeight: 1.5,
+ WebkitFontSmoothing: `antialiased`,
+});
+
+globalStyle(`img, picture, video, canvas, svg`, {
+ display: `block`,
+ maxWidth: `100%`,
+});
+
+globalStyle(`input, button, textarea, select, output`, {
+ font: `inherit`,
+});
+
+// Avoid text overflow
+globalStyle(`p, h1, h2, h3, h4, h5, h6`, {
+ overflowWrap: `break-word`,
+});
+
+// Create a root stacking context
+globalStyle(`#root`, {
+ isolation: `isolate`,
+});
diff --git a/packages/react/src/styles/layers.css.ts b/packages/react/src/styles/layers.css.ts
new file mode 100644
index 00000000..b8aa0419
--- /dev/null
+++ b/packages/react/src/styles/layers.css.ts
@@ -0,0 +1,7 @@
+import { globalLayer } from "@vanilla-extract/css";
+
+// CSS Layers
+// Last layer defined here will have the most power
+// See: https://vanilla-extract.style/documentation/api/layer/#layer
+export const baseLayer = globalLayer("base");
+export const themeLayer = globalLayer("theme");
diff --git a/packages/react/src/styles/override/override.ts b/packages/react/src/styles/override/override.ts
new file mode 100644
index 00000000..820bce8f
--- /dev/null
+++ b/packages/react/src/styles/override/override.ts
@@ -0,0 +1,150 @@
+import { assignInlineVars, setElementVars } from "@vanilla-extract/dynamic";
+import { merge } from "lodash";
+import type {
+ OverridableProp,
+ OverrideValue,
+ ComponentOverrideMap,
+ ComponentOverrideSchema,
+ OverridableComponents,
+ CustomThemeVars,
+} from "./override.types";
+import type { ThemeVariant } from "../../models/system.model";
+import {
+ themeVars,
+ commonVars,
+ darkThemeClass,
+ lightThemeClass,
+} from "../../styles/themes.css";
+import type { ThemeContractValues } from "../../styles/themes.css";
+
+// ====
+import { buttonOverrides } from "../../ui/button/button.helper";
+import { clipboardCopyTextOverrides } from "../../ui/clipboard-copy-text/clipboard-copy-text.helper";
+import { connectModalOverrides } from "../../ui/connect-modal/connect-modal.helper";
+import { connectModalHeadTitleOverrides } from "../../ui/connect-modal-head/connect-modal-head.helper";
+import { installButtonOverrides } from "../../ui/connect-modal-install-button/connect-modal-install-button.helper";
+import {
+ connectQRCodeOverrides,
+ connectQRCodeShadowOverrides,
+} from "../../ui/connect-modal-qrcode/connect-modal-qrcode.helper";
+import {
+ connectModalQRCodeErrorOverrides,
+ connectModalQRCodeErrorButtonOverrides,
+} from "../../ui/connect-modal-qrcode-error/connect-modal-qrcode-error.helper";
+import { connectModalQRCodeSkeletonOverrides } from "../../ui/connect-modal-qrcode-skeleton/connect-modal-qrcode-skeleton.helper";
+import {
+ buttonOverrides as walletButtonOverrides,
+ buttonLabelOverrides as walletButtonLabelOverrides,
+ buttonSublogoOverrides as walletButtonSubLogoOverrides,
+} from "../../ui/connect-modal-wallet-button/connect-modal-wallet-button.helper";
+
+// Must manually add the overrides schema to this object for every component that you want users
+// to be able to override styles
+const overrideSchemas: Record =
+ {
+ button: buttonOverrides,
+ "clipboard-copy-text": clipboardCopyTextOverrides,
+ "connect-modal": connectModalOverrides,
+ "connect-modal-head-title": connectModalHeadTitleOverrides,
+ "connect-modal-install-button": installButtonOverrides,
+ "connect-modal-qr-code": connectQRCodeOverrides,
+ "connect-modal-qr-code-shadow": connectQRCodeShadowOverrides,
+ "connect-modal-qr-code-error": connectModalQRCodeErrorOverrides,
+ "connect-modal-qr-code-error-button":
+ connectModalQRCodeErrorButtonOverrides,
+ "connect-modal-qr-code-loading": connectModalQRCodeSkeletonOverrides,
+ "connect-modal-wallet-button": walletButtonOverrides,
+ "connect-modal-wallet-button-label": walletButtonLabelOverrides,
+ "connect-modal-wallet-button-sublogo": walletButtonSubLogoOverrides,
+ };
+
+export class OverrideStyleManager {
+ _overrideMap: ComponentOverrideMap;
+ _theme: ThemeVariant;
+
+ constructor(theme: ThemeVariant) {
+ this._overrideMap = {};
+ this._theme = theme;
+ }
+
+ update(overrideMap: ComponentOverrideMap | null, theme: ThemeVariant | null) {
+ if (overrideMap) {
+ this._overrideMap = overrideMap;
+ }
+ if (theme) {
+ this._theme = theme;
+ }
+ }
+
+ applyOverrides(component: OverridableComponents) {
+ const schema = overrideSchemas[component];
+ const componentOverrideValue = this._overrideMap[component] ?? {};
+ const configByTheme = groupByTheme(componentOverrideValue);
+
+ if (!schema || !configByTheme) {
+ return {};
+ }
+
+ const config = configByTheme[this._theme];
+ const varsMap: Record = {};
+
+ // Get the actual css value for each css var
+ for (const [vanillaCssVar, propertyName] of schema.overrides) {
+ const propertyValue = config[propertyName];
+
+ if (!propertyValue) {
+ continue;
+ }
+
+ varsMap[`${vanillaCssVar}`] = propertyValue;
+ }
+
+ return assignInlineVars(varsMap);
+ }
+}
+
+// Transform config object
+// From:
+// { bg: { light: 'red500', dark: 'blue400' },
+// color: { light: 'yellow200', dark: 'gray400'}
+// }
+// To: {
+// light: { bg: 'red500', color: 'yellow200' },
+// dark: { bg: 'blue400', color: 'gray400' }
+// }
+
+function groupByTheme(config: OverrideValue) {
+ if (!config) return null;
+
+ const result: Record<
+ ThemeVariant,
+ Partial>
+ > = {
+ light: {},
+ dark: {},
+ };
+
+ for (const [overrideProperty, propertyConfig] of Object.entries(config)) {
+ for (const theme of ["light", "dark"] as ThemeVariant[]) {
+ result[theme] = {
+ ...result[theme],
+ [overrideProperty]: propertyConfig[theme],
+ };
+ }
+ }
+
+ return result;
+}
+
+export function assignThemeVars(
+ customTheme: CustomThemeVars,
+ colorMode: ThemeVariant,
+) {
+ const schemeClass = colorMode === "light" ? lightThemeClass : darkThemeClass;
+ const elements = document.getElementsByClassName(schemeClass);
+ const mergedVars = merge(commonVars, customTheme as ThemeContractValues);
+
+ for (let el of elements) {
+ setElementVars(el as HTMLElement, themeVars, mergedVars);
+ }
+}
diff --git a/packages/react/src/styles/override/override.types.ts b/packages/react/src/styles/override/override.types.ts
new file mode 100644
index 00000000..70e355b8
--- /dev/null
+++ b/packages/react/src/styles/override/override.types.ts
@@ -0,0 +1,75 @@
+import { createVar } from "@vanilla-extract/css";
+import { themeContractTemplate } from "../../styles/themes.css";
+import type { LiteralUnion, PartialDeep } from "../../helpers/types";
+import type { DeepStringConstructor } from "../../helpers/types";
+
+export type CssVar = ReturnType;
+
+export type OverridableState = "hover" | "active" | "disabled" | "focused";
+
+export type Bg = "bg" | `${OverridableState}Bg`;
+
+export type TextColor = "color" | `${OverridableState}Color`;
+
+export type Shadow = "shadow" | `${OverridableState}Shadow`;
+
+export type BorderColor = "borderColor" | `${OverridableState}BorderColor`;
+
+export type OverridableProp = Bg | TextColor | Shadow | BorderColor;
+
+// Add more slots here when you need a component to be overridable
+// TODO: infer through a register() function so that we don't need to do this manually
+export type OverridableComponents =
+ | "button"
+ | "clipboard-copy-text"
+ | "connect-modal"
+ | "connect-modal-install-button"
+ | "connect-modal-head-title"
+ // == Connect wallet button in wallet list
+ | "connect-modal-wallet-button"
+ | "connect-modal-wallet-button-label"
+ | "connect-modal-wallet-button-sublogo"
+ | "connect-modal-qr-code"
+ | "connect-modal-qr-code-shadow"
+ | "connect-modal-qr-code-error"
+ | "connect-modal-qr-code-error-button"
+ | "connect-modal-qr-code-loading";
+
+export type OverrideValue = Partial<
+ Record
+>;
+
+export type OverrideValueByVariant<
+ TVariant extends LiteralUnion<"default", string>,
+> = Record;
+
+// This is provided for user to provide concrete values to override a component
+export type ComponentOverrideMap = Partial<
+ Record
+>;
+
+// This tells us how to override a component
+export type ComponentOverrideSchema = {
+ name: OverridableComponents;
+ overrides: Array<[CssVar, OverridableProp]>;
+};
+
+export type CustomThemeVars = PartialDeep<
+ DeepStringConstructor
+>;
+
+// Theme contract customization
+export type SingleThemeDef = {
+ name: string;
+ vars: CustomThemeVars;
+};
+
+export type DualThemeDef = {
+ name: string;
+ vars: {
+ light: CustomThemeVars;
+ dark: CustomThemeVars;
+ };
+};
+
+export type ThemeDef = SingleThemeDef;
diff --git a/packages/react/src/styles/rainbow-sprinkles.css.ts b/packages/react/src/styles/rainbow-sprinkles.css.ts
new file mode 100644
index 00000000..7112941b
--- /dev/null
+++ b/packages/react/src/styles/rainbow-sprinkles.css.ts
@@ -0,0 +1,249 @@
+import {
+ createRainbowSprinkles,
+ defineProperties,
+ SprinklesFn,
+} from "rainbow-sprinkles";
+import { themeVars } from "./themes.css";
+import { breakpoints } from "./tokens";
+
+const extendedSpace = {
+ "1/5": "20%",
+ "1/4": "25%",
+ "1/3": "33.333333%",
+ "1/2": "50%",
+ "2/3": "66.666667%",
+ "3/4": "75%",
+};
+
+const allSpace = { ...themeVars.space, ...extendedSpace };
+
+const margins = themeVars.space;
+
+const responsiveProperties = defineProperties({
+ conditions: transformBreakpoints<{
+ mobile: {};
+ tablet: {};
+ desktop: {};
+ mdMobile: {};
+ }>(breakpoints),
+ defaultCondition: "mobile",
+ dynamicProperties: {
+ display: true,
+ tableLayout: true,
+ backgroundImage: true,
+ backgroundSize: true,
+ backgroundPosition: true,
+ backgroundRepeat: true,
+ objectFit: true,
+ flex: true,
+ flexBasis: true,
+ flexShrink: true,
+ flexGrow: true,
+ flexDirection: true,
+ flexWrap: true,
+ alignItems: true,
+ alignSelf: true,
+ justifyContent: true,
+ padding: true,
+ paddingLeft: true,
+ paddingRight: true,
+ paddingTop: true,
+ paddingBottom: true,
+ width: true,
+ height: true,
+ minWidth: true,
+ minHeight: true,
+ maxWidth: true,
+ maxHeight: true,
+ borderRadius: themeVars.radii,
+ borderTopLeftRadius: true,
+ borderBottomLeftRadius: true,
+ borderTopRightRadius: true,
+ borderBottomRightRadius: true,
+ fontFamily: themeVars.font,
+ fontSize: themeVars.fontSize,
+ lineHeight: themeVars.lineHeight,
+ textAlign: true,
+ zIndex: themeVars.zIndex,
+ position: true,
+ top: margins,
+ left: margins,
+ right: margins,
+ bottom: margins,
+ verticalAlign: true,
+ margin: margins,
+ marginBottom: margins,
+ marginLeft: margins,
+ marginRight: margins,
+ marginTop: margins,
+ letterSpacing: themeVars.letterSpacing,
+ textTransform: true,
+ fontWeight: themeVars.fontWeight,
+ whiteSpace: true,
+ wordBreak: true,
+ overflowWrap: true,
+ fill: true,
+ overflow: true,
+ overflowX: true,
+ overflowY: true,
+ textOverflow: true,
+ aspectRatio: true,
+ opacity: true,
+ cursor: true,
+ gridTemplateColumns: true,
+ grid: true,
+ gridArea: true,
+ gridAutoColumns: true,
+ gridAutoFlow: true,
+ gridAutoRows: true,
+ gridColumn: true,
+ gridColumnEnd: true,
+ gridColumnStart: true,
+ gridTemplate: true,
+ gridTemplateAreas: true,
+ gridTemplateRows: true,
+ gridRow: true,
+ gridRowEnd: true,
+ gridRowStart: true,
+ gap: themeVars.space,
+ columnGap: themeVars.space,
+ rowGap: themeVars.space,
+ inset: true,
+ insetInlineStart: true,
+ insetInlineEnd: true,
+ transform: true,
+ appearance: true,
+ userSelect: true,
+ },
+ staticProperties: {
+ display: [
+ "block",
+ "inline-block",
+ "inline",
+ "flex",
+ "inline-flex",
+ "grid",
+ "inline-grid",
+ "table",
+ "table-row",
+ "table-cell",
+ "none",
+ ],
+ position: ["absolute", "relative", "fixed", "sticky"],
+ padding: allSpace,
+ paddingLeft: allSpace,
+ paddingRight: allSpace,
+ paddingTop: allSpace,
+ paddingBottom: allSpace,
+ width: allSpace,
+ height: allSpace,
+ minWidth: allSpace,
+ minHeight: allSpace,
+ maxWidth: allSpace,
+ maxHeight: allSpace,
+ },
+ shorthands: {
+ p: ["padding"],
+ pl: ["paddingLeft"],
+ pr: ["paddingRight"],
+ pt: ["paddingTop"],
+ pb: ["paddingBottom"],
+ paddingX: ["paddingLeft", "paddingRight"],
+ paddingY: ["paddingTop", "paddingBottom"],
+ px: ["paddingLeft", "paddingRight"],
+ py: ["paddingTop", "paddingBottom"],
+ m: ["margin"],
+ mr: ["marginRight"],
+ ml: ["marginLeft"],
+ mt: ["marginTop"],
+ mb: ["marginBottom"],
+ marginX: ["marginLeft", "marginRight"],
+ marginY: ["marginTop", "marginBottom"],
+ mx: ["marginLeft", "marginRight"],
+ my: ["marginTop", "marginBottom"],
+ },
+});
+
+const interactiveProperties = defineProperties({
+ conditions: {
+ base: {},
+ hover: { selector: "&:hover:not([disabled])" },
+ focus: { selector: "&:focus:not([disabled])" },
+ active: { selector: "&:active:not([disabled])" },
+ },
+ defaultCondition: "base",
+ dynamicProperties: {
+ color: themeVars.colors,
+ outline: true,
+ visibility: true,
+ filter: true,
+ fill: themeVars.colors,
+ stroke: themeVars.colors,
+ backgroundColor: themeVars.colors,
+ borderWidth: themeVars.borderWidth,
+ borderStyle: themeVars.borderStyle,
+ borderColor: themeVars.colors,
+ borderBottomColor: themeVars.colors,
+ borderBottomStyle: true,
+ borderBottomWidth: true,
+ borderTopColor: themeVars.colors,
+ borderTopStyle: true,
+ borderTopWidth: true,
+ borderLeftColor: themeVars.colors,
+ borderLeftStyle: true,
+ borderLeftWidth: true,
+ borderRightColor: themeVars.colors,
+ borderRightStyle: true,
+ borderRightWidth: true,
+ borderCollapse: true,
+ boxShadow: themeVars.boxShadow,
+ transform: true,
+ transition: true,
+ transitionProperty: true,
+ transitionDuration: true,
+ animation: true,
+ textDecoration: true,
+ zIndex: themeVars.zIndex,
+ fontVariantNumeric: true,
+ },
+ staticProperties: {
+ color: themeVars.colors,
+ backgroundColor: themeVars.colors,
+ borderColor: themeVars.colors,
+ boxShadow: themeVars.boxShadow,
+ visibility: ["collapse", "hidden", "visible"],
+ },
+ shorthands: {
+ bg: ["backgroundColor"],
+ border: ["borderWidth", "borderStyle", "borderColor"],
+ },
+});
+
+export const rainbowSprinkles = createRainbowSprinkles(
+ responsiveProperties,
+ interactiveProperties,
+);
+
+export type Sprinkles = Parameters[0];
+
+function transformBreakpoints