Request Request Design System

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" />