refactor(ui): replace inline INPUT_CLS/LABEL_CLS/BTN_DANGER constants and action link classes with CSS component classes
Remove duplicated Tailwind class string constants from 15 component files. Use app-input, app-select, app-label, app-action-danger-btn, and app-action-delete CSS component classes from globals.css instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -151,7 +151,7 @@ export function ProjectAssignmentsTable({ assignments }: ProjectAssignmentsTable
|
||||
type="button"
|
||||
onClick={() => void handleDelete(assignment.id)}
|
||||
disabled={deletingId === assignment.id}
|
||||
className="text-xs font-medium text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-200 disabled:opacity-50"
|
||||
className="app-action-delete disabled:opacity-50"
|
||||
>
|
||||
{deletingId === assignment.id ? "Deleting..." : "Confirm"}
|
||||
</button>
|
||||
@@ -168,7 +168,7 @@ export function ProjectAssignmentsTable({ assignments }: ProjectAssignmentsTable
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmId(assignment.id)}
|
||||
className="text-xs font-medium text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-200"
|
||||
className="app-action-delete"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
@@ -30,18 +30,12 @@ const ALLOCATION_TYPE_OPTIONS = [
|
||||
{ value: "EXT", label: "EXT" },
|
||||
] as const;
|
||||
|
||||
const INPUT_CLS = "app-input";
|
||||
const SELECT_CLS = "app-select w-full";
|
||||
const LABEL_CLS = "app-label";
|
||||
|
||||
const BTN_PRIMARY =
|
||||
"px-5 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50 transition-colors";
|
||||
|
||||
const BTN_SECONDARY =
|
||||
"px-5 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium transition-colors";
|
||||
|
||||
const BTN_DANGER = "app-action-danger-btn";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Assignment {
|
||||
@@ -176,7 +170,7 @@ function DynamicFieldInput({
|
||||
onChange={(e) => onChange(field.key, e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
rows={3}
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
);
|
||||
case FieldType.NUMBER:
|
||||
@@ -190,7 +184,7 @@ function DynamicFieldInput({
|
||||
onChange(field.key, e.target.value === "" ? "" : parseFloat(e.target.value))
|
||||
}
|
||||
placeholder={field.placeholder}
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
);
|
||||
case FieldType.BOOLEAN:
|
||||
@@ -217,7 +211,7 @@ function DynamicFieldInput({
|
||||
<select
|
||||
value={strVal}
|
||||
onChange={(e) => onChange(field.key, e.target.value)}
|
||||
className={SELECT_CLS}
|
||||
className="app-select w-full"
|
||||
>
|
||||
<option value="">— select —</option>
|
||||
{(field.options ?? []).map((opt) => (
|
||||
@@ -259,7 +253,7 @@ function DynamicFieldInput({
|
||||
value={strVal}
|
||||
onChange={(e) => onChange(field.key, e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -318,7 +312,7 @@ function Step1({ state, onChange }: Step1Props) {
|
||||
<div className="space-y-5">
|
||||
{/* Blueprint picker */}
|
||||
<div>
|
||||
<label className={LABEL_CLS}>Project Blueprint (optional)<InfoTooltip content="Blueprints are templates that pre-fill role presets and default settings. Selecting one loads staffing requirements into Step 3." /></label>
|
||||
<label className="app-label">Project Blueprint (optional)<InfoTooltip content="Blueprints are templates that pre-fill role presets and default settings. Selecting one loads staffing requirements into Step 3." /></label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mb-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -370,36 +364,36 @@ function Step1({ state, onChange }: Step1Props) {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Short code */}
|
||||
<div>
|
||||
<label className={LABEL_CLS}>Short Code *<InfoTooltip content="Unique chargecode for this project, used for time tracking and cost attribution. Must be uppercase alphanumeric." /></label>
|
||||
<label className="app-label">Short Code *<InfoTooltip content="Unique chargecode for this project, used for time tracking and cost attribution. Must be uppercase alphanumeric." /></label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.shortCode}
|
||||
onChange={(e) => onChange({ shortCode: e.target.value.toUpperCase() })}
|
||||
placeholder="e.g. BMW26D"
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-0.5">Uppercase alphanumeric, max 20 chars</p>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className={LABEL_CLS}>Project Name *<InfoTooltip content="Display name shown on the timeline and in reports." /></label>
|
||||
<label className="app-label">Project Name *<InfoTooltip content="Display name shown on the timeline and in reports." /></label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.name}
|
||||
onChange={(e) => onChange({ name: e.target.value })}
|
||||
placeholder="e.g. BMW X3 Campaign"
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Order type */}
|
||||
<div>
|
||||
<label className={LABEL_CLS}>Order Type *<InfoTooltip content="BD = Business Development (pre-sales), CHARGEABLE = billable client work, INTERNAL = internal project, OVERHEAD = non-project overhead." /></label>
|
||||
<label className="app-label">Order Type *<InfoTooltip content="BD = Business Development (pre-sales), CHARGEABLE = billable client work, INTERNAL = internal project, OVERHEAD = non-project overhead." /></label>
|
||||
<select
|
||||
value={state.orderType}
|
||||
onChange={(e) => onChange({ orderType: e.target.value })}
|
||||
className={SELECT_CLS}
|
||||
className="app-select w-full"
|
||||
>
|
||||
{ORDER_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
@@ -411,11 +405,11 @@ function Step1({ state, onChange }: Step1Props) {
|
||||
|
||||
{/* Allocation type */}
|
||||
<div>
|
||||
<label className={LABEL_CLS}>Allocation Type *<InfoTooltip content="INT = staffed with internal resources, EXT = staffed with external contractors or freelancers." /></label>
|
||||
<label className="app-label">Allocation Type *<InfoTooltip content="INT = staffed with internal resources, EXT = staffed with external contractors or freelancers." /></label>
|
||||
<select
|
||||
value={state.allocationType}
|
||||
onChange={(e) => onChange({ allocationType: e.target.value })}
|
||||
className={SELECT_CLS}
|
||||
className="app-select w-full"
|
||||
>
|
||||
{ALLOCATION_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
@@ -437,7 +431,7 @@ function Step1({ state, onChange }: Step1Props) {
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((field) => (
|
||||
<div key={field.key} className={field.type === FieldType.TEXTAREA ? "col-span-2" : ""}>
|
||||
<label className={LABEL_CLS}>
|
||||
<label className="app-label">
|
||||
{field.label}
|
||||
{field.required && " *"}
|
||||
{field.description && (
|
||||
@@ -517,7 +511,7 @@ function ResourcePersonPicker({ value, onChange }: { value: string; onChange: (v
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
placeholder="Search by name or EID…"
|
||||
className={`${INPUT_CLS} ${isConfirmed ? "ring-2 ring-green-400 ring-offset-0" : ""}`}
|
||||
className={`app-input ${isConfirmed ? "ring-2 ring-green-400 ring-offset-0" : ""}`}
|
||||
/>
|
||||
{isConfirmed && (
|
||||
<span className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-green-500">
|
||||
@@ -577,14 +571,14 @@ function Step2({ state, onChange }: Step2Props) {
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={LABEL_CLS}>Start Date *<InfoTooltip content="First day of the project period. Assignments and budget tracking begin from this date." /></label>
|
||||
<label className="app-label">Start Date *<InfoTooltip content="First day of the project period. Assignments and budget tracking begin from this date." /></label>
|
||||
<DateInput
|
||||
value={state.startDate}
|
||||
onChange={(v) => onChange({ startDate: v })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={LABEL_CLS}>End Date *<InfoTooltip content="Last day of the project period. Defines the timeline boundary for all assignments." /></label>
|
||||
<label className="app-label">End Date *<InfoTooltip content="Last day of the project period. Defines the timeline boundary for all assignments." /></label>
|
||||
<DateInput
|
||||
value={state.endDate}
|
||||
onChange={(v) => onChange({ endDate: v })}
|
||||
@@ -594,7 +588,7 @@ function Step2({ state, onChange }: Step2Props) {
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={LABEL_CLS}>Budget (EUR)<InfoTooltip content="Total project budget in EUR. Stored internally as cents. Used to track spending against assignments." /></label>
|
||||
<label className="app-label">Budget (EUR)<InfoTooltip content="Total project budget in EUR. Stored internally as cents. Used to track spending against assignments." /></label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
@@ -602,11 +596,11 @@ function Step2({ state, onChange }: Step2Props) {
|
||||
value={state.budgetEur}
|
||||
onChange={(e) => onChange({ budgetEur: e.target.value })}
|
||||
placeholder="e.g. 50000"
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={LABEL_CLS}>Responsible Person<InfoTooltip content="Project lead or account manager. Search by name or employee ID." /></label>
|
||||
<label className="app-label">Responsible Person<InfoTooltip content="Project lead or account manager. Search by name or employee ID." /></label>
|
||||
<ResourcePersonPicker
|
||||
value={state.responsiblePerson}
|
||||
onChange={(v) => onChange({ responsiblePerson: v })}
|
||||
@@ -615,7 +609,7 @@ function Step2({ state, onChange }: Step2Props) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={LABEL_CLS}>
|
||||
<label className="app-label">
|
||||
Win Probability: <strong>{state.winProbability}%</strong>
|
||||
<InfoTooltip content="Likelihood of winning this project (0-100%). Affects the weighted pipeline value: budget x probability." />
|
||||
</label>
|
||||
@@ -727,7 +721,7 @@ function Step3({ state, onChange }: Step3Props) {
|
||||
onChange({ staffingReqs: state.staffingReqs.map((r, i) => i === idx ? rest : r) });
|
||||
}
|
||||
}}
|
||||
className={SELECT_CLS}
|
||||
className="app-select w-full"
|
||||
>
|
||||
<option value="">Custom / Free text…</option>
|
||||
{roles.map((ro) => (
|
||||
@@ -741,7 +735,7 @@ function Step3({ state, onChange }: Step3Props) {
|
||||
value={req.role}
|
||||
onChange={(e) => updateReq(idx, { role: e.target.value })}
|
||||
placeholder="e.g. 3D Artist"
|
||||
className={clsx(INPUT_CLS, roles.length > 0 && "mt-1")}
|
||||
className={clsx("app-input", roles.length > 0 && "mt-1")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -754,7 +748,7 @@ function Step3({ state, onChange }: Step3Props) {
|
||||
max={24}
|
||||
step={0.5}
|
||||
onChange={(e) => updateReq(idx, { hoursPerDay: parseFloat(e.target.value) || 0 })}
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-16">
|
||||
@@ -765,7 +759,7 @@ function Step3({ state, onChange }: Step3Props) {
|
||||
min={1}
|
||||
max={20}
|
||||
onChange={(e) => updateReq(idx, { headcount: parseInt(e.target.value, 10) || 1 })}
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-28">
|
||||
@@ -780,14 +774,14 @@ function Step3({ state, onChange }: Step3Props) {
|
||||
updateReq(idx, { budgetCents: Number.isFinite(val) && val > 0 ? Math.round(val * 100) : 0 } as Partial<StaffingRequirement>);
|
||||
}}
|
||||
placeholder="0"
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeReq(idx)}
|
||||
className={BTN_DANGER}
|
||||
className="app-action-danger-btn"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -820,7 +814,7 @@ function Step3({ state, onChange }: Step3Props) {
|
||||
updateReq(idx, { chapter: e.target.value || undefined } as Partial<StaffingRequirement>)
|
||||
}
|
||||
placeholder="e.g. Art Direction"
|
||||
className={INPUT_CLS}
|
||||
className="app-input"
|
||||
/>
|
||||
{chapters.length > 0 && (
|
||||
<datalist id="chapter-options">
|
||||
|
||||
Reference in New Issue
Block a user