feat: enhance input showcase with new input components and status handling

This commit is contained in:
Firman Ramdhani
2026-01-26 12:23:26 +07:00
parent d4114702a0
commit 3c11b98153
8 changed files with 396 additions and 138 deletions
@@ -1,126 +1,193 @@
import { Input, InputNumber, InputStatusType, TextArea } from '@repo/ui'; import { Input, InputPassword, InputNumber, InputCurrency, InputStatusType, TextArea } from '@repo/ui';
// Helper untuk label kolom
const Label = ({ children }: { children: React.ReactNode }) => (
<div className="text-xs font-bold text-gray-400 uppercase mb-3 tracking-wider">{children}</div>
);
// Helper untuk prefix/suffix visual
const Fix = ({ children }: { children: React.ReactNode }) => <span className="text-gray-400 text-sm">{children}</span>;
export default function GroupComponent({ status }: { status?: InputStatusType }) { export default function GroupComponent({ status }: { status?: InputStatusType }) {
return ( return (
<div> <div className="space-y-12 p-8 rounded-2xl">
<div className="space-y-4 bg-white p-5 rounded-xl border border-gray-200 shadow-xs"> {/* --- SECTION 1: OUTLINED VARIANT (DEFAULT) --- */}
<h3 className="text-xs font-bold text-gray-400 uppercase">Default</h3> <section className="space-y-6">
<div className="grid grid-cols-4 gap-4 "> <h2 className="text-lg font-bold text-gray-800 border-b pb-2">Outlined Variant (Standard)</h2>
<div className="space-y-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="text-gray-500 ">Default</div> <div className="space-y-3">
<div className="space-y-2"> <Label>Basic</Label>
<Input status={status} placeholder="Outlined Placeholder" /> <Input status={status} placeholder="Input text..." />
<InputNumber status={status} className="w-full" placeholder="OutlinedNumber Placeholder" /> <InputPassword status={status} placeholder="Password..." />
<TextArea status={status} placeholder="Outlined TextArea Placeholder" rows={2} /> <InputNumber status={status} className="w-full" placeholder="Number..." />
<Input status={status} variant="filled" placeholder="Filled Placeholder" /> <InputCurrency status={status} className="w-full" placeholder="Currency..." />
<TextArea status={status} placeholder="TextArea..." rows={2} />
</div>
<div className="space-y-3">
<Label>With Prefix</Label>
<Input status={status} prefix={<Fix>@</Fix>} placeholder="username" />
<InputPassword status={status} prefix={<Fix>🔒</Fix>} placeholder="password" />
<InputNumber status={status} prefix={<Fix>#</Fix>} className="w-full" placeholder="id" />
<InputCurrency status={status} prefix="Rp " className="w-full" placeholder="price" />
</div>
<div className="space-y-3">
<Label>With Suffix</Label>
<Input status={status} suffix={<Fix>.com</Fix>} placeholder="website" />
<InputPassword status={status} suffix={<Fix>Required</Fix>} placeholder="password" />
<InputNumber status={status} suffix={<Fix>kg</Fix>} className="w-full" placeholder="weight" />
<InputCurrency status={status} suffix={<Fix>nett</Fix>} className="w-full" placeholder="total" />
</div>
<div className="space-y-3">
<Label>Both & Special</Label>
<Input status={status} prefix={<Fix>https://</Fix>} suffix={<Fix>.io</Fix>} placeholder="domain" />
{/* Password Tanpa Toggle */}
<InputPassword
status={status}
visibilityToggle={false}
prefix={<Fix>🔑</Fix>}
placeholder="Hidden Password"
/>
<InputNumber <InputNumber
status={status} status={status}
variant="filled" prefix={<Fix>+</Fix>}
suffix={<Fix>%</Fix>}
className="w-full" className="w-full"
placeholder="Filled Number Placeholder" placeholder="tax"
/>
<InputCurrency
status={status}
prefix="USD "
suffix={<Fix>cents</Fix>}
className="w-full"
placeholder="amount"
/> />
<TextArea status={status} variant="filled" placeholder="Filled TextArea Placeholder" rows={2} />
</div> </div>
</div> </div>
<div className="space-y-4"> </section>
<div className="text-gray-500 ">With Prefix</div>
<div className="space-y-2"> {/* --- SECTION 2: FILLED VARIANT --- */}
<section className="space-y-6">
<h2 className="text-lg font-bold text-gray-800 border-b pb-2">Filled Variant</h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="space-y-3">
<Label>Basic Filled</Label>
<Input variant="filled" status={status} placeholder="Filled Input" />
<InputPassword variant="filled" status={status} placeholder="Filled Password" />
<InputNumber variant="filled" status={status} className="w-full" placeholder="Filled Number" />
<InputCurrency variant="filled" status={status} className="w-full" placeholder="Filled Currency" />
<TextArea variant="filled" status={status} placeholder="Filled TextArea" rows={2} />
</div>
<div className="space-y-3">
<Label>Filled + Prefix</Label>
<Input variant="filled" status={status} prefix={<Fix>ID</Fix>} placeholder="12345" />
<InputPassword variant="filled" status={status} prefix={<Fix>🔑</Fix>} placeholder="password" />
<InputNumber variant="filled" status={status} prefix={<Fix>Qty:</Fix>} className="w-full" />
<InputCurrency variant="filled" status={status} prefix="Rp " className="w-full" />
</div>
<div className="space-y-3">
<Label>Filled + Suffix</Label>
<Input variant="filled" status={status} suffix={<Fix>Optional</Fix>} placeholder="Full Name" />
<InputPassword variant="filled" status={status} suffix={<Fix>?</Fix>} placeholder="password" />
<InputNumber variant="filled" status={status} suffix={<Fix>pts</Fix>} className="w-full" />
<InputCurrency variant="filled" status={status} suffix={<Fix>tax-incl</Fix>} className="w-full" />
</div>
<div className="space-y-3">
<Label>Both & Special</Label>
<Input <Input
variant="filled"
status={status} status={status}
placeholder="Outlined Placeholder" prefix={<Fix>https://</Fix>}
prefix={<span className="text-gray-500">$</span>} suffix={<Fix>.io</Fix>}
placeholder="domain"
/>
{/* Password Tanpa Toggle */}
<InputPassword
variant="filled"
status={status}
visibilityToggle={false}
prefix={<Fix>🔑</Fix>}
placeholder="Hidden Password"
/> />
<InputNumber <InputNumber
status={status}
className="w-full"
placeholder="OutlinedNumber Placeholder"
prefix={<span className="text-gray-500">$</span>}
/>
<TextArea status={status} placeholder="Outlined TextArea Placeholder" rows={2} />
<Input
status={status}
variant="filled" variant="filled"
placeholder="Filled Placeholder"
prefix={<span className="text-gray-500">$</span>}
/>
<InputNumber
status={status} status={status}
variant="filled" prefix={<Fix>+</Fix>}
suffix={<Fix>%</Fix>}
className="w-full" className="w-full"
placeholder="Filled Number Placeholder" placeholder="tax"
prefix={<span className="text-gray-500">$</span>}
/> />
<TextArea status={status} variant="filled" placeholder="Filled TextArea Placeholder" rows={2} /> <InputCurrency
</div>
</div>
<div className="space-y-4">
<div className="text-gray-500 ">With Suffix</div>
<div className="space-y-2">
<Input
status={status}
placeholder="Outlined Placeholder"
suffix={<span className="text-gray-500">$</span>}
/>
<InputNumber
status={status}
className="w-full"
placeholder="OutlinedNumber Placeholder"
suffix={<span className="text-gray-500">$</span>}
/>
<TextArea status={status} placeholder="Outlined TextArea Placeholder" rows={2} />
<Input
status={status}
variant="filled" variant="filled"
placeholder="Filled Placeholder"
suffix={<span className="text-gray-500">$</span>}
/>
<InputNumber
status={status} status={status}
variant="filled" prefix="USD "
suffix={<Fix>cents</Fix>}
className="w-full" className="w-full"
placeholder="Filled Number Placeholder" placeholder="amount"
suffix={<span className="text-gray-500">$</span>}
/> />
<TextArea status={status} variant="filled" placeholder="Filled TextArea Placeholder" rows={2} />
</div> </div>
</div> </div>
<div className="space-y-4"> </section>
<div className="text-gray-500 ">With Prefix and Suffix</div>
<div className="space-y-2"> {/* --- SECTION 3: Component States & Validation --- */}
<Input
status={status} <section className="space-y-6">
placeholder="Outlined Placeholder" <h2 className="text-lg font-bold text-gray-800 border-b pb-2">Component States & Validation</h2>
prefix={<span className="text-gray-500">$</span>}
suffix={<span className="text-gray-500">$</span>} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
/> {/* COLUMN 1: DISABLED (Semua Component) */}
<InputNumber <div className="space-y-3">
status={status} <Label>Disabled State</Label>
className="w-full" <div className="p-4 bg-gray-50 rounded-lg space-y-3 border border-dashed border-gray-200">
placeholder="OutlinedNumber Placeholder" <Input disabled placeholder="Disabled Input" prefix={<Fix>🚫</Fix>} />
prefix={<span className="text-gray-500">$</span>} <InputPassword disabled placeholder="Disabled Password" />
suffix={<span className="text-gray-500">$</span>} <InputNumber disabled className="w-full" placeholder="0" />
/> <InputCurrency disabled className="w-full" placeholder="Rp 0" />
<TextArea status={status} placeholder="Outlined TextArea Placeholder" rows={2} /> <TextArea disabled placeholder="Disabled TextArea" rows={2} />
<Input
status={status}
variant="filled"
placeholder="Filled Placeholder"
prefix={<span className="text-gray-500">$</span>}
suffix={<span className="text-gray-500">$</span>}
/>
<InputNumber
status={status}
variant="filled"
className="w-full"
placeholder="Filled Number Placeholder"
prefix={<span className="text-gray-500">$</span>}
suffix={<span className="text-gray-500">$</span>}
/>
<TextArea status={status} variant="filled" placeholder="Filled TextArea Placeholder" rows={2} />
</div> </div>
</div> </div>
{/* COLUMN 2: READ ONLY (Semua Component) */}
<div className="space-y-3">
<Label>Read Only State</Label>
<div className="p-4 bg-gray-50 rounded-lg space-y-3 border border-dashed border-gray-200">
<Input readOnly value="Read only text" prefix={<Fix>📖</Fix>} />
<InputPassword readOnly value="secretpassword" />
<InputNumber readOnly className="w-full" value={12345} />
<InputCurrency readOnly className="w-full" value={50000} />
<TextArea readOnly value="This content is read only and cannot be edited." rows={2} />
</div>
</div>
{/* COLUMN 3: ERROR OUTLINED */}
<div className="space-y-3">
<Label>Error (Outlined)</Label>
<div className="space-y-3">
<Input status="error" placeholder="Invalid input" />
<InputPassword status="error" placeholder="Wrong password" />
<InputNumber status="error" className="w-full" placeholder="Out of range" />
<InputCurrency status="error" className="w-full" placeholder="Invalid amount" />
<TextArea status="error" placeholder="Error in message" rows={2} />
</div>
</div>
{/* COLUMN 4: ERROR FILLED */}
<div className="space-y-3">
<Label>Error (Filled)</Label>
<div className="space-y-3">
<Input variant="filled" status="error" placeholder="Invalid input" />
<InputPassword variant="filled" status="error" placeholder="Wrong password" />
<InputNumber variant="filled" status="error" className="w-full" placeholder="Out of range" />
<InputCurrency variant="filled" status="error" className="w-full" placeholder="Invalid amount" />
<TextArea variant="filled" status="error" placeholder="Error in message" rows={2} />
</div>
</div> </div>
</div> </div>
</section>
</div> </div>
); );
} }
@@ -1,28 +1,69 @@
import { useState } from 'react';
import GroupComponent from './group-component'; import GroupComponent from './group-component';
import { InputStatusType } from '@repo/ui';
export default function InputShowCase() { export default function InputShowCase() {
// State untuk menyimpan status yang dipilih
const [selectedStatus, setSelectedStatus] = useState<InputStatusType>('normal');
const statuses: { label: string; value: InputStatusType; color: string }[] = [
{ label: 'Normal', value: 'normal', color: 'bg-gray-500' },
{ label: 'Error', value: 'error', color: 'bg-red-500' },
{ label: 'Warning', value: 'warning', color: 'bg-amber-500' },
{ label: 'Success', value: 'success', color: 'bg-emerald-500' },
{ label: 'Info', value: 'info', color: 'bg-blue-500' },
];
return ( return (
<div className="min-h-screen "> <div className="min-h-screen bg-gray-50/50">
<main className="mx-auto p-8 space-y-12"> <main className="mx-auto p-8 space-y-8">
<section className="space-y-6"> {/* HEADER & SELECTOR */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white p-6 rounded-2xl border border-gray-200 shadow-sm">
<div className="space-y-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2 className="text-lg font-bold">1. All Conditions Matrix</h2> <h2 className="text-xl font-bold text-gray-800">Input Components</h2>
<span className="px-2 py-0.5 text-[10px] bg-gray-200 text-gray-600 rounded uppercase tracking-wider"> <span className="px-2 py-0.5 text-[10px] bg-blue-100 text-blue-600 rounded-full uppercase tracking-wider font-bold">
Visual Audit Interactive Lab
</span> </span>
</div> </div>
<p className="text-sm text-gray-500">
Pilih status untuk melihat perubahan validasi pada semua tipe input.
</p>
</div>
<div className="grid grid-cols-1 gap-6"> {/* TAB SELECTOR */}
{/* COLUMN: ERROR */} <div className="flex bg-gray-100 p-1 rounded-xl w-fit border border-gray-200">
<GroupComponent status="error" /> {statuses.map((item) => (
{/* COLUMN: NORMAL */} <button
<GroupComponent /> key={item.value}
{/* COLUMN: INFO */} onClick={() => setSelectedStatus(item.value)}
<GroupComponent status="info" /> className={`
{/* COLUMN: WARNING */} px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200
<GroupComponent status="warning" /> ${
{/* COLUMN: SUCCESS */} selectedStatus === item.value
<GroupComponent status="success" /> ? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}
`}
>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${item.color}`} />
{item.label}
</div>
</button>
))}
</div>
</div>
{/* RENDER COMPONENT */}
<section className="animate-in fade-in slide-in-from-bottom-2 duration-500">
<div className="relative">
{/* Label Indikator Status Aktif */}
<div className="absolute -top-3 left-8 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest bg-white border border-gray-200 shadow-sm z-10">
Active Status: <span className="text-blue-600">{selectedStatus}</span>
</div>
<GroupComponent status={selectedStatus} />
</div> </div>
</section> </section>
</main> </main>
@@ -3,6 +3,7 @@ export * from './form-provider';
export * from './input/input.component'; export * from './input/input.component';
export * from './input/input-password.component'; export * from './input/input-password.component';
export * from './input/input-currency.component';
export * from './input/input-number.component'; export * from './input/input-number.component';
export * from './textarea/textarea.component'; export * from './textarea/textarea.component';
@@ -0,0 +1,41 @@
import { InputCurrencyProps } from '../types';
import { InputNumber } from './input-number.component';
export const InputCurrency = (props: InputCurrencyProps) => {
const { prefix = 'Rp ', ...restProps } = props;
// Change format from 10000 to "Rp 10.000"
const formatCurrency = (value: number | string | undefined) => {
if (value === undefined || value === null || value === '') {
return '';
}
// Convert value to string for processing
const valStr = `${value}`;
const formatted = valStr.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
return `${prefix}${formatted}`;
};
// Change parse from "Rp 10.000" to 10000
const parseCurrency = (displayValue: string | undefined) => {
if (!displayValue) return '';
// Remove prefix and all non-digit characters (except comma if decimal support is needed)
// Here we assume pure integer input, so dots (.) are removed.
const cleanValue = displayValue.replace(prefix, '').replace(/\./g, '');
// If you need decimal support (comma), use this regex instead:
// return displayValue.replace(prefix, '').replace(/\./g, '').replace(/,/g, '.');
return cleanValue;
};
return (
<InputNumber
{...restProps}
formatter={formatCurrency}
parser={parseCurrency}
// Ensure minimum value is 0 for currency input
min={props.min !== undefined ? props.min : 0}
/>
);
};
@@ -1,6 +1,101 @@
import React, { useState } from 'react';
import { InputPasswordProps } from '../types'; import { InputPasswordProps } from '../types';
import { Input } from './input.component'; import { Input } from './input.component';
/**
* Eye Icon for visibility toggle
* @returns JSX Element
*/
const EyeIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
width="1em"
height="1em"
className="w-4 h-4"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
/**
* Eye Off Icon for visibility toggle
* @returns JSX Element
*/
const EyeOffIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
width="1em"
height="1em"
className="w-4 h-4"
>
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
<line x1="1" y1="1" x2="23" y2="23" />
</svg>
);
/**
* InputPassword Component
* @param props InputPasswordProps
* @returns JSX Element
*/
export const InputPassword = (props: InputPasswordProps) => { export const InputPassword = (props: InputPasswordProps) => {
return <Input {...props} type="password" />; // Set default props and state
const { suffix, disabled, visibilityToggle = true, ...restProps } = props;
const [visible, setVisible] = useState(false);
const toggleVisibility = () => {
if (disabled) return;
setVisible(!visible);
};
// If no suffix and no visibility toggle, render simple password input
if (!suffix && !visibilityToggle) {
return <Input {...restProps} disabled={disabled} type="password" />;
}
const combinedSuffix = (
<div className="flex items-center gap-2">
{/* Suffix User */}
{suffix && <span className="text-gray-400 select-none">{suffix}</span>}
{/* Visibility Toggle Button */}
{visibilityToggle && (
<button
type="button"
onClick={toggleVisibility}
disabled={disabled}
className={`
flex items-center justify-center
focus:outline-none transition-colors
${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer text-gray-400 hover:text-gray-600'}
`}
>
{visible ? <EyeIcon /> : <EyeOffIcon />}
</button>
)}
</div>
);
return (
<Input
{...restProps}
disabled={disabled}
// If toggle is disabled, force type to 'password', ignore visible state
type={visibilityToggle && visible ? 'text' : 'password'}
suffix={combinedSuffix}
/>
);
}; };
@@ -163,6 +163,17 @@
border-color: var(--color-success-500) !important; border-color: var(--color-success-500) !important;
} }
/* Info (Outlined) */
.input-number-variant-outlined.input-number-status-info {
border-color: var(--color-info-500) !important;
}
.input-number-variant-outlined.input-number-status-info:focus-within,
.input-number-variant-outlined.input-number-status-info.rc-input-number-focused,
.input-number-variant-outlined.input-number-status-info.rc-input-number-affix-wrapper-focused {
box-shadow: 0 0 0 3px var(--color-info-100) !important;
border-color: var(--color-info-500) !important;
}
/* ==================== /* ====================
VARIANT: FILLED VARIANT: FILLED
==================== */ ==================== */
@@ -43,12 +43,13 @@ interface BaseComponentProps {
* Define specific component props * Define specific component props
*/ */
export interface InputProps extends RcInputProps, BaseComponentProps {} export interface InputProps extends RcInputProps, BaseComponentProps {}
export interface InputPasswordProps extends InputProps {
visibilityToggle?: boolean; // Default: true
}
/**
* Extend specific component props from base props
* This ensures consistency across different input components
* and allows for shared properties like 'status'
*/
export interface InputPasswordProps extends InputProps {}
export interface InputNumberProps extends RcInputNumberProps, BaseComponentProps {} export interface InputNumberProps extends RcInputNumberProps, BaseComponentProps {}
export interface InputCurrencyProps extends InputNumberProps, BaseComponentProps {
prefix?: string; // Optional: Custom prefix, default 'Rp '
}
export interface TextAreaProps extends RcTextAreaProps, BaseComponentProps {} export interface TextAreaProps extends RcTextAreaProps, BaseComponentProps {}
+2 -1
View File
@@ -11,7 +11,8 @@
@layer base { @layer base {
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--color-gray-50); /* background-color: var(--color-gray-50); */
background-color: #ffffff;
color: var(--color-gray-800); color: var(--color-gray-800);
@apply text-base; /* Set global base font to 13px */ @apply text-base; /* Set global base font to 13px */
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;