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:
2026-04-10 09:21:03 +02:00
parent 9ba49c9ab8
commit 05aa864359
15 changed files with 159 additions and 197 deletions
@@ -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">