Exemplos
Componentes prontos a colar no teu projecto, estilo shadcn. Os tokens vêm do package; os componentes vivem no teu código.
Laravel Blade
Setup + componente Button anónimo + componente Input com error state.
1. Setup
// tailwind.config.js
const tokens = require('@request-labs/tokens/tailwind');
module.exports = {
content: [
'./resources/**/*.blade.php',
'./resources/**/*.js',
],
theme: {
extend: {
colors: tokens.colors,
spacing: tokens.spacing,
fontFamily: tokens.fontFamily,
fontSize: tokens.fontSize,
fontWeight: tokens.fontWeight,
lineHeight: tokens.lineHeight,
borderRadius: tokens.borderRadius,
boxShadow: tokens.boxShadow,
transitionDuration: tokens.transitionDuration,
transitionTimingFunction: tokens.transitionTimingFunction,
zIndex: tokens.zIndex,
},
},
};
// resources/css/app.css
@import "@request-labs/brand/fonts.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
html { font-family: 'Hanken Grotesk', 'Inter', ui-sans-serif, system-ui, sans-serif; } 2. Button (x-button)
{{-- resources/views/components/button.blade.php --}}
@props([
'variant' => 'primary',
'type' => 'button',
])
@php
$variants = [
'primary' => 'bg-rose-600 text-white hover:bg-rose-700',
'secondary' => 'bg-white border border-slate-300 text-slate-700 hover:bg-slate-50',
'ghost' => 'text-slate-700 hover:bg-slate-100',
];
$classes = 'inline-flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors duration-fast focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2 ' . $variants[$variant];
@endphp
<button type="{{ $type }}" {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</button>
{{-- Uso --}}
<x-button variant="primary">Guardar</x-button>
<x-button variant="secondary">Cancelar</x-button> 3. Input (x-input)
{{-- resources/views/components/input.blade.php --}}
@props([
'label' => null,
'required' => false,
'error' => null,
'help' => null,
])
<div>
@if($label)
<label for="{{ $attributes->get('id') }}" class="block text-sm font-medium text-slate-700 mb-1">
{{ $label }}
@if($required)<span class="text-rose-600">*</span>@endif
</label>
@endif
<input
{{ $attributes->merge([
'class' => 'w-full px-3 py-2 rounded-md border text-base focus:outline-none focus:ring-2 focus:ring-rose-500 ' .
($error ? 'border-red-300 focus:border-red-500' : 'border-slate-300 focus:border-rose-500'),
'aria-invalid' => $error ? 'true' : 'false',
]) }}
/>
@if($error)
<p class="mt-1 text-xs text-red-600">{{ $error }}</p>
@elseif($help)
<p class="mt-1 text-xs text-slate-500">{{ $help }}</p>
@endif
</div>
{{-- Uso --}}
<x-input id="email" name="email" label="Email" type="email" required help="Usado para login." /> React
TypeScript + function components. Testado com Vite 5 e Tailwind 3.
1. Setup
// tailwind.config.js
import tokens from '@request-labs/tokens/tailwind';
export default {
content: ['./src/**/*.{ts,tsx}', './index.html'],
theme: {
extend: {
colors: tokens.colors,
spacing: tokens.spacing,
fontFamily: tokens.fontFamily,
fontSize: tokens.fontSize,
fontWeight: tokens.fontWeight,
lineHeight: tokens.lineHeight,
borderRadius: tokens.borderRadius,
boxShadow: tokens.boxShadow,
transitionDuration: tokens.transitionDuration,
transitionTimingFunction: tokens.transitionTimingFunction,
zIndex: tokens.zIndex,
},
},
};
// src/index.css
@import "@request-labs/brand/fonts.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
html { font-family: 'Hanken Grotesk', 'Inter', ui-sans-serif, system-ui, sans-serif; } 2. Button
// src/components/Button.tsx
import { ButtonHTMLAttributes, ReactNode } from 'react';
type Variant = 'primary' | 'secondary' | 'ghost';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
children: ReactNode;
}
const variants: Record<Variant, string> = {
primary: 'bg-rose-600 text-white hover:bg-rose-700',
secondary: 'bg-white border border-slate-300 text-slate-700 hover:bg-slate-50',
ghost: 'text-slate-700 hover:bg-slate-100',
};
const base = 'inline-flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors duration-fast focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2';
export function Button({ variant = 'primary', className = '', children, ...rest }: ButtonProps) {
return (
<button className={`${base} ${variants[variant]} ${className}`} {...rest}>
{children}
</button>
);
}
// Uso
<Button variant="primary">Guardar</Button>
<Button variant="secondary" onClick={() => {}}>Cancelar</Button> 3. Input (forwardRef + a11y)
// src/components/Input.tsx
import { InputHTMLAttributes, forwardRef } from 'react';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
required?: boolean;
error?: string;
help?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ label, required, error, help, id, className = '', ...rest }, ref,
) {
const errId = error ? `${id}-err` : undefined;
return (
<div>
{label && (
<label htmlFor={id} className="block text-sm font-medium text-slate-700 mb-1">
{label}
{required && <span className="text-rose-600"> *</span>}
</label>
)}
<input
ref={ref}
id={id}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={errId}
className={`w-full px-3 py-2 rounded-md border text-base focus:outline-none focus:ring-2 ${error ? 'border-red-300 focus:border-red-500 focus:ring-red-500' : 'border-slate-300 focus:border-rose-500 focus:ring-rose-500'} ${className}`}
{...rest}
/>
{error ? (
<p id={errId} className="mt-1 text-xs text-red-600">{error}</p>
) : help ? (
<p className="mt-1 text-xs text-slate-500">{help}</p>
) : null}
</div>
);
});
// Uso
<Input id="email" name="email" type="email" label="Email" required help="Usado para login." /> Vue
Vue 3 Composition API + <script setup>.
1. Setup
// tailwind.config.js
const tokens = require('@request-labs/tokens/tailwind');
module.exports = {
content: ['./src/**/*.{vue,js,ts}', './index.html'],
theme: { extend: { ...tokens } },
};
// src/main.css
@import "@request-labs/brand/fonts.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
html { font-family: 'Hanken Grotesk', 'Inter', ui-sans-serif, system-ui, sans-serif; } 2. Button (RButton)
<!-- src/components/RButton.vue -->
<script setup lang="ts">
type Variant = 'primary' | 'secondary' | 'ghost';
withDefaults(defineProps<{ variant?: Variant }>(), { variant: 'primary' });
const variants: Record<Variant, string> = {
primary: 'bg-rose-600 text-white hover:bg-rose-700',
secondary: 'bg-white border border-slate-300 text-slate-700 hover:bg-slate-50',
ghost: 'text-slate-700 hover:bg-slate-100',
};
</script>
<template>
<button
:class="[
'inline-flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors duration-fast focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2',
variants[variant],
]"
>
<slot />
</button>
</template>
<!-- Uso -->
<RButton variant="primary">Guardar</RButton>
<RButton variant="secondary">Cancelar</RButton> 3. Input com v-model
<!-- src/components/RInput.vue -->
<script setup lang="ts">
defineProps<{
id: string;
label?: string;
required?: boolean;
error?: string;
help?: string;
modelValue?: string;
}>();
defineEmits<{ (e: 'update:modelValue', v: string): void }>();
</script>
<template>
<div>
<label v-if="label" :for="id" class="block text-sm font-medium text-slate-700 mb-1">
{{ label }}<span v-if="required" class="text-rose-600"> *</span>
</label>
<input
:id="id"
:value="modelValue"
:aria-invalid="!!error"
:aria-describedby="error ? id + '-err' : undefined"
:class="[
'w-full px-3 py-2 rounded-md border text-base focus:outline-none focus:ring-2',
error
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-slate-300 focus:border-rose-500 focus:ring-rose-500',
]"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
<p v-if="error" :id="id + '-err'" class="mt-1 text-xs text-red-600">{{ error }}</p>
<p v-else-if="help" class="mt-1 text-xs text-slate-500">{{ help }}</p>
</div>
</template>
<!-- Uso -->
<RInput id="email" label="Email" type="email" required help="Usado para login." v-model="email" />