Manual Setup
Your first document
Getting started with twjlabs/ui is a breeze. This guide will walk you through the steps to set up your first project using our component library.
Installation
Install the required dependencies via npm or yarn
npm install clsx tailwind-mergeif you were using twjlabs/ui cli, it shouldve been pretty easy for you but guess you want to fuck up your mental health
So, first we need to copy and paste the following code in your main css file globals.css if you are using Next.js or main.css for react
Copy to globals.css
@import "tailwindcss";
@theme {
/* =========================================
1. CORE COMPONENT TOKENS
========================================= */
/* Backgrounds & Foregrounds */
--color-background: var(--color-background);
--color-foreground: var(--color-foreground);
/* Surfaces (Cards, Modals, Dropdowns) */
--color-card: var(--color-card);
--color-card-foreground: var(--color-card-foreground);
--color-popover: var(--color-popover);
--color-popover-foreground: var(--color-popover-foreground);
/* Brand Colors */
--color-primary: var(--color-primary);
--color-primary-foreground: var(--color-primary-foreground);
--color-primary-dark: var(--color-primary-dark);
--color-secondary: var(--color-secondary);
--color-secondary-foreground: var(--color-secondary-foreground);
/* State Colors */
--color-muted: var(--color-muted);
--color-muted-foreground: var(--color-muted-foreground);
--color-accent: var(--color-accent);
--color-accent-foreground: var(--color-accent-foreground);
--color-destructive: var(--color-destructive);
--color-destructive-foreground: var(--color-destructive-foreground);
/* Boundaries & Form Elements */
--color-border: var(--color-border);
--color-input: var(--color-input);
--color-ring: var(--color-ring);
/* =========================================
2. LEGACY / CUSTOM TOKENS (Preserved)
========================================= */
--color-gradient-one: var(--color-gradient-one);
--color-gradient-two: var(--color-gradient-two);
--color-gradient-three: var(--color-gradient-three);
--color-gr-three: var(--color-gr-three); /* Alias for gradient-three */
--color-surface: var(--color-surface); /* Legacy alias for card/background */
/* =========================================
3. TYPOGRAPHY & SHAPE
========================================= */
--radius-theme: var(--radius-theme);
--font-theme: var(--font-family);
--font-modern: var(--font-manrope);
--font-elegant: var(--font-lora);
--font-futuristic: var(--font-orbitron);
--font-playful: var(--font-fredoka);
--font-brutalist: var(--font-roboto-condensed);
--font-organic: var(--font-nunito);
--font-vintage: var(--font-grenze-gotisch);
}
/* =========================================
4. THEME DEFINITIONS
========================================= */
@layer utilities {
/* --- MODERN (Clean, Indigo, Standard) --- */
.theme-modern {
--color-background: #ffffff;
--color-foreground: #09090b;
--color-card: #ffffff;
--color-card-foreground: #09090b;
--color-popover: #ffffff;
--color-popover-foreground: #09090b;
--color-primary: #6366f1; /* Indigo 500 */
--color-primary-foreground: #ffffff;
--color-primary-dark: #4f46e5; /* Indigo 600 */
--color-secondary: #f4f4f5; /* Zinc 100 */
--color-secondary-foreground: #18181b;
--color-muted: #f4f4f5;
--color-muted-foreground: #71717a;
--color-accent: #f4f4f5; /* Used for hover states */
--color-accent-foreground: #18181b;
--color-destructive: #ef4444;
--color-destructive-foreground: #ffffff;
--color-border: #e4e4e7;
--color-input: #e4e4e7;
--color-ring: #6366f1;
--color-surface: #f0f0f0; /* Legacy */
--color-gradient-one: #6366f1;
--color-gradient-two: #8b5cf6;
--color-gradient-three: #a78bfa;
--color-gr-three: #a78bfa;
--font-family: var(--font-manrope);
--radius-theme: 0.5rem;
}
/* --- MINIMALIST (Monochrome, High Contrast) --- */
.theme-minimalist {
--color-background: #ffffff;
--color-foreground: #0f172a;
--color-card: #ffffff;
--color-card-foreground: #0f172a;
--color-popover: #ffffff;
--color-popover-foreground: #0f172a;
--color-primary: #18181b; /* Zinc 900 */
--color-primary-foreground: #fafafa;
--color-primary-dark: #000000;
--color-secondary: #f4f4f5;
--color-secondary-foreground: #18181b;
--color-muted: #f4f4f5;
--color-muted-foreground: #a1a1aa;
--color-accent: #f4f4f5;
--color-accent-foreground: #18181b;
--color-destructive: #dc2626;
--color-destructive-foreground: #fafafa;
--color-border: #e4e4e7;
--color-input: #e4e4e7;
--color-ring: #18181b;
--color-surface: #ffffff;
--color-gradient-one: #d4d4d8;
--color-gradient-two: #a1a1aa;
--color-gradient-three: #71717a;
--color-gr-three: #71717a;
--font-family: var(--font-inter); /* Standard Sans */
--radius-theme: 0.25rem;
}
/* --- ELEGANT (Serif, Warm, Purple/Gold) --- */
.theme-elegant {
--color-background: #faf7f2; /* Cream */
--color-foreground: #2d2424;
--color-card: #ffffff;
--color-card-foreground: #2d2424;
--color-popover: #ffffff;
--color-popover-foreground: #2d2424;
--color-primary: #7c3aed; /* Violet */
--color-primary-foreground: #ffffff;
--color-primary-dark: #6d28d9;
--color-secondary: #f3e8ff; /* Light Purple */
--color-secondary-foreground: #4c1d95;
--color-muted: #f5f3f0;
--color-muted-foreground: #8b7e7e;
--color-accent: #f3e8ff;
--color-accent-foreground: #4c1d95;
--color-destructive: #9f1239;
--color-destructive-foreground: #fff1f2;
--color-border: #e5e0da;
--color-input: #e5e0da;
--color-ring: #7c3aed;
--color-surface: #ffffff;
--color-gradient-one: #7c3aed;
--color-gradient-two: #d946ef;
--color-gradient-three: #fca5a5;
--color-gr-three: #fca5a5;
--font-family: var(--font-lora);
--radius-theme: 0.375rem;
}
/* --- FUTURISTIC (Dark, Neon Cyan, Tech) --- */
.theme-futuristic {
--color-background: #0a0f14;
--color-foreground: #e2ecf3;
--color-card: #111820;
--color-card-foreground: #e2ecf3;
--color-popover: #111820;
--color-popover-foreground: #e2ecf3;
--color-primary: #06b6d4; /* Cyan 500 */
--color-primary-foreground: #000000;
--color-primary-dark: #0891b2;
--color-secondary: #1e293b;
--color-secondary-foreground: #e2ecf3;
--color-muted: #1e293b;
--color-muted-foreground: #94a3b8;
--color-accent: #0f172a;
--color-accent-foreground: #22d3ee;
--color-destructive: #ef4444;
--color-destructive-foreground: #ffffff;
--color-border: #1e293b;
--color-input: #1e293b;
--color-ring: #06b6d4;
--color-surface: #111820;
--color-gradient-one: #06b6d4;
--color-gradient-two: #3b82f6;
--color-gradient-three: #6366f1;
--color-gr-three: #6366f1;
--font-family: var(--font-orbitron);
--radius-theme: 2px;
}
/* --- PLAYFUL (Pastel, Rounded, Red/Yellow) --- */
.theme-playful {
--color-background: #fffbee;
--color-foreground: #2b2b2b;
--color-card: #ffffff;
--color-card-foreground: #2b2b2b;
--color-popover: #ffffff;
--color-popover-foreground: #2b2b2b;
--color-primary: #ff6b6b; /* Soft Red */
--color-primary-foreground: #ffffff;
--color-primary-dark: #ee5253;
--color-secondary: #ffd93d; /* Yellow */
--color-secondary-foreground: #2b2b2b;
--color-muted: #fff0f0;
--color-muted-foreground: #7d7d7d;
--color-accent: #48dbfb; /* Cyan accent */
--color-accent-foreground: #ffffff;
--color-destructive: #ff6b6b;
--color-destructive-foreground: #ffffff;
--color-border: #ffd93d;
--color-input: #fff0f0;
--color-ring: #ff6b6b;
--color-surface: #ffffff;
--color-gradient-one: #ff6b6b;
--color-gradient-two: #ffd93d;
--color-gradient-three: #48dbfb;
--color-gr-three: #48dbfb;
--font-family: var(--font-fredoka);
--radius-theme: 1rem;
}
/* --- BRUTALIST (Bold, Sharp, Primary Blue) --- */
.theme-brutalist {
--color-background: #ffffff;
--color-foreground: #000000;
--color-card: #f3f3f3;
--color-card-foreground: #000000;
--color-popover: #ffffff;
--color-popover-foreground: #000000;
--color-primary: #2563eb; /* Blue 600 */
--color-primary-foreground: #ffffff;
--color-primary-dark: #1d4ed8;
--color-secondary: #bef264; /* Lime */
--color-secondary-foreground: #000000;
--color-muted: #e5e5e5;
--color-muted-foreground: #525252;
--color-accent: #bef264;
--color-accent-foreground: #000000;
--color-destructive: #000000;
--color-destructive-foreground: #ffffff;
--color-border: #000000;
--color-input: #ffffff;
--color-ring: #000000;
--color-surface: #f8f8f8;
--color-gradient-one: #2563eb;
--color-gradient-two: #000000;
--color-gradient-three: #bef264;
--color-gr-three: #bef264;
--font-family: var(--font-roboto-condensed);
--radius-theme: 0px;
}
/* --- ORGANIC (Nature, Green, Soft) --- */
.theme-organic {
--color-background: #f4f8f5;
--color-foreground: #2c4f2e;
--color-card: #ffffff;
--color-card-foreground: #2c4f2e;
--color-popover: #ffffff;
--color-popover-foreground: #2c4f2e;
--color-primary: #558b2f; /* Green 800 */
--color-primary-foreground: #ffffff;
--color-primary-dark: #33691e;
--color-secondary: #dcedc8; /* Light Green */
--color-secondary-foreground: #33691e;
--color-muted: #e8f5e9;
--color-muted-foreground: #558b2f;
--color-accent: #f1f8e9;
--color-accent-foreground: #2c4f2e;
--color-destructive: #c62828;
--color-destructive-foreground: #ffffff;
--color-border: #c5e1a5;
--color-input: #ffffff;
--color-ring: #558b2f;
--color-surface: #ffffff;
--color-gradient-one: #558b2f;
--color-gradient-two: #8bc34a;
--color-gradient-three: #dcedc8;
--color-gr-three: #dcedc8;
--font-family: var(--font-nunito);
--radius-theme: 1.5rem;
}
/* --- VINTAGE (Sepia, Paper, Brown) --- */
.theme-vintage {
--color-background: #fdf6e3;
--color-foreground: #5c4033;
--color-card: #f4ecd8;
--color-card-foreground: #5c4033;
--color-popover: #f4ecd8;
--color-popover-foreground: #5c4033;
--color-primary: #8d6e63; /* Brown 400 */
--color-primary-foreground: #ffffff;
--color-primary-dark: #6d4c41;
--color-secondary: #efebe9;
--color-secondary-foreground: #4e342e;
--color-muted: #d7ccc8;
--color-muted-foreground: #8d6e63;
--color-accent: #efebe9;
--color-accent-foreground: #3e2723;
--color-destructive: #8c2f39;
--color-destructive-foreground: #ffffff;
--color-border: #bcaaa4;
--color-input: #ffffff;
--color-ring: #8d6e63;
--color-surface: #ffffff;
--color-gradient-one: #8d6e63;
--color-gradient-two: #a1887f;
--color-gradient-three: #d7ccc8;
--color-gr-three: #d7ccc8;
--font-family: var(--font-grenze-gotisch);
--radius-theme: 0.5rem;
}
}
@layer base {
body {
@apply bg-background text-foreground border-border;
}
}Create ui-theme context
Now you need to create a folder in your root directory named contexts and inside that folder, create a file named ui-theme-context.tsx and copy-paste the below code into that file
"use client";
import React, { createContext, useContext, useState } from "react";
import type { Theme } from "@/twj-lib/types";
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({
children,
initialTheme = "modern",
}: {
children: React.ReactNode;
initialTheme?: Theme;
}) {
// ✅ 1. Initialize State with the prop.
// This happens once on mount. You don't need useEffect to set this.
const [theme, setTheme] = useState<Theme>(initialTheme);
// ❌ REMOVED: The useEffect was causing the double-render/sync error.
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}copy the below code into twj-lib/tw.tsx
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}copy the below code into twj-lib/types.ts
export type Theme = 'modern' | 'elegant' | 'futuristic' | 'playful' | 'brutalist' | 'organic' | 'minimalist' | 'vintage';
export const themes: Theme[] = ['modern', 'elegant', 'futuristic', 'playful', 'brutalist', 'organic', 'minimalist', 'vintage'];
export interface TWJComponentsProps {
theme?: Theme;
}Create AI Context
You need to create another file in the contexts folder named ai-context.tsx and copy-paste the below code into that file
"use client"
import { TWJComponentsProps } from "@/twj-lib/types";
// AIContextProvider.tsx
import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react';
// 2. The definition of a component tool
export type AIAction = (...args: any[]) => Promise<void> | void;
export interface AIRegisteredComponent {
id: string;
description: string;
// We store the actual functions here, but we won't send this object to the AI directly
actions: Record<string, AIAction>;
}
// 3. YOUR INTERFACE (The State) - This is what we feed the LLM
export interface AIContextState extends TWJComponentsProps {
route: {
path: string;
params?: Record<string, string>; // Made optional for flexibility
};
pageContent: {
rawText: string;
lastUpdated: number;
};
// We only expose the NAMES of actions to the state to keep it clean/serializable
activeComponents: Map<string, {
id: string;
description: string;
availableActions: string[];
}>;
assistantPersona: 'helpful_guide' | 'technical_debugger' | 'sales_rep';
knowledgeBase?: string;
}
// 4. The Context Value (State + Methods)
export interface AIContextValue {
state: AIContextState;
// Methods to control the system
registerComponent: (comp: AIRegisteredComponent) => void;
unregisterComponent: (id: string) => void;
executeAction: (componentId: string, actionName: string, actionValue?:any) => Promise<void>;
setPersona: (persona: AIContextState['assistantPersona']) => void;
}
const AIContext = createContext<AIContextValue | null>(null);
interface AIProviderProps extends TWJComponentsProps {
children: React.ReactNode;
currentPath?: string;
initialPersona?: AIContextState['assistantPersona'];
knowledgeBase?: string;
}
export const AIContextProvider: React.FC<AIProviderProps> = ({
children,
currentPath = typeof window !== 'undefined' ? window.location.pathname : '/',
initialPersona = 'helpful_guide',
knowledgeBase='',
...rest // Capture TWJ props
}) => {
// --- INTERNAL STATE (The Shadow Registry) ---
// We use a Ref for the functions to avoid re-renders when actions technically change identity
// but logically stay the same.
const actionRegistry = useRef<Map<string, AIRegisteredComponent>>(new Map());
// --- PUBLIC STATE (What the AI Sees) ---
const [state, setState] = useState<AIContextState>({
...rest,
route: { path: currentPath, params: {} },
pageContent: { rawText: '', lastUpdated: Date.now() },
activeComponents: new Map(),
assistantPersona: initialPersona,
knowledgeBase: knowledgeBase
});
// 1. AWARENESS: Register Components
const registerComponent = useCallback((comp: AIRegisteredComponent) => {
// A. Update the Shadow Registry (Logic)
actionRegistry.current.set(comp.id, comp);
// B. Update the Public State (Visibility)
setState((prev) => {
const newMap = new Map(prev.activeComponents);
newMap.set(comp.id, {
id: comp.id,
description: comp.description,
availableActions: Object.keys(comp.actions) // Only store strings!
});
return { ...prev, activeComponents: newMap };
});
}, []);
const unregisterComponent = useCallback((id: string) => {
actionRegistry.current.delete(id);
setState((prev) => {
const newMap = new Map(prev.activeComponents);
newMap.delete(id);
return { ...prev, activeComponents: newMap };
});
}, []);
// 2. ACTION: The Executor
const executeAction = useCallback(async (componentId: string, actionName: string, actionValue?:any) => {
const comp = actionRegistry.current.get(componentId);
if (!comp) {
console.warn(`[AI] Component not found: ${componentId}`);
return;
}
const action = comp.actions[actionName];
if (action) {
console.log(`[AI] Executing: ${componentId} > ${actionName} with value:`, actionValue);
await action(actionValue);
} else {
console.warn(`[AI] Action '${actionName}' not found on ${componentId}`);
}
}, []);
// 3. SIGHT: DOM Observer
useEffect(() => {
if (typeof window === 'undefined') return;
let timeoutId: NodeJS.Timeout;
const scrapeDOM = () => {
const idleCallback = (window as any).requestIdleCallback || ((cb: any) => setTimeout(cb, 1));
idleCallback(() => {
// Basic scraping strategy
const text = document.body.innerText
.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gm, "") // remove scripts
.replace(/\s+/g, ' ')
.trim()
.slice(0, 10000); // Limit context size for tokens
setState(prev => {
if (prev.pageContent.rawText === text) return prev;
return {
...prev,
pageContent: { rawText: text, lastUpdated: Date.now() }
};
});
});
};
const observer = new MutationObserver(() => {
clearTimeout(timeoutId);
timeoutId = setTimeout(scrapeDOM, 1000); // 1s debounce
});
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
scrapeDOM();
return () => {
observer.disconnect();
clearTimeout(timeoutId);
};
}, [state.route.path]); // Re-scrape when path changes
// 4. ROUTE: Update state when prop changes
useEffect(() => {
setState(prev => ({
...prev,
route: { ...prev.route, path: currentPath }
}));
}, [currentPath]);
// 🆕 Update state if the prop changes dynamically (e.g. if you fetch docs)
useEffect(() => {
setState(prev => ({ ...prev, knowledgeBase }));
}, [knowledgeBase]);
const setPersona = (persona: AIContextState['assistantPersona']) => {
setState(prev => ({ ...prev, assistantPersona: persona }));
};
return (
<AIContext.Provider value={{ state, registerComponent, unregisterComponent, executeAction, setPersona }}>
{children}
</AIContext.Provider>
);
};
export const useAIContext = () => {
const context = useContext(AIContext);
if (!context) throw new Error("useAIContext must be used within AIContextProvider");
return context;
};
// ============== HOOK ==============
//=======================================
interface UseAIControlProps {
id: string;
description: string;
actions: Record<string, AIAction>;
}
export const useAIControl = ({ id, description, actions }: UseAIControlProps) => {
const { registerComponent, unregisterComponent } = useAIContext();
// Keep actions stable in a ref so we don't re-register constantly
const actionsRef = useRef(actions);
actionsRef.current = actions;
useEffect(() => {
registerComponent({
id,
description,
actions: actionsRef.current,
});
return () => {
unregisterComponent(id);
};
}, [id, description, registerComponent, unregisterComponent]);
};BRO , YOU MADE IT TILL HERE! HATS OFF TO YOU, I still dont understand why the fuck you are not using @twjlabs-cli
We're still not done lil bro, we still got a few more things to do alright
Changes in layout.tsx
There is one more step to complete the setup. You need to wrap your application with the ThemeProvider and add fonts to enable theming and global styles. Open your layout.tsx file and make the following changes:
import './globals.css';
import { ThemeProvider } from '@/contexts/ui-theme-context';
import { AIContextProvider } from "@/contexts/ai-context";
// 1. Import the fonts
import {
Manrope,
Lora,
Orbitron,
Fredoka,
Roboto_Condensed,
Nunito,
Grenze_Gotisch
} from "next/font/google";
// 2. Setup the font instances with specific variable names
const manrope = Manrope({ subsets: ["latin"], variable: "--font-manrope", display: "swap" });
const lora = Lora({ subsets: ["latin"], variable: "--font-lora", display: "swap" });
const orbitron = Orbitron({ subsets: ["latin"], variable: "--font-orbitron", display: "swap" });
const fredoka = Fredoka({ subsets: ["latin"], variable: "--font-fredoka", display: "swap" });
const robotoCondensed = Roboto_Condensed({ subsets: ["latin"], variable: "--font-roboto-condensed", display: "swap" });
const nunito = Nunito({ subsets: ["latin"], variable: "--font-nunito", display: "swap" });
const grenzeGotisch = Grenze_Gotisch({ subsets: ["latin"], variable: "--font-grenze-gotisch", display: "swap" });
export default function Layout({ children }: LayoutProps<'/'>) {
return (
// Add any theme you want as initialTheme
<ThemeProvider initialTheme='brutalist'>
<AIContextProvider
initialPersona="technical_debugger"
>
<html lang="en" className={`${manrope.variable}
${lora.variable}
${orbitron.variable}
${fredoka.variable}
${robotoCondensed.variable}
${nunito.variable}
${grenzeGotisch.variable}`
}
suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
{children}
</body>
</html>
</AIContextProvider>
</ThemeProvider>
);
}Ok so, first you need to import the ThemeProvider from the ui-theme-context and AIContextProvider from the ai-context. Then, import the fonts you want to use from next/font/google. Next, create instances of each font with a unique CSS variable name. Finally, wrap your application in the ThemeProvider and AIContextProvider, setting an initial theme of your choice, and apply the font variables to the <html> element.
That's it! You're now ready to start building with twjlabs/ui. Happy coding!
Walah! You have successfully set up twjlabs/ui in your project manually. Althogh i still think you are crazy for not using the CLI but hey, to each their own. Enjoy building with twjlabs/ui! 🚀
Next Steps
- Explore the Components to see what you can use in your project.
- Check out the Theming Guide to customize the look and feel of your application.