refactor(ui): replace inline INPUT_CLS/BTN_DANGER/action link constants with component classes

- Replace 13 local INPUT_CLS/SELECT_CLS/LABEL_CLS/BTN_DANGER constants with
  app-input, app-select, app-label, app-action-danger-btn component classes
  (CustomFieldFilterBar, RolePresetsEditor, FieldCard, BlueprintFieldCatalog,
  BlueprintFieldEditor, BlueprintsClient, EstimateWizard, EstimateWorkspace-
  DraftEditor, DemandLineEditor, ScopeItemEditor, AssumptionEditor,
  ProjectWizard, BulkEditModal)
- Replace inline text-blue-600/text-red-500 action link strings with
  app-action-edit / app-action-delete in AllocationsClient, ProjectsClient,
  ScenarioPlanner, ProjectDemandsTable, RolesClient, BlueprintsClient,
  CreateTaskModal, RateCardsClient, UsersClient, ManagementLevelsClient

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 07:02:08 +02:00
parent e575462b01
commit 9b5cd8549d
22 changed files with 37 additions and 57 deletions
@@ -669,7 +669,7 @@ export function ProjectsClient() {
> >
Edit Edit
</button> </button>
<Link href={`/projects/${project.id}`} className="link-hover-underline text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-300 dark:hover:text-blue-200"> <Link href={`/projects/${project.id}`} className="app-action-edit link-hover-underline">
View View
</Link> </Link>
</div> </div>
@@ -189,7 +189,7 @@ export function ManagementLevelsClient() {
<button <button
type="button" type="button"
onClick={() => setConfirmDeleteLevel(level.id)} onClick={() => setConfirmDeleteLevel(level.id)}
className="text-xs text-red-500 hover:text-red-700 font-medium" className="app-action-delete"
> >
Delete Delete
</button> </button>
@@ -531,7 +531,7 @@ export function RateCardsClient() {
<button <button
type="button" type="button"
onClick={() => setConfirmDeleteLine(line.id)} onClick={() => setConfirmDeleteLine(line.id)}
className="text-xs text-red-500 hover:text-red-700 font-medium" className="app-action-delete"
> >
Delete Delete
</button> </button>
@@ -669,7 +669,7 @@ export function UsersClient() {
<button <button
type="button" type="button"
onClick={() => setDeleteTarget({ userId: user.id, userName: user.name ?? user.email })} onClick={() => setDeleteTarget({ userId: user.id, userName: user.name ?? user.email })}
className="text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 font-medium" className="app-action-delete"
title="Permanently delete user" title="Permanently delete user"
> >
Delete Delete
@@ -552,12 +552,12 @@ export function AllocationsClient() {
})} })}
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
<button type="button" onClick={() => openEdit(alloc)} className="text-xs font-medium text-blue-600 hover:text-blue-800 hover:underline dark:text-blue-300 dark:hover:text-blue-200">Edit</button> <button type="button" onClick={() => openEdit(alloc)} className="app-action-edit">Edit</button>
<button <button
type="button" type="button"
onClick={() => setConfirmDelete({ single: alloc })} onClick={() => setConfirmDelete({ single: alloc })}
disabled={singleDeletePending} disabled={singleDeletePending}
className="text-xs font-medium text-red-500 hover:text-red-700 hover:underline disabled:opacity-50 dark:text-red-300 dark:hover:text-red-200" className="app-action-delete disabled:opacity-50"
> >
Delete Delete
</button> </button>
@@ -977,7 +977,7 @@ export function AllocationsClient() {
<button <button
type="button" type="button"
onClick={() => openEdit(demand as AllocationWithDetails)} onClick={() => openEdit(demand as AllocationWithDetails)}
className="text-xs font-medium text-blue-600 hover:text-blue-800 hover:underline dark:text-blue-300 dark:hover:text-blue-200" className="app-action-edit"
> >
Edit Edit
</button> </button>
@@ -985,7 +985,7 @@ export function AllocationsClient() {
type="button" type="button"
onClick={() => setConfirmDelete({ single: demand as AllocationWithDetails })} onClick={() => setConfirmDelete({ single: demand as AllocationWithDetails })}
disabled={singleDeletePending} disabled={singleDeletePending}
className="text-xs font-medium text-red-500 hover:text-red-700 hover:underline disabled:opacity-50 dark:text-red-300 dark:hover:text-red-200" className="app-action-delete disabled:opacity-50"
> >
Delete Delete
</button> </button>
@@ -18,8 +18,7 @@ import type { CatalogField } from "~/lib/blueprint-field-catalog.js";
// Styles // Styles
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const INPUT_CLS = const INPUT_CLS = "app-input";
"px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500";
const BTN_PRIMARY = const BTN_PRIMARY =
"px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"; "px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50";
@@ -18,8 +18,7 @@ const FIELD_TYPES: { value: FieldType; label: string }[] = [
{ value: FieldType.EMAIL, label: "Email" }, { value: FieldType.EMAIL, label: "Email" },
]; ];
const INPUT_CLS = const INPUT_CLS = "app-input";
"px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm";
const BTN_PRIMARY = const BTN_PRIMARY =
"px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"; "px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50";
@@ -27,8 +26,7 @@ const BTN_PRIMARY =
const BTN_SECONDARY = const BTN_SECONDARY =
"px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium"; "px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium";
const BTN_DANGER = const BTN_DANGER = "app-action-danger-btn";
"px-2 py-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded text-sm transition-colors";
function makeEmptyField(order: number): BlueprintFieldDefinition { function makeEmptyField(order: number): BlueprintFieldDefinition {
return { return {
@@ -14,8 +14,7 @@ import { SortableColumnHeader } from "~/components/ui/SortableColumnHeader.js";
import { useTableSort } from "~/hooks/useTableSort.js"; import { useTableSort } from "~/hooks/useTableSort.js";
import { useViewPrefs } from "~/hooks/useViewPrefs.js"; import { useViewPrefs } from "~/hooks/useViewPrefs.js";
const INPUT_CLS = const INPUT_CLS = "app-input";
"px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm";
const BTN_PRIMARY = const BTN_PRIMARY =
"px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"; "px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50";
@@ -430,7 +429,7 @@ export function BlueprintsClient() {
} }
}} }}
disabled={deleteMutation.isPending} disabled={deleteMutation.isPending}
className="text-xs text-red-500 hover:text-red-700 disabled:opacity-50" className="app-action-delete disabled:opacity-50"
> >
Delete Delete
</button> </button>
@@ -59,8 +59,7 @@ interface FieldCardProps {
// Component // Component
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const INPUT_CLS = const INPUT_CLS = "app-input";
"px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm";
export function FieldCard({ field, overrides, onChange }: FieldCardProps) { export function FieldCard({ field, overrides, onChange }: FieldCardProps) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
@@ -4,11 +4,8 @@ import { useState } from "react";
import type { StaffingRequirement } from "@capakraken/shared"; import type { StaffingRequirement } from "@capakraken/shared";
import { uuid } from "~/lib/uuid.js"; import { uuid } from "~/lib/uuid.js";
const INPUT_CLS = const INPUT_CLS = "app-input";
"px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm"; const BTN_DANGER = "app-action-danger-btn";
const BTN_DANGER =
"px-2 py-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded text-sm transition-colors";
function makeEmptyPreset(): StaffingRequirement { function makeEmptyPreset(): StaffingRequirement {
return { return {
@@ -14,10 +14,9 @@ import { ResourceCombobox } from "~/components/ui/ResourceCombobox.js";
import { trpc } from "~/lib/trpc/client.js"; import { trpc } from "~/lib/trpc/client.js";
import { uuid } from "~/lib/uuid.js"; import { uuid } from "~/lib/uuid.js";
const INPUT_CLS = const INPUT_CLS = "app-input";
"w-full rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-100"; const SELECT_CLS = "app-select w-full";
const SELECT_CLS = INPUT_CLS; const LABEL_CLS = "app-label";
const LABEL_CLS = "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-500";
const STEP_LABELS = ["Setup", "Assumptions", "Scope", "Staffing", "Review"]; const STEP_LABELS = ["Setup", "Assumptions", "Scope", "Staffing", "Review"];
interface AssumptionRow { interface AssumptionRow {
@@ -39,9 +39,8 @@ interface ResourceListView {
resources: ResourceOption[]; resources: ResourceOption[];
} }
const INPUT_CLS = const INPUT_CLS = "app-input";
"w-full rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-100"; const LABEL_CLS = "app-label";
const LABEL_CLS = "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-500";
function toNumber(value: string) { function toNumber(value: string) {
const parsed = Number.parseFloat(value); const parsed = Number.parseFloat(value);
@@ -2,9 +2,8 @@
import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
const INPUT_CLS = const INPUT_CLS = "app-input";
"w-full rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-100"; const LABEL_CLS = "app-label";
const LABEL_CLS = "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-500";
export interface EditableAssumption { export interface EditableAssumption {
id?: string; id?: string;
@@ -13,9 +13,8 @@ import type { EstimateResourceSnapshotView } from "~/components/estimates/Estima
import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { formatMoney } from "~/lib/format.js"; import { formatMoney } from "~/lib/format.js";
const INPUT_CLS = const INPUT_CLS = "app-input";
"w-full rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-100"; const LABEL_CLS = "app-label";
const LABEL_CLS = "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-500";
function toNumber(value: string) { function toNumber(value: string) {
const parsed = Number.parseFloat(value); const parsed = Number.parseFloat(value);
@@ -4,9 +4,8 @@ import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { isSpreadsheetFile } from "~/lib/excel.js"; import { isSpreadsheetFile } from "~/lib/excel.js";
import { parseScopeImport } from "~/lib/scopeImportParser.js"; import { parseScopeImport } from "~/lib/scopeImportParser.js";
const INPUT_CLS = const INPUT_CLS = "app-input";
"w-full rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-100"; const LABEL_CLS = "app-label";
const LABEL_CLS = "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-500";
export interface EditableScopeItem { export interface EditableScopeItem {
id?: string; id?: string;
@@ -238,7 +238,7 @@ export function CreateTaskModal({ onClose, onSuccess }: CreateTaskModalProps) {
<button <button
type="button" type="button"
onClick={() => setUserId("")} onClick={() => setUserId("")}
className="text-xs text-red-500 hover:text-red-700" className="app-action-delete"
> >
Clear Clear
</button> </button>
@@ -166,7 +166,7 @@ export function ProjectDemandsTable({ demands, project }: ProjectDemandsTablePro
<button <button
type="button" type="button"
onClick={() => setEditTarget(demand as unknown as AllocationWithDetails)} onClick={() => setEditTarget(demand as unknown as AllocationWithDetails)}
className="text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" className="app-action-edit"
> >
Edit Edit
</button> </button>
@@ -30,13 +30,9 @@ const ALLOCATION_TYPE_OPTIONS = [
{ value: "EXT", label: "EXT" }, { value: "EXT", label: "EXT" },
] as const; ] as const;
const INPUT_CLS = const INPUT_CLS = "app-input";
"px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm w-full"; const SELECT_CLS = "app-select w-full";
const LABEL_CLS = "app-label";
const SELECT_CLS =
"px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm w-full bg-white";
const LABEL_CLS = "block text-xs font-medium text-gray-600 mb-1";
const BTN_PRIMARY = 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"; "px-5 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50 transition-colors";
@@ -44,8 +40,7 @@ const BTN_PRIMARY =
const BTN_SECONDARY = 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"; "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 = const BTN_DANGER = "app-action-danger-btn";
"px-2 py-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded text-sm transition-colors";
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
@@ -501,7 +501,7 @@ function ScenarioRowEditor({
<button <button
type="button" type="button"
onClick={() => onRestore(row.key)} onClick={() => onRestore(row.key)}
className="text-xs text-blue-600 hover:text-blue-800 underline" className="app-action-edit"
> >
Restore Restore
</button> </button>
@@ -5,8 +5,7 @@ import { FieldType } from "@capakraken/shared";
import type { BlueprintFieldDefinition } from "@capakraken/shared"; import type { BlueprintFieldDefinition } from "@capakraken/shared";
import { trpc } from "~/lib/trpc/client.js"; import { trpc } from "~/lib/trpc/client.js";
const INPUT_CLS = const INPUT_CLS = "app-input";
"px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm";
interface Props { interface Props {
selectedIds: string[]; selectedIds: string[];
@@ -280,7 +280,7 @@ export function RolesClient() {
setConfirmDelete(role); setConfirmDelete(role);
setActionError(null); setActionError(null);
}} }}
className="text-xs text-red-500 hover:text-red-700" className="app-action-delete"
> >
Delete Delete
</button> </button>
@@ -11,8 +11,7 @@ interface Props {
onSetFilter: (key: string, value: string, type: FieldType) => void; onSetFilter: (key: string, value: string, type: FieldType) => void;
} }
const INPUT_CLS = const INPUT_CLS = "app-input";
"px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-white";
export function CustomFieldFilterBar({ filterableFields, activeFilters, onSetFilter }: Props) { export function CustomFieldFilterBar({ filterableFields, activeFilters, onSetFilter }: Props) {
if (filterableFields.length === 0) return null; if (filterableFields.length === 0) return null;