=== NEW === - studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan) - docs/FACTORY-V2.md — Factory v2 architecture doc - docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report === UPDATED SERVERS === - fieldedge: Added jobs-tools, UI build script, main entry update - lightspeed: Updated main + server entry points - squarespace: Added collection-browser + page-manager apps - toast: Added main + server entry points === INFRA === - infra/command-center/state.json — Updated pipeline state - infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
111 lines
3.0 KiB
TypeScript
111 lines
3.0 KiB
TypeScript
'use client';
|
|
import React from 'react';
|
|
import { clsx } from 'clsx';
|
|
import { twMerge } from 'tailwind-merge';
|
|
|
|
export type InputVariant = 'default' | 'search';
|
|
|
|
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
variant?: InputVariant;
|
|
label?: string;
|
|
error?: string;
|
|
helperText?: string;
|
|
className?: string;
|
|
wrapperClassName?: string;
|
|
}
|
|
|
|
const SearchIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
<svg
|
|
className={twMerge('h-4 w-4 text-gray-500', className)}
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
strokeWidth={2}
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
|
/>
|
|
</svg>
|
|
);
|
|
|
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
(
|
|
{
|
|
variant = 'default',
|
|
label,
|
|
error,
|
|
helperText,
|
|
className,
|
|
wrapperClassName,
|
|
id,
|
|
...props
|
|
},
|
|
ref,
|
|
) => {
|
|
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-');
|
|
const hasError = Boolean(error);
|
|
|
|
return (
|
|
<div className={twMerge('flex flex-col gap-1.5', wrapperClassName)}>
|
|
{label && (
|
|
<label
|
|
htmlFor={inputId}
|
|
className="text-sm font-medium text-gray-300"
|
|
>
|
|
{label}
|
|
</label>
|
|
)}
|
|
|
|
<div className="relative">
|
|
{variant === 'search' && (
|
|
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
|
<SearchIcon />
|
|
</div>
|
|
)}
|
|
|
|
<input
|
|
ref={ref}
|
|
id={inputId}
|
|
className={twMerge(
|
|
clsx(
|
|
'w-full rounded-lg px-3 py-2 text-sm',
|
|
'bg-gray-800 text-gray-100 placeholder-gray-500',
|
|
'border transition-colors duration-150',
|
|
'focus:outline-none focus:ring-2 focus:ring-offset-0',
|
|
hasError
|
|
? 'border-red-500 focus:border-red-500 focus:ring-red-500/30'
|
|
: 'border-gray-600 focus:border-indigo-500 focus:ring-indigo-500/30',
|
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
variant === 'search' && 'pl-9',
|
|
),
|
|
className,
|
|
)}
|
|
aria-invalid={hasError}
|
|
aria-describedby={
|
|
hasError ? `${inputId}-error` : helperText ? `${inputId}-helper` : undefined
|
|
}
|
|
{...props}
|
|
/>
|
|
</div>
|
|
|
|
{hasError && (
|
|
<p id={`${inputId}-error`} className="text-xs text-red-400" role="alert">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
{!hasError && helperText && (
|
|
<p id={`${inputId}-helper`} className="text-xs text-gray-500">
|
|
{helperText}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
Input.displayName = 'Input';
|