Este documento é a fonte única de verdade para implementar a SDK Dheme. Ele contém toda a informação necessária para que uma IA possa construir a SDK completa do zero.
- Visão Geral
- Estrutura do Projeto
- API Reference Completa
- Implementação da SDK Base (@dheme/sdk)
- Implementação da React Library (@dheme/react)
- Exemplos de Uso
- Testes
- Build e Publicação
A SDK Dheme consiste em dois pacotes npm organizados em um monorepo:
-
@dheme/sdk - SDK base TypeScript pura, agnóstica de framework
- Compatível com: Browser, Node.js, Edge Runtime, React Native
- Features: HTTP client, retry, interceptors, error handling, rate limiting, color conversion
-
@dheme/react - Bindings React que dependem da SDK base
- Features: ThemeProvider, hooks customizados, CSS variables, localStorage, SSR-safe
- TypeScript 5.0+ - Type safety
- tsup - Build tool (gera CJS + ESM + types)
- Vitest - Testing
- npm workspaces - Monorepo
- Fetch API - HTTP (nativo no Browser/Node 18+)
sdk/
├── README.md # Este arquivo
├── .gitignore
├── package.json # Root (workspaces)
├── tsconfig.json # Base config
├── .eslintrc.json
├── .prettierrc
└── packages/
├── sdk/ # @dheme/sdk
│ ├── package.json
│ ├── tsconfig.json
│ ├── README.md
│ ├── src/
│ │ ├── index.ts
│ │ ├── client.ts
│ │ ├── types.ts
│ │ ├── errors.ts
│ │ ├── constants.ts
│ │ ├── middleware/
│ │ │ ├── retry.ts
│ │ │ └── interceptors.ts
│ │ └── utils/
│ │ ├── request.ts
│ │ ├── validators.ts
│ │ └── colorConversion.ts
│ └── examples/
│ ├── basic.ts
│ ├── nodejs.ts
│ └── edge.ts
└── react/ # @dheme/react
├── package.json
├── tsconfig.json
├── README.md
├── src/
│ ├── index.ts
│ ├── ThemeProvider.tsx
│ ├── ThemeContext.tsx
│ ├── hooks/
│ │ ├── useGenerateTheme.ts
│ │ └── useTheme.ts
│ └── utils/
│ ├── cssVariables.ts
│ └── localStorage.ts
└── examples/
├── basic.tsx
└── nextjs.tsx
Production: https://theme.dheme.com (substituir pela URL real)
Todas as requisições públicas requerem API key no header:
x-api-key: dheme_xxxxxxxx_[resto-da-chave]Formato da API Key:
- Prefixo:
dheme_ - 8 caracteres alfanuméricos (identificador)
- Underscore
_ - Resto da chave em base64url
Exemplo: dheme_abc12345_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789
Todos os endpoints retornam headers de rate limit:
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9847
X-RateLimit-Reset: 2025-01-01T00:00:00.000ZQuando o limite é excedido, retorna 429 Too Many Requests.
Gera um tema completo com todos os tokens de cor (light e dark mode).
Request Headers:
Content-Type: application/json
x-api-key: dheme_xxxxxxxx_...Request Body:
{
"theme": "#3b82f6", // Cor primária (HEX obrigatório)
"secondaryColor": "#10b981", // Cor secundária (HEX opcional)
"radius": 0.5, // Border radius em rem (0-2, default: 0.5)
"saturationAdjust": 0, // Ajuste saturação -100 a 100 (default: 0)
"lightnessAdjust": 0, // Ajuste luminosidade -100 a 100 (default: 0)
"contrastAdjust": 0, // Ajuste contraste -100 a 100 (default: 0)
"cardIsColored": false, // Cards com cor (default: false)
"backgroundIsColored": true // Background com cor (default: true)
}Response 200 OK:
{
"theme": "#3b82f6",
"secondaryColor": "#10b981",
"radius": 0.5,
"saturationAdjust": 0,
"lightnessAdjust": 0,
"contrastAdjust": 0,
"cardIsColored": false,
"backgroundIsColored": true,
"colors": {
"light": {
"background": { "h": 0, "s": 0, "l": 100 },
"foreground": { "h": 222.2, "s": 84, "l": 4.9 },
"card": { "h": 0, "s": 0, "l": 100 },
"cardForeground": { "h": 222.2, "s": 84, "l": 4.9 },
"popover": { "h": 0, "s": 0, "l": 100 },
"popoverForeground": { "h": 222.2, "s": 84, "l": 4.9 },
"primary": { "h": 221.2, "s": 83.2, "l": 53.3 },
"primaryForeground": { "h": 210, "s": 40, "l": 98 },
"secondary": { "h": 210, "s": 40, "l": 96.1 },
"secondaryForeground": { "h": 222.2, "s": 47.4, "l": 11.2 },
"muted": { "h": 210, "s": 40, "l": 96.1 },
"mutedForeground": { "h": 215.4, "s": 16.3, "l": 46.9 },
"accent": { "h": 210, "s": 40, "l": 96.1 },
"accentForeground": { "h": 222.2, "s": 47.4, "l": 11.2 },
"destructive": { "h": 0, "s": 84.2, "l": 60.2 },
"destructiveForeground": { "h": 210, "s": 40, "l": 98 },
"border": { "h": 214.3, "s": 31.8, "l": 91.4 },
"input": { "h": 214.3, "s": 31.8, "l": 91.4 },
"ring": { "h": 221.2, "s": 83.2, "l": 53.3 }
},
"dark": {
"background": { "h": 222.2, "s": 84, "l": 4.9 },
"foreground": { "h": 210, "s": 40, "l": 98 },
// ... similar structure
}
},
"backgrounds": {
"primary": {
"light": "#ffffff",
"dark": "#0a0a0a"
},
"secondary": {
"light": "#f5f5f5",
"dark": "#1a1a1a"
}
}
}Response 400 Bad Request:
{
"error": "Invalid theme color"
}Response 401 Unauthorized:
{
"error": "Missing API key. Provide it in the x-api-key header."
}Response 429 Too Many Requests:
{
"error": "Rate limit exceeded. Upgrade your plan or wait until the next billing cycle.",
"limit": 10000,
"resetAt": "2025-01-01T00:00:00.000Z",
"plan": "basic"
}Retorna CSS pronto para uso com variáveis CSS do Shadcn/ui.
Request: Idêntico ao endpoint /api/generate-theme
Response 200 OK:
Content-Type: text/css
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}Retorna tokens com múltiplos formatos de cor (HSL, RGB, HEX).
Request: Idêntico ao endpoint /api/generate-theme
Response 200 OK:
{
"light": {
"background": {
"hsl": { "h": 0, "s": 0, "l": 100 },
"rgb": [255, 255, 255],
"hex": "#ffffff"
},
"foreground": {
"hsl": { "h": 222.2, "s": 84, "l": 4.9 },
"rgb": [2, 8, 23],
"hex": "#020817"
},
"primary": {
"hsl": { "h": 221.2, "s": 83.2, "l": 53.3 },
"rgb": [59, 130, 246],
"hex": "#3b82f6"
}
// ... todos os tokens
},
"dark": {
"background": {
"hsl": { "h": 222.2, "s": 84, "l": 4.9 },
"rgb": [2, 8, 23],
"hex": "#020817"
}
// ... todos os tokens
},
"radius": 0.5,
"backgrounds": {
"primary": { "light": "#ffffff", "dark": "#0a0a0a" },
"secondary": { "light": "#f5f5f5", "dark": "#1a1a1a" }
}
}Retorna estatísticas de uso da API key.
Request Headers:
x-api-key: dheme_xxxxxxxx_...Response 200 OK:
{
"usage": 847,
"limit": 10000,
"remaining": 9153,
"percentage": 8.47,
"resetAt": "2025-01-01T00:00:00.000Z",
"plan": "basic"
}Criar arquivo /package.json:
{
"name": "dheme-sdk-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "npm run build --workspaces",
"dev": "npm run dev --workspaces",
"test": "npm run test --workspaces",
"lint": "npm run lint --workspaces",
"typecheck": "npm run typecheck --workspaces"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.3.0",
"eslint": "^8.56.0",
"@typescript-eslint/parser": "^6.19.0",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"prettier": "^3.2.0"
}
}Criar arquivo /tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}Criar arquivo /.gitignore:
node_modules/
dist/
*.tsbuildinfo
.DS_Store
*.log
coverage/
.env
.env.local
Criar arquivo /.prettierrc:
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"arrowParens": "always"
}Criar arquivo /packages/sdk/package.json:
{
"name": "@dheme/sdk",
"version": "1.0.0",
"description": "TypeScript SDK for Dheme Theme Generator API",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": ["dist", "README.md", "LICENSE"],
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src --ext .ts",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build"
},
"keywords": [
"dheme",
"theme",
"generator",
"shadcn",
"tailwind",
"sdk",
"typescript",
"api-client"
],
"author": "Dheme Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/dheme/sdk"
},
"homepage": "https://dheme.com",
"bugs": {
"url": "https://github.com/dheme/sdk/issues"
},
"dependencies": {},
"devDependencies": {
"@types/node": "^20.0.0",
"tsup": "^8.0.1",
"typescript": "^5.3.0",
"vitest": "^1.2.0",
"eslint": "^8.56.0"
},
"engines": {
"node": ">=18.0.0"
}
}Criar arquivo /packages/sdk/tsconfig.json:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}Criar arquivo /packages/sdk/src/types.ts:
/**
* Configuração do cliente Dheme SDK
*/
export interface DhemeClientConfig {
/** API key (obrigatório) */
apiKey: string;
/** Base URL da API (opcional, default: production URL) */
baseUrl?: string;
/** Timeout em milissegundos (opcional, default: 30000) */
timeout?: number;
/** Configuração de retry (opcional) */
retryConfig?: RetryConfig;
/** Debug mode (opcional, default: false) */
debug?: boolean;
/** Interceptors de request/response (opcional) */
interceptors?: Interceptors;
}
/**
* Configuração de retry com backoff exponencial
*/
export interface RetryConfig {
/** Máximo de tentativas (default: 3) */
maxRetries: number;
/** Delay inicial em ms (default: 1000) */
initialDelay: number;
/** Delay máximo em ms (default: 10000) */
maxDelay: number;
/** Multiplicador do backoff (default: 2) */
backoffMultiplier: number;
/** Status codes que permitem retry (default: [408, 429, 500, 502, 503, 504]) */
retryableStatusCodes: number[];
}
/**
* Sistema de interceptors
*/
export interface Interceptors {
/** Interceptors de request */
request?: RequestInterceptor[];
/** Interceptors de response */
response?: ResponseInterceptor[];
}
export type RequestInterceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>;
export type ResponseInterceptor = (response: Response) => Response | Promise<Response>;
/**
* Configuração interna de request
*/
export interface RequestConfig {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
url: string;
headers: Record<string, string>;
body?: unknown;
timeout?: number;
}
/**
* Parâmetros para gerar tema
*/
export interface GenerateThemeRequest {
/** Cor primária em HEX (obrigatório) */
theme: string;
/** Cor secundária em HEX (opcional) */
secondaryColor?: string;
/** Border radius em rem, 0-2 (opcional, default: 0.5) */
radius?: number;
/** Ajuste de saturação, -100 a 100 (opcional, default: 0) */
saturationAdjust?: number;
/** Ajuste de luminosidade, -100 a 100 (opcional, default: 0) */
lightnessAdjust?: number;
/** Ajuste de contraste, -100 a 100 (opcional, default: 0) */
contrastAdjust?: number;
/** Cards com cor (opcional, default: false) */
cardIsColored?: boolean;
/** Background com cor (opcional, default: true) */
backgroundIsColored?: boolean;
}
/**
* Cor em formato HSL
*/
export interface HSLColor {
h: number; // 0-360
s: number; // 0-100
l: number; // 0-100
}
/**
* Cor em formato RGB
*/
export interface RGBColor {
r: number; // 0-255
g: number; // 0-255
b: number; // 0-255
}
/**
* Tokens de cor do Shadcn
*/
export interface ColorTokens {
background: HSLColor;
foreground: HSLColor;
card: HSLColor;
cardForeground: HSLColor;
popover: HSLColor;
popoverForeground: HSLColor;
primary: HSLColor;
primaryForeground: HSLColor;
secondary: HSLColor;
secondaryForeground: HSLColor;
muted: HSLColor;
mutedForeground: HSLColor;
accent: HSLColor;
accentForeground: HSLColor;
destructive: HSLColor;
destructiveForeground: HSLColor;
border: HSLColor;
input: HSLColor;
ring: HSLColor;
}
/**
* Response completo do tema gerado
*/
export interface GenerateThemeResponse {
theme: string;
secondaryColor: string;
radius: number;
saturationAdjust: number;
lightnessAdjust: number;
contrastAdjust: number;
cardIsColored: boolean;
backgroundIsColored: boolean;
colors: {
light: ColorTokens;
dark: ColorTokens;
};
backgrounds: {
primary: {
light: string;
dark: string;
};
secondary: {
light: string;
dark: string;
} | null;
};
}
/**
* Cor em múltiplos formatos
*/
export interface ColorFormats {
hsl: HSLColor;
rgb: [number, number, number];
hex: string;
}
/**
* Response de tokens com múltiplos formatos
*/
export interface TokensResponse {
light: Record<string, ColorFormats>;
dark: Record<string, ColorFormats>;
radius: number;
backgrounds: {
primary: { light: string; dark: string };
secondary: { light: string; dark: string } | null;
} | null;
}
/**
* Response de estatísticas de uso
*/
export interface UsageResponse {
usage: number;
limit: number;
remaining: number;
percentage: number;
resetAt: string;
plan: string;
}
/**
* Headers de rate limit
*/
export interface RateLimitHeaders {
limit: number;
remaining: number;
reset: string;
}
/**
* Response com informação de rate limit
*/
export interface ResponseWithRateLimit<T> {
data: T;
rateLimit: RateLimitHeaders;
}Criar arquivo /packages/sdk/src/errors.ts:
/**
* Erro base da SDK Dheme
*/
export class DhemeError extends Error {
constructor(
message: string,
public readonly statusCode?: number,
public readonly response?: unknown
) {
super(message);
this.name = 'DhemeError';
Object.setPrototypeOf(this, DhemeError.prototype);
}
}
/**
* Erro de autenticação (401)
*/
export class AuthenticationError extends DhemeError {
constructor(message: string, response?: unknown) {
super(message, 401, response);
this.name = 'AuthenticationError';
Object.setPrototypeOf(this, AuthenticationError.prototype);
}
}
/**
* Erro de validação (400)
*/
export class ValidationError extends DhemeError {
constructor(
message: string,
public readonly errors?: Record<string, string[]>,
response?: unknown
) {
super(message, 400, response);
this.name = 'ValidationError';
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
/**
* Erro de rate limit (429)
*/
export class RateLimitError extends DhemeError {
constructor(
message: string,
public readonly limit: number,
public readonly resetAt: string,
public readonly plan: string,
response?: unknown
) {
super(message, 429, response);
this.name = 'RateLimitError';
Object.setPrototypeOf(this, RateLimitError.prototype);
}
}
/**
* Erro de rede/timeout
*/
export class NetworkError extends DhemeError {
constructor(
message: string,
public readonly cause?: Error
) {
super(message);
this.name = 'NetworkError';
Object.setPrototypeOf(this, NetworkError.prototype);
}
}
/**
* Erro interno do servidor (500)
*/
export class ServerError extends DhemeError {
constructor(message: string, response?: unknown) {
super(message, 500, response);
this.name = 'ServerError';
Object.setPrototypeOf(this, ServerError.prototype);
}
}Criar arquivo /packages/sdk/src/constants.ts:
import type { RetryConfig } from './types';
/**
* URL base da API (produção)
* TODO: Substituir pela URL real quando disponível
*/
export const DEFAULT_BASE_URL = 'https://theme.dheme.com';
/**
* Timeout padrão (30 segundos)
*/
export const DEFAULT_TIMEOUT = 30000;
/**
* Configuração de retry padrão
*/
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 3,
initialDelay: 1000,
maxDelay: 10000,
backoffMultiplier: 2,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
};
/**
* Headers padrão
*/
export const DEFAULT_HEADERS = {
'Content-Type': 'application/json',
'User-Agent': 'dheme-sdk/1.0.0',
};Criar arquivo /packages/sdk/src/utils/colorConversion.ts:
import type { HSLColor, RGBColor } from '../types';
/**
* Converte HEX para HSL
*/
export function hexToHSL(hex: string): HSLColor {
// Remove # se presente
hex = hex.replace('#', '');
// Parse RGB
const r = parseInt(hex.substring(0, 2), 16) / 255;
const g = parseInt(hex.substring(2, 4), 16) / 255;
const b = parseInt(hex.substring(4, 6), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
break;
case g:
h = ((b - r) / d + 2) / 6;
break;
case b:
h = ((r - g) / d + 4) / 6;
break;
}
}
return {
h: Math.round(h * 360 * 10) / 10,
s: Math.round(s * 100 * 10) / 10,
l: Math.round(l * 100 * 10) / 10,
};
}
/**
* Converte HSL para RGB
*/
export function hslToRGB(hsl: HSLColor): RGBColor {
const h = hsl.h / 360;
const s = hsl.s / 100;
const l = hsl.l / 100;
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
};
}
/**
* Converte RGB para HEX
*/
export function rgbToHex(rgb: RGBColor): string {
const toHex = (n: number) => {
const hex = n.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
}
/**
* Converte HSL para HEX
*/
export function hslToHex(hsl: HSLColor): string {
return rgbToHex(hslToRGB(hsl));
}
/**
* Converte HEX para RGB
*/
export function hexToRGB(hex: string): RGBColor {
hex = hex.replace('#', '');
return {
r: parseInt(hex.substring(0, 2), 16),
g: parseInt(hex.substring(2, 4), 16),
b: parseInt(hex.substring(4, 6), 16),
};
}
/**
* Converte RGB para HSL
*/
export function rgbToHSL(rgb: RGBColor): HSLColor {
return hexToHSL(rgbToHex(rgb));
}
/**
* Formata HSL para string CSS (ex: "221.2 83.2% 53.3%")
*/
export function formatHSLString(hsl: HSLColor): string {
return `${hsl.h} ${hsl.s}% ${hsl.l}%`;
}
/**
* Valida se string é HEX válido
*/
export function isValidHex(hex: string): boolean {
return /^#?[0-9A-Fa-f]{6}$/.test(hex);
}Criar arquivo /packages/sdk/src/utils/validators.ts:
import type { GenerateThemeRequest } from '../types';
import { ValidationError } from '../errors';
import { isValidHex } from './colorConversion';
/**
* Valida parâmetros de geração de tema
*/
export function validateGenerateThemeRequest(params: GenerateThemeRequest): void {
const errors: Record<string, string[]> = {};
// Validar theme (obrigatório)
if (!params.theme) {
errors.theme = ['Theme color is required'];
} else if (!isValidHex(params.theme)) {
errors.theme = ['Theme must be a valid HEX color'];
}
// Validar secondaryColor (opcional)
if (params.secondaryColor && !isValidHex(params.secondaryColor)) {
errors.secondaryColor = ['Secondary color must be a valid HEX color'];
}
// Validar radius
if (params.radius !== undefined) {
if (typeof params.radius !== 'number' || params.radius < 0 || params.radius > 2) {
errors.radius = ['Radius must be a number between 0 and 2'];
}
}
// Validar ajustes (-100 a 100)
const adjustments = ['saturationAdjust', 'lightnessAdjust', 'contrastAdjust'] as const;
for (const key of adjustments) {
const value = params[key];
if (value !== undefined) {
if (typeof value !== 'number' || value < -100 || value > 100) {
errors[key] = [`${key} must be a number between -100 and 100`];
}
}
}
// Validar booleans
const booleans = ['cardIsColored', 'backgroundIsColored'] as const;
for (const key of booleans) {
const value = params[key];
if (value !== undefined && typeof value !== 'boolean') {
errors[key] = [`${key} must be a boolean`];
}
}
// Se houver erros, lançar ValidationError
if (Object.keys(errors).length > 0) {
throw new ValidationError('Invalid request parameters', errors);
}
}
/**
* Valida formato da API key
*/
export function validateApiKey(apiKey: string): void {
if (!apiKey) {
throw new ValidationError('API key is required');
}
// Formato: dheme_[8 chars]_[...]
const format = /^dheme_[a-zA-Z0-9]{8}_[a-zA-Z0-9_-]+$/;
if (!format.test(apiKey)) {
throw new ValidationError('Invalid API key format. Expected: dheme_xxxxxxxx_...');
}
}Criar arquivo /packages/sdk/src/utils/request.ts:
import { NetworkError } from '../errors';
import type { RequestConfig, RateLimitHeaders } from '../types';
/**
* Detecta se está rodando em Node.js
*/
export function isNode(): boolean {
return (
typeof process !== 'undefined' && process.versions != null && process.versions.node != null
);
}
/**
* Faz HTTP request universal (Browser/Node.js/Edge)
*/
export async function makeRequest(config: RequestConfig): Promise<Response> {
const { method, url, headers, body, timeout = 30000 } = config;
// Controller para timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error) {
if (error.name === 'AbortError') {
throw new NetworkError(`Request timeout after ${timeout}ms`, error);
}
throw new NetworkError(`Network request failed: ${error.message}`, error);
}
throw new NetworkError('Network request failed with unknown error');
}
}
/**
* Parse rate limit headers do response
*/
export function parseRateLimitHeaders(headers: Headers): RateLimitHeaders | null {
const limit = headers.get('x-ratelimit-limit');
const remaining = headers.get('x-ratelimit-remaining');
const reset = headers.get('x-ratelimit-reset');
if (!limit || !remaining || !reset) {
return null;
}
return {
limit: parseInt(limit, 10),
remaining: parseInt(remaining, 10),
reset,
};
}Criar arquivo /packages/sdk/src/middleware/retry.ts:
import type { RetryConfig } from '../types';
import { DEFAULT_RETRY_CONFIG } from '../constants';
/**
* Executa função com retry e backoff exponencial
*/
export async function withRetry<T>(
fn: () => Promise<T>,
config: Partial<RetryConfig> = {}
): Promise<T> {
const { maxRetries, initialDelay, maxDelay, backoffMultiplier, retryableStatusCodes } = {
...DEFAULT_RETRY_CONFIG,
...config,
};
let lastError: Error | undefined;
let delay = initialDelay;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
// Verificar se deve fazer retry
const shouldRetry =
attempt < maxRetries &&
error instanceof Error &&
'statusCode' in error &&
typeof error.statusCode === 'number' &&
retryableStatusCodes.includes(error.statusCode);
if (!shouldRetry) {
throw error;
}
// Aguardar antes de tentar novamente
await sleep(delay);
// Aumentar delay com backoff exponencial
delay = Math.min(delay * backoffMultiplier, maxDelay);
}
}
throw lastError;
}
/**
* Sleep helper
*/
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}Criar arquivo /packages/sdk/src/middleware/interceptors.ts:
import type { RequestInterceptor, ResponseInterceptor, RequestConfig } from '../types';
/**
* Aplica interceptors de request
*/
export async function applyRequestInterceptors(
config: RequestConfig,
interceptors?: RequestInterceptor[]
): Promise<RequestConfig> {
if (!interceptors || interceptors.length === 0) {
return config;
}
let currentConfig = config;
for (const interceptor of interceptors) {
currentConfig = await interceptor(currentConfig);
}
return currentConfig;
}
/**
* Aplica interceptors de response
*/
export async function applyResponseInterceptors(
response: Response,
interceptors?: ResponseInterceptor[]
): Promise<Response> {
if (!interceptors || interceptors.length === 0) {
return response;
}
let currentResponse = response;
for (const interceptor of interceptors) {
currentResponse = await interceptor(currentResponse);
}
return currentResponse;
}
/**
* Interceptor de logging (exemplo)
*/
export const loggingInterceptor: RequestInterceptor = (config) => {
console.log('[Dheme SDK Request]', config.method, config.url);
return config;
};
/**
* Interceptor de custom headers (exemplo)
*/
export function customHeadersInterceptor(headers: Record<string, string>): RequestInterceptor {
return (config) => ({
...config,
headers: { ...config.headers, ...headers },
});
}
/**
* Interceptor de response logging (exemplo)
*/
export const responseLoggingInterceptor: ResponseInterceptor = (response) => {
console.log('[Dheme SDK Response]', response.status, response.statusText);
return response;
};Criar arquivo /packages/sdk/src/client.ts:
import {
DEFAULT_BASE_URL,
DEFAULT_TIMEOUT,
DEFAULT_HEADERS,
DEFAULT_RETRY_CONFIG,
} from './constants';
import {
AuthenticationError,
ValidationError,
RateLimitError,
ServerError,
DhemeError,
} from './errors';
import type {
DhemeClientConfig,
GenerateThemeRequest,
GenerateThemeResponse,
TokensResponse,
UsageResponse,
ResponseWithRateLimit,
RequestConfig,
RateLimitHeaders,
} from './types';
import { validateApiKey, validateGenerateThemeRequest } from './utils/validators';
import { makeRequest, parseRateLimitHeaders } from './utils/request';
import { withRetry } from './middleware/retry';
import { applyRequestInterceptors, applyResponseInterceptors } from './middleware/interceptors';
/**
* Cliente principal da SDK Dheme
*/
export class DhemeClient {
private readonly config: Required<Omit<DhemeClientConfig, 'interceptors'>> & {
interceptors?: DhemeClientConfig['interceptors'];
};
constructor(config: DhemeClientConfig) {
// Validar API key
validateApiKey(config.apiKey);
// Configuração com defaults
this.config = {
apiKey: config.apiKey,
baseUrl: config.baseUrl || DEFAULT_BASE_URL,
timeout: config.timeout || DEFAULT_TIMEOUT,
retryConfig: { ...DEFAULT_RETRY_CONFIG, ...config.retryConfig },
debug: config.debug || false,
interceptors: config.interceptors,
};
if (this.config.debug) {
console.log('[DhemeClient] Initialized with config:', {
baseUrl: this.config.baseUrl,
timeout: this.config.timeout,
retryConfig: this.config.retryConfig,
});
}
}
/**
* Gera tema completo (JSON)
*/
async generateTheme(
params: GenerateThemeRequest
): Promise<ResponseWithRateLimit<GenerateThemeResponse>> {
validateGenerateThemeRequest(params);
return this.makeApiRequest<GenerateThemeResponse>('POST', '/api/generate-theme', params);
}
/**
* Gera CSS do Shadcn
*/
async generateShadcnCSS(params: GenerateThemeRequest): Promise<string> {
validateGenerateThemeRequest(params);
const response = await this.makeRawRequest('POST', '/api/generate-theme/shadcn', params);
return response.text();
}
/**
* Gera tokens com múltiplos formatos
*/
async generateTokens(
params: GenerateThemeRequest
): Promise<ResponseWithRateLimit<TokensResponse>> {
validateGenerateThemeRequest(params);
return this.makeApiRequest<TokensResponse>('POST', '/api/generate-theme/tokens', params);
}
/**
* Obtém estatísticas de uso
*/
async getUsage(): Promise<ResponseWithRateLimit<UsageResponse>> {
return this.makeApiRequest<UsageResponse>('GET', '/api/usage');
}
/**
* Faz request HTTP com retry e error handling
*/
private async makeApiRequest<T>(
method: 'GET' | 'POST',
endpoint: string,
body?: unknown
): Promise<ResponseWithRateLimit<T>> {
return withRetry(async () => {
const response = await this.makeRawRequest(method, endpoint, body);
// Parse JSON
const data = await response.json();
// Parse rate limit headers
const rateLimit = parseRateLimitHeaders(response.headers);
return {
data: data as T,
rateLimit: rateLimit || {
limit: 0,
remaining: 0,
reset: new Date().toISOString(),
},
};
}, this.config.retryConfig);
}
/**
* Faz request HTTP raw
*/
private async makeRawRequest(
method: 'GET' | 'POST',
endpoint: string,
body?: unknown
): Promise<Response> {
const url = `${this.config.baseUrl}${endpoint}`;
// Configuração do request
let requestConfig: RequestConfig = {
method,
url,
headers: {
...DEFAULT_HEADERS,
'x-api-key': this.config.apiKey,
},
body,
timeout: this.config.timeout,
};
// Aplicar interceptors de request
requestConfig = await applyRequestInterceptors(
requestConfig,
this.config.interceptors?.request
);
if (this.config.debug) {
console.log('[DhemeClient] Request:', requestConfig);
}
// Fazer request
let response = await makeRequest(requestConfig);
// Aplicar interceptors de response
response = await applyResponseInterceptors(response, this.config.interceptors?.response);
// Handle errors
if (!response.ok) {
await this.handleErrorResponse(response);
}
return response;
}
/**
* Handle error responses
*/
private async handleErrorResponse(response: Response): Promise<never> {
let errorData: any;
try {
errorData = await response.json();
} catch {
errorData = { error: response.statusText };
}
const message = errorData.error || 'Unknown error';
switch (response.status) {
case 400:
throw new ValidationError(message, undefined, errorData);
case 401:
throw new AuthenticationError(message, errorData);
case 429:
throw new RateLimitError(
message,
errorData.limit || 0,
errorData.resetAt || new Date().toISOString(),
errorData.plan || 'unknown',
errorData
);
case 500:
case 502:
case 503:
case 504:
throw new ServerError(message, errorData);
default:
throw new DhemeError(message, response.status, errorData);
}
}
}Criar arquivo /packages/sdk/src/index.ts:
// Cliente
export { DhemeClient } from './client';
// Tipos
export type {
DhemeClientConfig,
GenerateThemeRequest,
GenerateThemeResponse,
TokensResponse,
UsageResponse,
ColorTokens,
HSLColor,
RGBColor,
ColorFormats,
ResponseWithRateLimit,
RateLimitHeaders,
RetryConfig,
Interceptors,
RequestInterceptor,
ResponseInterceptor,
} from './types';
// Erros
export {
DhemeError,
AuthenticationError,
ValidationError,
RateLimitError,
NetworkError,
ServerError,
} from './errors';
// Utilitários de conversão de cor (exportados para uso público)
export {
hexToHSL,
hslToRGB,
rgbToHex,
hslToHex,
hexToRGB,
rgbToHSL,
formatHSLString,
isValidHex,
} from './utils/colorConversion';
// Interceptors úteis
export {
loggingInterceptor,
customHeadersInterceptor,
responseLoggingInterceptor,
} from './middleware/interceptors';Criar arquivo /packages/react/package.json:
{
"name": "@dheme/react",
"version": "1.0.0",
"description": "React bindings for Dheme SDK",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": ["dist", "README.md", "LICENSE"],
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean --external react --external react-dom",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch --external react --external react-dom",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build"
},
"keywords": ["dheme", "theme", "react", "hooks", "shadcn", "tailwind"],
"author": "Dheme Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/dheme/sdk"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"dependencies": {
"@dheme/sdk": "workspace:*"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@testing-library/react": "^14.1.0",
"@testing-library/react-hooks": "^8.0.1",
"tsup": "^8.0.1",
"typescript": "^5.3.0",
"vitest": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}Criar arquivo /packages/react/src/ThemeContext.tsx:
import { createContext } from 'react';
import type { GenerateThemeResponse, DhemeClient, GenerateThemeRequest } from '@dheme/sdk';
export interface ThemeContextValue {
client: DhemeClient;
theme: GenerateThemeResponse | null;
isLoading: boolean;
error: Error | null;
generateTheme: (params: GenerateThemeRequest) => Promise<void>;
clearTheme: () => void;
applyTheme: (mode: 'light' | 'dark') => void;
}
export const ThemeContext = createContext<ThemeContextValue | null>(null);Criar arquivo /packages/react/src/utils/cssVariables.ts:
import type { GenerateThemeResponse } from '@dheme/sdk';
import { formatHSLString } from '@dheme/sdk';
/**
* Aplica CSS variables do tema no document root
*/
export function applyThemeCSSVariables(theme: GenerateThemeResponse, mode: 'light' | 'dark'): void {
if (typeof document === 'undefined') return;
const root = document.documentElement;
const colors = theme.colors[mode];
// Aplicar variáveis CSS
root.style.setProperty('--background', formatHSLString(colors.background));
root.style.setProperty('--foreground', formatHSLString(colors.foreground));
root.style.setProperty('--card', formatHSLString(colors.card));
root.style.setProperty('--card-foreground', formatHSLString(colors.cardForeground));
root.style.setProperty('--popover', formatHSLString(colors.popover));
root.style.setProperty('--popover-foreground', formatHSLString(colors.popoverForeground));
root.style.setProperty('--primary', formatHSLString(colors.primary));
root.style.setProperty('--primary-foreground', formatHSLString(colors.primaryForeground));
root.style.setProperty('--secondary', formatHSLString(colors.secondary));
root.style.setProperty('--secondary-foreground', formatHSLString(colors.secondaryForeground));
root.style.setProperty('--muted', formatHSLString(colors.muted));
root.style.setProperty('--muted-foreground', formatHSLString(colors.mutedForeground));
root.style.setProperty('--accent', formatHSLString(colors.accent));
root.style.setProperty('--accent-foreground', formatHSLString(colors.accentForeground));
root.style.setProperty('--destructive', formatHSLString(colors.destructive));
root.style.setProperty('--destructive-foreground', formatHSLString(colors.destructiveForeground));
root.style.setProperty('--border', formatHSLString(colors.border));
root.style.setProperty('--input', formatHSLString(colors.input));
root.style.setProperty('--ring', formatHSLString(colors.ring));
root.style.setProperty('--radius', `${theme.radius}rem`);
// Backgrounds (se disponíveis)
if (theme.backgrounds?.primary) {
root.style.setProperty('--background-primary', theme.backgrounds.primary[mode]);
}
if (theme.backgrounds?.secondary) {
root.style.setProperty('--background-secondary', theme.backgrounds.secondary[mode]);
}
}
/**
* Remove CSS variables do tema
*/
export function removeThemeCSSVariables(): void {
if (typeof document === 'undefined') return;
const root = document.documentElement;
const variables = [
'--background',
'--foreground',
'--card',
'--card-foreground',
'--popover',
'--popover-foreground',
'--primary',
'--primary-foreground',
'--secondary',
'--secondary-foreground',
'--muted',
'--muted-foreground',
'--accent',
'--accent-foreground',
'--destructive',
'--destructive-foreground',
'--border',
'--input',
'--ring',
'--radius',
'--background-primary',
'--background-secondary',
];
variables.forEach((v) => root.style.removeProperty(v));
}Criar arquivo /packages/react/src/utils/localStorage.ts:
import type { GenerateThemeResponse } from '@dheme/sdk';
const STORAGE_KEY = 'dheme-theme';
/**
* Salva tema no localStorage
*/
export function saveThemeToStorage(theme: GenerateThemeResponse): void {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(theme));
} catch (error) {
console.warn('[Dheme] Failed to save theme to localStorage:', error);
}
}
/**
* Carrega tema do localStorage
*/
export function loadThemeFromStorage(): GenerateThemeResponse | null {
if (typeof window === 'undefined') return null;
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
return JSON.parse(stored);
}
} catch (error) {
console.warn('[Dheme] Failed to load theme from localStorage:', error);
}
return null;
}
/**
* Remove tema do localStorage
*/
export function removeThemeFromStorage(): void {
if (typeof window === 'undefined') return;
try {
localStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.warn('[Dheme] Failed to remove theme from localStorage:', error);
}
}Criar arquivo /packages/react/src/ThemeProvider.tsx:
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import {
DhemeClient,
type GenerateThemeRequest,
type GenerateThemeResponse,
} from '@dheme/sdk';
import { ThemeContext } from './ThemeContext';
import { applyThemeCSSVariables } from './utils/cssVariables';
import { loadThemeFromStorage, saveThemeToStorage } from './utils/localStorage';
export interface ThemeProviderProps {
/** API key (obrigatório) */
apiKey: string;
/** Base URL da API (opcional) */
baseUrl?: string;
/** Children components */
children: React.ReactNode;
/** Persistir tema no localStorage (default: true) */
persistTheme?: boolean;
/** Aplicar CSS variables automaticamente (default: true) */
autoApply?: boolean;
/** Modo inicial light/dark (default: 'light') */
mode?: 'light' | 'dark';
/** Callback quando tema mudar (opcional) */
onThemeChange?: (theme: GenerateThemeResponse | null) => void;
}
export function ThemeProvider({
apiKey,
baseUrl,
children,
persistTheme = true,
autoApply = true,
mode = 'light',
onThemeChange,
}: ThemeProviderProps) {
// Cliente SDK
const client = useMemo(
() => new DhemeClient({ apiKey, baseUrl }),
[apiKey, baseUrl]
);
// Estado
const [theme, setTheme] = useState<GenerateThemeResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Carregar tema do localStorage no mount (SSR-safe)
useEffect(() => {
if (persistTheme && typeof window !== 'undefined') {
const stored = loadThemeFromStorage();
if (stored) {
setTheme(stored);
}
}
}, [persistTheme]);
// Aplicar tema quando mudar
useEffect(() => {
if (theme && autoApply) {
applyThemeCSSVariables(theme, mode);
}
}, [theme, mode, autoApply]);
// Notificar mudança
useEffect(() => {
onThemeChange?.(theme);
}, [theme, onThemeChange]);
// Gerar tema
const generateTheme = useCallback(
async (params: GenerateThemeRequest) => {
setIsLoading(true);
setError(null);
try {
const response = await client.generateTheme(params);
setTheme(response.data);
if (persistTheme) {
saveThemeToStorage(response.data);
}
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error');
setError(error);
throw error;
} finally {
setIsLoading(false);
}
},
[client, persistTheme]
);
// Limpar tema
const clearTheme = useCallback(() => {
setTheme(null);
setError(null);
if (persistTheme && typeof window !== 'undefined') {
localStorage.removeItem('dheme-theme');
}
}, [persistTheme]);
// Aplicar tema manualmente
const applyTheme = useCallback(
(mode: 'light' | 'dark') => {
if (theme) {
applyThemeCSSVariables(theme, mode);
}
},
[theme]
);
const value = useMemo(
() => ({
client,
theme,
isLoading,
error,
generateTheme,
clearTheme,
applyTheme,
}),
[client, theme, isLoading, error, generateTheme, clearTheme, applyTheme]
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}Criar arquivo /packages/react/src/hooks/useTheme.ts:
import { useContext } from 'react';
import { ThemeContext } from '../ThemeContext';
/**
* Hook para acessar o ThemeContext
*/
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}Criar arquivo /packages/react/src/hooks/useGenerateTheme.ts:
import { useState, useCallback } from 'react';
import { useTheme } from './useTheme';
import type { GenerateThemeRequest } from '@dheme/sdk';
/**
* Hook para gerar temas com loading state local
*/
export function useGenerateTheme() {
const { generateTheme: generate } = useTheme();
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<Error | null>(null);
const generateTheme = useCallback(
async (params: GenerateThemeRequest) => {
setIsGenerating(true);
setError(null);
try {
await generate(params);
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error');
setError(error);
throw error;
} finally {
setIsGenerating(false);
}
},
[generate]
);
return {
generateTheme,
isGenerating,
error,
};
}Criar arquivo /packages/react/src/index.ts:
// Provider e Context
export { ThemeProvider } from './ThemeProvider';
export type { ThemeProviderProps } from './ThemeProvider';
export { ThemeContext } from './ThemeContext';
export type { ThemeContextValue } from './ThemeContext';
// Hooks
export { useTheme } from './hooks/useTheme';
export { useGenerateTheme } from './hooks/useGenerateTheme';
// Utilitários
export { applyThemeCSSVariables, removeThemeCSSVariables } from './utils/cssVariables';
export {
saveThemeToStorage,
loadThemeFromStorage,
removeThemeFromStorage,
} from './utils/localStorage';
// Re-exportar tipos da SDK base
export type {
GenerateThemeRequest,
GenerateThemeResponse,
ColorTokens,
HSLColor,
} from '@dheme/sdk';Criar arquivo /packages/sdk/examples/basic.ts:
import { DhemeClient } from '@dheme/sdk';
async function main() {
// Criar cliente
const client = new DhemeClient({
apiKey: 'dheme_xxxxxxxx_...',
});
try {
// Gerar tema
const { data, rateLimit } = await client.generateTheme({
theme: '#3b82f6',
radius: 0.5,
});
console.log('Theme generated successfully!');
console.log('Primary color (light):', data.colors.light.primary);
console.log('Rate limit remaining:', rateLimit.remaining);
} catch (error) {
console.error('Error:', error);
}
}
main();Criar arquivo /packages/sdk/examples/nodejs.ts:
import { DhemeClient, RateLimitError } from '@dheme/sdk';
async function main() {
const client = new DhemeClient({
apiKey: process.env.DHEME_API_KEY!,
retryConfig: {
maxRetries: 5,
initialDelay: 2000,
},
});
try {
// Gerar CSS
const css = await client.generateShadcnCSS({
theme: '#10b981',
secondaryColor: '#3b82f6',
});
console.log('CSS generated:');
console.log(css);
} catch (error) {
if (error instanceof RateLimitError) {
console.error('Rate limit exceeded!');
console.error('Limit:', error.limit);
console.error('Reset at:', error.resetAt);
} else {
console.error('Error:', error);
}
}
}
main();Criar arquivo /packages/react/examples/basic.tsx:
import React from 'react';
import { ThemeProvider, useGenerateTheme, useTheme } from '@dheme/react';
function App() {
return (
<ThemeProvider apiKey="dheme_xxxxxxxx_...">
<ThemeGenerator />
</ThemeProvider>
);
}
function ThemeGenerator() {
const { generateTheme, isGenerating } = useGenerateTheme();
const { theme } = useTheme();
const handleGenerate = async () => {
try {
await generateTheme({
theme: '#3b82f6',
radius: 0.5,
});
} catch (error) {
console.error('Failed to generate theme:', error);
}
};
return (
<div>
<button onClick={handleGenerate} disabled={isGenerating}>
{isGenerating ? 'Generating...' : 'Generate Theme'}
</button>
{theme && (
<div>
<h2>Theme Generated!</h2>
<p>Primary: {theme.colors.light.primary.h}°</p>
</div>
)}
</div>
);
}
export default App;Criar arquivo /packages/react/examples/nextjs.tsx:
// app/layout.tsx
import { ThemeProvider } from '@dheme/react';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<ThemeProvider
apiKey={process.env.NEXT_PUBLIC_DHEME_API_KEY!}
persistTheme={true}
autoApply={true}
mode="light"
>
{children}
</ThemeProvider>
</body>
</html>
);
}
// app/page.tsx
'use client';
import { useGenerateTheme, useTheme } from '@dheme/react';
export default function Home() {
const { generateTheme, isGenerating } = useGenerateTheme();
const { theme } = useTheme();
return (
<div className="p-8">
<h1 className="text-4xl font-bold mb-4">Theme Generator</h1>
<button
onClick={() => generateTheme({ theme: '#3b82f6' })}
disabled={isGenerating}
className="px-4 py-2 bg-primary text-primary-foreground rounded"
>
Generate Theme
</button>
{theme && (
<div className="mt-4 p-4 border rounded">
<p>Theme applied successfully!</p>
</div>
)}
</div>
);
}Criar arquivo /packages/sdk/src/utils/colorConversion.test.ts:
import { describe, it, expect } from 'vitest';
import { hexToHSL, hslToRGB, rgbToHex, isValidHex } from './colorConversion';
describe('colorConversion', () => {
describe('isValidHex', () => {
it('should validate correct HEX colors', () => {
expect(isValidHex('#3b82f6')).toBe(true);
expect(isValidHex('3b82f6')).toBe(true);
expect(isValidHex('#FFFFFF')).toBe(true);
});
it('should reject invalid HEX colors', () => {
expect(isValidHex('#123')).toBe(false);
expect(isValidHex('xyz123')).toBe(false);
expect(isValidHex('')).toBe(false);
});
});
describe('hexToHSL', () => {
it('should convert HEX to HSL correctly', () => {
const result = hexToHSL('#3b82f6');
expect(result.h).toBeCloseTo(221.2, 1);
expect(result.s).toBeCloseTo(83.2, 1);
expect(result.l).toBeCloseTo(53.3, 1);
});
});
describe('conversion round-trip', () => {
it('should convert HEX -> HSL -> RGB -> HEX correctly', () => {
const original = '#3b82f6';
const hsl = hexToHSL(original);
const rgb = hslToRGB(hsl);
const final = rgbToHex(rgb);
expect(final.toLowerCase()).toBe(original.toLowerCase());
});
});
});# Root do monorepo
npm install
# Build todos os pacotes
npm run build
# Dev mode (watch)
npm run dev
# Testes
npm run test
# Lint
npm run lint
# Typecheck
npm run typecheck# Publish SDK base
cd packages/sdk
npm publish --access public
# Publish React library
cd packages/react
npm publish --access public-
Setup Inicial
- Criar estrutura de diretórios
- Configurar npm workspaces
- Instalar dependências
-
Implementar SDK Base
- Seguir ordem: types → errors → constants → utils → middleware → client → index
- Escrever testes unitários
- Testar em diferentes ambientes
-
Implementar React Library
- Context → Provider → hooks → utils → index
- Escrever testes de componentes
- Testar com Next.js e Vite
-
Documentação
- README para cada pacote
- CHANGELOG
- Exemplos práticos
-
Publicação
- Configurar CI/CD
- Publicar no npm
- Criar releases no GitHub
- URL Base: Substituir
https://theme.dheme.compela URL real da API - TypeScript: Manter strict mode ativado
- Testes: Escrever testes para todas as funções utilitárias
- SSR: Garantir que código React é SSR-safe
- Tree-shaking: Usar named exports, evitar default exports
- Bundle size: Manter dependências mínimas
Este README contém toda a informação necessária para implementar a SDK Dheme completa. Siga a ordem sugerida de implementação e mantenha a estrutura de arquivos conforme especificado.