Jake Shore 96e52666c5 MCPEngine full sync — studio scaffold, factory v2, server updates, state.json — 2026-02-12
=== 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
2026-02-12 17:58:33 -05:00

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';