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
@@ -18,8 +18,6 @@ import type { CatalogField } from "~/lib/blueprint-field-catalog.js";
// Styles
// ---------------------------------------------------------------------------
const INPUT_CLS = "app-input";
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";
@@ -496,7 +494,7 @@ export function BlueprintFieldCatalog({
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search fields..."
className={`${INPUT_CLS} w-full`}
className="app-input"
autoFocus
/>
</div>
@@ -636,7 +634,7 @@ export function BlueprintFieldCatalog({
)
}
placeholder="field_key"
className={`${INPUT_CLS} font-mono`}
className="app-input font-mono"
/>
</div>
<div className="flex flex-col gap-1">
@@ -651,7 +649,7 @@ export function BlueprintFieldCatalog({
setCustomLabel(e.target.value)
}
placeholder="Display Label"
className={INPUT_CLS}
className="app-input"
/>
</div>
<div className="flex flex-col gap-1">
@@ -665,7 +663,7 @@ export function BlueprintFieldCatalog({
e.target.value as FieldType,
)
}
className={INPUT_CLS}
className="app-input"
>
{FIELD_TYPES.map((ft) => (
<option key={ft.value} value={ft.value}>
@@ -18,16 +18,12 @@ const FIELD_TYPES: { value: FieldType; label: string }[] = [
{ value: FieldType.EMAIL, label: "Email" },
];
const INPUT_CLS = "app-input";
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";
const BTN_SECONDARY =
"px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium";
const BTN_DANGER = "app-action-danger-btn";
function makeEmptyField(order: number): BlueprintFieldDefinition {
return {
id: Math.random().toString(36).slice(2),
@@ -73,19 +69,19 @@ function OptionsEditor({ options, onChange }: OptionsEditorProps) {
value={opt.value}
onChange={(e) => updateOption(idx, "value", e.target.value)}
placeholder="value"
className={`${INPUT_CLS} flex-1`}
className="app-input flex-1"
/>
<input
type="text"
value={opt.label}
onChange={(e) => updateOption(idx, "label", e.target.value)}
placeholder="label"
className={`${INPUT_CLS} flex-1`}
className="app-input flex-1"
/>
<button
type="button"
onClick={() => removeOption(idx)}
className={BTN_DANGER}
className="app-action-danger-btn"
aria-label="Remove option"
>
×
@@ -140,7 +136,7 @@ function FieldRow({ field, onChange, onDelete }: FieldRowProps) {
value={field.key}
onChange={(e) => update("key", e.target.value)}
placeholder="field_key"
className={`${INPUT_CLS} w-36 font-mono`}
className="app-input w-36 font-mono"
aria-label="Field key"
/>
@@ -150,7 +146,7 @@ function FieldRow({ field, onChange, onDelete }: FieldRowProps) {
value={field.label}
onChange={(e) => update("label", e.target.value)}
placeholder="Label"
className={`${INPUT_CLS} w-40`}
className="app-input w-40"
aria-label="Field label"
/>
@@ -166,7 +162,7 @@ function FieldRow({ field, onChange, onDelete }: FieldRowProps) {
: undefined;
onChange({ ...field, type: t, options: clearedOptions } as BlueprintFieldDefinition);
}}
className={`${INPUT_CLS} w-36`}
className="app-input w-36"
aria-label="Field type"
>
{FIELD_TYPES.map((ft) => (
@@ -201,7 +197,7 @@ function FieldRow({ field, onChange, onDelete }: FieldRowProps) {
<button
type="button"
onClick={onDelete}
className={BTN_DANGER}
className="app-action-danger-btn"
aria-label="Delete field"
>
×
@@ -218,7 +214,7 @@ function FieldRow({ field, onChange, onDelete }: FieldRowProps) {
value={field.group ?? ""}
onChange={(e) => update("group", e.target.value || undefined)}
placeholder="Section heading"
className={INPUT_CLS}
className="app-input"
/>
</div>
<div className="flex flex-col gap-1">
@@ -232,7 +228,7 @@ function FieldRow({ field, onChange, onDelete }: FieldRowProps) {
update("placeholder", e.target.value || undefined)
}
placeholder="Placeholder text"
className={INPUT_CLS}
className="app-input"
/>
</div>
<div className="flex flex-col gap-1">
@@ -246,7 +242,7 @@ function FieldRow({ field, onChange, onDelete }: FieldRowProps) {
update("description", e.target.value || undefined)
}
placeholder="Helper text"
className={INPUT_CLS}
className="app-input"
/>
</div>
@@ -14,8 +14,6 @@ import { SortableColumnHeader } from "~/components/ui/SortableColumnHeader.js";
import { useTableSort } from "~/hooks/useTableSort.js";
import { useViewPrefs } from "~/hooks/useViewPrefs.js";
const INPUT_CLS = "app-input";
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";
@@ -75,15 +73,15 @@ function NewBlueprintModal({ onClose, onCreated }: NewBlueprintModalProps) {
<form onSubmit={handleSubmit} className="px-6 py-5 space-y-4">
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-gray-700">Name <span className="text-red-500">*</span></label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Resource Extended Fields" className={INPUT_CLS} autoFocus />
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Resource Extended Fields" className="app-input" autoFocus />
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-gray-700">Description</label>
<textarea value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Optional description" className={`${INPUT_CLS} resize-none`} rows={2} />
<textarea value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Optional description" className="app-input resize-none" rows={2} />
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-gray-700">Target</label>
<select value={target} onChange={(e) => setTarget(e.target.value as BlueprintTargetValue)} className={INPUT_CLS}>
<select value={target} onChange={(e) => setTarget(e.target.value as BlueprintTargetValue)} className="app-input">
<option value="RESOURCE">Resource</option>
<option value="PROJECT">Project</option>
</select>
@@ -59,8 +59,6 @@ interface FieldCardProps {
// Component
// ---------------------------------------------------------------------------
const INPUT_CLS = "app-input";
export function FieldCard({ field, overrides, onChange }: FieldCardProps) {
const [expanded, setExpanded] = useState(false);
@@ -193,7 +191,7 @@ export function FieldCard({ field, overrides, onChange }: FieldCardProps) {
value={overrides.description}
onChange={(e) => update({ description: e.target.value })}
placeholder={field.description}
className={INPUT_CLS}
className="app-input"
/>
</div>
</div>
@@ -240,7 +238,7 @@ function DefaultValueInput({
onChange(e.target.value === "" ? undefined : Number(e.target.value))
}
placeholder="No default"
className={INPUT_CLS}
className="app-input"
/>
);
@@ -252,7 +250,7 @@ function DefaultValueInput({
onChange={(e) =>
onChange(e.target.value === "" ? undefined : e.target.value)
}
className={INPUT_CLS}
className="app-input"
/>
);
@@ -263,7 +261,7 @@ function DefaultValueInput({
onChange={(e) =>
onChange(e.target.value === "" ? undefined : e.target.value)
}
className={INPUT_CLS}
className="app-input"
>
<option value="">No default</option>
{(options ?? []).map((opt) => (
@@ -292,7 +290,7 @@ function DefaultValueInput({
onChange(e.target.value === "" ? undefined : e.target.value)
}
placeholder="https://..."
className={INPUT_CLS}
className="app-input"
/>
);
@@ -305,7 +303,7 @@ function DefaultValueInput({
onChange(e.target.value === "" ? undefined : e.target.value)
}
placeholder="name@example.com"
className={INPUT_CLS}
className="app-input"
/>
);
@@ -317,7 +315,7 @@ function DefaultValueInput({
onChange(e.target.value === "" ? undefined : e.target.value)
}
placeholder="No default"
className={`${INPUT_CLS} resize-none`}
className="app-input resize-none"
rows={2}
/>
);
@@ -331,7 +329,7 @@ function DefaultValueInput({
onChange(e.target.value === "" ? undefined : e.target.value)
}
placeholder="No default"
className={INPUT_CLS}
className="app-input"
/>
);
}
@@ -4,9 +4,6 @@ import { useState } from "react";
import type { StaffingRequirement } from "@capakraken/shared";
import { uuid } from "~/lib/uuid.js";
const INPUT_CLS = "app-input";
const BTN_DANGER = "app-action-danger-btn";
function makeEmptyPreset(): StaffingRequirement {
return {
id: uuid(),
@@ -38,7 +35,7 @@ function PresetRow({ preset, onChange, onDelete }: PresetRowProps) {
value={preset.role}
onChange={(e) => update("role", e.target.value)}
placeholder="Role name"
className={`${INPUT_CLS} flex-1 min-w-32`}
className="app-input flex-1 min-w-32"
aria-label="Role name"
/>
@@ -58,7 +55,7 @@ function PresetRow({ preset, onChange, onDelete }: PresetRowProps) {
)
}
placeholder="e.g. 3D Modeling, Lighting"
className={INPUT_CLS}
className="app-input"
aria-label="Required skills"
/>
</div>
@@ -73,7 +70,7 @@ function PresetRow({ preset, onChange, onDelete }: PresetRowProps) {
max={24}
step={0.5}
onChange={(e) => update("hoursPerDay", parseFloat(e.target.value) || 0)}
className={INPUT_CLS}
className="app-input"
aria-label="Hours per day"
/>
</div>
@@ -87,7 +84,7 @@ function PresetRow({ preset, onChange, onDelete }: PresetRowProps) {
min={1}
max={20}
onChange={(e) => update("headcount", parseInt(e.target.value, 10) || 1)}
className={INPUT_CLS}
className="app-input"
aria-label="Headcount"
/>
</div>
@@ -96,7 +93,7 @@ function PresetRow({ preset, onChange, onDelete }: PresetRowProps) {
<button
type="button"
onClick={onDelete}
className={`${BTN_DANGER} self-end mb-0.5`}
className="app-action-danger-btn self-end mb-0.5"
aria-label="Remove preset"
>
×
@@ -119,7 +116,7 @@ function PresetRow({ preset, onChange, onDelete }: PresetRowProps) {
)
}
placeholder="e.g. Compositing, Art Direction"
className={`${INPUT_CLS} w-full mt-0.5`}
className="app-input mt-0.5"
aria-label="Preferred skills"
/>
</div>
@@ -14,9 +14,6 @@ import { ResourceCombobox } from "~/components/ui/ResourceCombobox.js";
import { trpc } from "~/lib/trpc/client.js";
import { uuid } from "~/lib/uuid.js";
const INPUT_CLS = "app-input";
const SELECT_CLS = "app-select w-full";
const LABEL_CLS = "app-label";
const STEP_LABELS = ["Setup", "Assumptions", "Scope", "Staffing", "Review"];
interface AssumptionRow {
@@ -504,20 +501,20 @@ export function EstimateWizard({ onClose }: { onClose: () => void }) {
<div className="space-y-5">
<div className="grid gap-5 md:grid-cols-2">
<div>
<label className={LABEL_CLS}>Estimate Name <InfoTooltip content="A descriptive name for this estimate, e.g. project name + scope qualifier." /></label>
<input value={name} onChange={(event) => setName(event.target.value)} className={INPUT_CLS} placeholder="CGI Breakdown Q2 2026" />
<label className="app-label">Estimate Name <InfoTooltip content="A descriptive name for this estimate, e.g. project name + scope qualifier." /></label>
<input value={name} onChange={(event) => setName(event.target.value)} className="app-input" placeholder="CGI Breakdown Q2 2026" />
</div>
<div>
<label className={LABEL_CLS}>Linked Project <InfoTooltip content="Link to an existing CapaKraken project. This enables automatic date-based phasing and planning handoff." /></label>
<label className="app-label">Linked Project <InfoTooltip content="Link to an existing CapaKraken project. This enables automatic date-based phasing and planning handoff." /></label>
<ProjectCombobox value={projectId} onChange={setProjectId} placeholder="Link to project" />
</div>
<div>
<label className={LABEL_CLS}>Opportunity ID <InfoTooltip content="Optional external reference from your CRM or sales system to track this opportunity." /></label>
<input value={opportunityId} onChange={(event) => setOpportunityId(event.target.value)} className={INPUT_CLS} placeholder="Optional CRM or sales reference" />
<label className="app-label">Opportunity ID <InfoTooltip content="Optional external reference from your CRM or sales system to track this opportunity." /></label>
<input value={opportunityId} onChange={(event) => setOpportunityId(event.target.value)} className="app-input" placeholder="Optional CRM or sales reference" />
</div>
<div>
<label className={LABEL_CLS}>Estimate Status <InfoTooltip content="DRAFT: work in progress. IN_REVIEW: submitted for approval. APPROVED: locked and ready for handoff. ARCHIVED: no longer active." /></label>
<select value={status} onChange={(event) => setStatus(event.target.value as EstimateStatus)} className={SELECT_CLS}>
<label className="app-label">Estimate Status <InfoTooltip content="DRAFT: work in progress. IN_REVIEW: submitted for approval. APPROVED: locked and ready for handoff. ARCHIVED: no longer active." /></label>
<select value={status} onChange={(event) => setStatus(event.target.value as EstimateStatus)} className="app-select w-full">
{Object.values(EstimateStatus).map((value) => (
<option key={value} value={value}>
{value.replace("_", " ")}
@@ -526,22 +523,22 @@ export function EstimateWizard({ onClose }: { onClose: () => void }) {
</select>
</div>
<div>
<label className={LABEL_CLS}>Base Currency <InfoTooltip content="ISO 4217 currency code (e.g. EUR, USD) used for all monetary values in this estimate." /></label>
<input value={baseCurrency} onChange={(event) => setBaseCurrency(event.target.value.toUpperCase())} className={INPUT_CLS} maxLength={3} />
<label className="app-label">Base Currency <InfoTooltip content="ISO 4217 currency code (e.g. EUR, USD) used for all monetary values in this estimate." /></label>
<input value={baseCurrency} onChange={(event) => setBaseCurrency(event.target.value.toUpperCase())} className="app-input" maxLength={3} />
</div>
<div>
<label className={LABEL_CLS}>Version Label <InfoTooltip content="A label for the initial version snapshot. Use labels like 'Initial', 'Client revision 2', etc." /></label>
<input value={versionLabel} onChange={(event) => setVersionLabel(event.target.value)} className={INPUT_CLS} placeholder="Initial" />
<label className="app-label">Version Label <InfoTooltip content="A label for the initial version snapshot. Use labels like 'Initial', 'Client revision 2', etc." /></label>
<input value={versionLabel} onChange={(event) => setVersionLabel(event.target.value)} className="app-input" placeholder="Initial" />
</div>
</div>
<div>
<label className={LABEL_CLS}>Version Notes <InfoTooltip content="Free-text notes for this version. Document assumptions, exclusions, or client comments." /></label>
<label className="app-label">Version Notes <InfoTooltip content="Free-text notes for this version. Document assumptions, exclusions, or client comments." /></label>
<textarea
value={versionNotes}
onChange={(event) => setVersionNotes(event.target.value)}
rows={5}
className={INPUT_CLS}
className="app-input"
placeholder="Document assumptions, exclusions, or client comments."
/>
</div>
@@ -581,10 +578,10 @@ export function EstimateWizard({ onClose }: { onClose: () => void }) {
<div className="space-y-3">
{assumptions.map((row) => (
<div key={row.id} className="grid gap-3 rounded-2xl border border-gray-100 p-4 md:grid-cols-[140px,1fr,1fr,1.2fr,auto]">
<input value={row.category} onChange={(event) => updateAssumption(row.id, { category: event.target.value })} className={INPUT_CLS} placeholder="Category" />
<input value={row.label} onChange={(event) => updateAssumption(row.id, { label: event.target.value })} className={INPUT_CLS} placeholder="Label" />
<input value={row.key} onChange={(event) => updateAssumption(row.id, { key: event.target.value })} className={INPUT_CLS} placeholder="Key (optional)" />
<input value={row.value} onChange={(event) => updateAssumption(row.id, { value: event.target.value })} className={INPUT_CLS} placeholder="Value" />
<input value={row.category} onChange={(event) => updateAssumption(row.id, { category: event.target.value })} className="app-input" placeholder="Category" />
<input value={row.label} onChange={(event) => updateAssumption(row.id, { label: event.target.value })} className="app-input" placeholder="Label" />
<input value={row.key} onChange={(event) => updateAssumption(row.id, { key: event.target.value })} className="app-input" placeholder="Key (optional)" />
<input value={row.value} onChange={(event) => updateAssumption(row.id, { value: event.target.value })} className="app-input" placeholder="Value" />
<button type="button" onClick={() => setAssumptions((current) => current.filter((item) => item.id !== row.id))} className="rounded-xl border border-transparent px-3 py-2 text-sm text-red-500 transition hover:border-red-100 hover:bg-red-50">
Remove
</button>
@@ -623,10 +620,10 @@ export function EstimateWizard({ onClose }: { onClose: () => void }) {
<div className="space-y-3">
{scopeItems.map((item, index) => (
<div key={item.id} className="grid gap-3 rounded-2xl border border-gray-100 p-4 md:grid-cols-[90px,120px,1fr,1.2fr,auto]">
<input value={String(index + 1)} readOnly className={clsx(INPUT_CLS, "bg-gray-50 text-gray-500")} />
<input value={item.scopeType} onChange={(event) => updateScopeItem(item.id, { scopeType: event.target.value })} className={INPUT_CLS} placeholder="Type" />
<input value={item.name} onChange={(event) => updateScopeItem(item.id, { name: event.target.value })} className={INPUT_CLS} placeholder="Name" />
<input value={item.description} onChange={(event) => updateScopeItem(item.id, { description: event.target.value })} className={INPUT_CLS} placeholder="Description" />
<input value={String(index + 1)} readOnly className={clsx("app-input", "bg-gray-50 text-gray-500")} />
<input value={item.scopeType} onChange={(event) => updateScopeItem(item.id, { scopeType: event.target.value })} className="app-input" placeholder="Type" />
<input value={item.name} onChange={(event) => updateScopeItem(item.id, { name: event.target.value })} className="app-input" placeholder="Name" />
<input value={item.description} onChange={(event) => updateScopeItem(item.id, { description: event.target.value })} className="app-input" placeholder="Description" />
<button type="button" onClick={() => setScopeItems((current) => current.filter((row) => row.id !== item.id))} className="rounded-xl border border-transparent px-3 py-2 text-sm text-red-500 transition hover:border-red-100 hover:bg-red-50">
Remove
</button>
@@ -658,12 +655,12 @@ export function EstimateWizard({ onClose }: { onClose: () => void }) {
<div key={line.id} className="rounded-3xl border border-gray-100 p-4">
<div className="grid gap-4 lg:grid-cols-2">
<div>
<label className={LABEL_CLS}>Resource <InfoTooltip content="Link to a live CapaKraken resource. Auto-fills rates, chapter, and role." /></label>
<label className="app-label">Resource <InfoTooltip content="Link to a live CapaKraken resource. Auto-fills rates, chapter, and role." /></label>
<ResourceCombobox value={line.resourceId} onChange={(resourceId) => applyResource(resourceId, line.id)} placeholder="Search resource" />
</div>
<div>
<label className={LABEL_CLS}>Role <InfoTooltip content="The production role for this demand line (e.g. Compositor, Animator)." /></label>
<select value={line.roleId ?? ""} onChange={(event) => updateDemandLine(line.id, { roleId: event.target.value || null })} className={SELECT_CLS}>
<label className="app-label">Role <InfoTooltip content="The production role for this demand line (e.g. Compositor, Animator)." /></label>
<select value={line.roleId ?? ""} onChange={(event) => updateDemandLine(line.id, { roleId: event.target.value || null })} className="app-select w-full">
<option value="">Unassigned</option>
{roles.map((role) => (
<option key={role.id} value={role.id}>
@@ -673,28 +670,28 @@ export function EstimateWizard({ onClose }: { onClose: () => void }) {
</select>
</div>
<div>
<label className={LABEL_CLS}>Line Name <InfoTooltip content="Descriptive name for this staffing line, e.g. 'Compositing Lead' or 'PM overhead'." /></label>
<input value={line.name} onChange={(event) => updateDemandLine(line.id, { name: event.target.value })} className={INPUT_CLS} placeholder="Compositing, lighting, PM, ..." />
<label className="app-label">Line Name <InfoTooltip content="Descriptive name for this staffing line, e.g. 'Compositing Lead' or 'PM overhead'." /></label>
<input value={line.name} onChange={(event) => updateDemandLine(line.id, { name: event.target.value })} className="app-input" placeholder="Compositing, lighting, PM, ..." />
</div>
<div>
<label className={LABEL_CLS}>Chapter <InfoTooltip content="Department or cost center. Auto-filled when a resource is linked." /></label>
<input value={line.chapter} onChange={(event) => updateDemandLine(line.id, { chapter: event.target.value })} className={INPUT_CLS} placeholder="Auto-filled from resource when linked" />
<label className="app-label">Chapter <InfoTooltip content="Department or cost center. Auto-filled when a resource is linked." /></label>
<input value={line.chapter} onChange={(event) => updateDemandLine(line.id, { chapter: event.target.value })} className="app-input" placeholder="Auto-filled from resource when linked" />
</div>
<div>
<label className={LABEL_CLS}>Hours <InfoTooltip content="Total estimated effort in hours. Used to calculate line cost and price." /></label>
<input value={line.hours} onChange={(event) => updateDemandLine(line.id, { hours: event.target.value })} className={INPUT_CLS} inputMode="decimal" />
<label className="app-label">Hours <InfoTooltip content="Total estimated effort in hours. Used to calculate line cost and price." /></label>
<input value={line.hours} onChange={(event) => updateDemandLine(line.id, { hours: event.target.value })} className="app-input" inputMode="decimal" />
</div>
<div>
<label className={LABEL_CLS}>Currency <InfoTooltip content="ISO 4217 currency code for this line's rates." /></label>
<input value={line.currency} onChange={(event) => updateDemandLine(line.id, { currency: event.target.value.toUpperCase() })} className={INPUT_CLS} maxLength={3} />
<label className="app-label">Currency <InfoTooltip content="ISO 4217 currency code for this line's rates." /></label>
<input value={line.currency} onChange={(event) => updateDemandLine(line.id, { currency: event.target.value.toUpperCase() })} className="app-input" maxLength={3} />
</div>
<div>
<label className={LABEL_CLS}>Cost Rate / h <InfoTooltip content="Internal hourly cost rate in EUR. Line cost = hours x cost rate." /></label>
<input value={line.costRate} onChange={(event) => updateDemandLine(line.id, { costRate: event.target.value })} className={INPUT_CLS} inputMode="decimal" />
<label className="app-label">Cost Rate / h <InfoTooltip content="Internal hourly cost rate in EUR. Line cost = hours x cost rate." /></label>
<input value={line.costRate} onChange={(event) => updateDemandLine(line.id, { costRate: event.target.value })} className="app-input" inputMode="decimal" />
</div>
<div>
<label className={LABEL_CLS}>Sell Rate / h <InfoTooltip content="Client-facing hourly rate. Line price = hours x sell rate." /></label>
<input value={line.billRate} onChange={(event) => updateDemandLine(line.id, { billRate: event.target.value })} className={INPUT_CLS} inputMode="decimal" />
<label className="app-label">Sell Rate / h <InfoTooltip content="Client-facing hourly rate. Line price = hours x sell rate." /></label>
<input value={line.billRate} onChange={(event) => updateDemandLine(line.id, { billRate: event.target.value })} className="app-input" inputMode="decimal" />
</div>
</div>
@@ -39,9 +39,6 @@ interface ResourceListView {
resources: ResourceOption[];
}
const INPUT_CLS = "app-input";
const LABEL_CLS = "app-label";
function toNumber(value: string) {
const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) ? parsed : 0;
@@ -474,27 +471,27 @@ export function EstimateWorkspaceDraftEditor({
<section className="space-y-5 rounded-3xl border border-gray-200 bg-white p-6 shadow-sm">
<div className="grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLS}>Estimate name</span>
<input className={INPUT_CLS} value={name} onChange={(event) => setName(event.target.value)} />
<span className="app-label">Estimate name</span>
<input className="app-input" value={name} onChange={(event) => setName(event.target.value)} />
</label>
<label>
<span className={LABEL_CLS}>Opportunity ID</span>
<input className={INPUT_CLS} value={opportunityId} onChange={(event) => setOpportunityId(event.target.value)} />
<span className="app-label">Opportunity ID</span>
<input className="app-input" value={opportunityId} onChange={(event) => setOpportunityId(event.target.value)} />
</label>
<label>
<span className={LABEL_CLS}>Base currency</span>
<input className={INPUT_CLS} maxLength={3} value={baseCurrency} onChange={(event) => setBaseCurrency(event.target.value.toUpperCase())} />
<span className="app-label">Base currency</span>
<input className="app-input" maxLength={3} value={baseCurrency} onChange={(event) => setBaseCurrency(event.target.value.toUpperCase())} />
</label>
<label>
<span className={LABEL_CLS}>Version label</span>
<input className={INPUT_CLS} value={versionLabel} onChange={(event) => setVersionLabel(event.target.value)} />
<span className="app-label">Version label</span>
<input className="app-input" value={versionLabel} onChange={(event) => setVersionLabel(event.target.value)} />
</label>
</div>
<label className="block">
<span className={LABEL_CLS}>Version notes</span>
<span className="app-label">Version notes</span>
<textarea
className={`${INPUT_CLS} min-h-32`}
className="app-input min-h-32"
value={versionNotes}
onChange={(event) => setVersionNotes(event.target.value)}
/>
@@ -2,9 +2,6 @@
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
const INPUT_CLS = "app-input";
const LABEL_CLS = "app-label";
export interface EditableAssumption {
id?: string;
category: string;
@@ -38,30 +35,30 @@ export function AssumptionEditor({ assumptions, onChange }: AssumptionEditorProp
<div key={assumption.id ?? `new-${index}`} className="rounded-3xl border border-gray-200 bg-white p-5 shadow-sm">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<label>
<span className={LABEL_CLS}>Category <InfoTooltip content="Groups assumptions by topic, e.g. 'commercial', 'delivery', 'technical'." /></span>
<input className={INPUT_CLS} value={assumption.category} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, category: event.target.value } : item))} />
<span className="app-label">Category <InfoTooltip content="Groups assumptions by topic, e.g. 'commercial', 'delivery', 'technical'." /></span>
<input className="app-input" value={assumption.category} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, category: event.target.value } : item))} />
</label>
<label>
<span className={LABEL_CLS}>Key <InfoTooltip content="Machine-readable identifier, auto-generated from label if left empty." /></span>
<input className={INPUT_CLS} value={assumption.key} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, key: event.target.value } : item))} />
<span className="app-label">Key <InfoTooltip content="Machine-readable identifier, auto-generated from label if left empty." /></span>
<input className="app-input" value={assumption.key} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, key: event.target.value } : item))} />
</label>
<label>
<span className={LABEL_CLS}>Label <InfoTooltip content="Human-readable name for this assumption." /></span>
<input className={INPUT_CLS} value={assumption.label} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, label: event.target.value } : item))} />
<span className="app-label">Label <InfoTooltip content="Human-readable name for this assumption." /></span>
<input className="app-input" value={assumption.label} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, label: event.target.value } : item))} />
</label>
<label>
<span className={LABEL_CLS}>Type <InfoTooltip content="Data type of the value, e.g. 'string', 'number', 'boolean'." /></span>
<input className={INPUT_CLS} value={assumption.valueType} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, valueType: event.target.value } : item))} />
<span className="app-label">Type <InfoTooltip content="Data type of the value, e.g. 'string', 'number', 'boolean'." /></span>
<input className="app-input" value={assumption.valueType} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, valueType: event.target.value } : item))} />
</label>
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-[minmax(0,1fr),220px]">
<label className="block">
<span className={LABEL_CLS}>Value <InfoTooltip content="The concrete value or condition for this assumption." /></span>
<textarea className={`${INPUT_CLS} min-h-24`} value={assumption.value} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, value: event.target.value } : item))} />
<span className="app-label">Value <InfoTooltip content="The concrete value or condition for this assumption." /></span>
<textarea className="app-input min-h-24" value={assumption.value} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, value: event.target.value } : item))} />
</label>
<label className="block">
<span className={LABEL_CLS}>Notes <InfoTooltip content="Additional context or rationale for this assumption." /></span>
<textarea className={`${INPUT_CLS} min-h-24`} value={assumption.notes} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, notes: event.target.value } : item))} />
<span className="app-label">Notes <InfoTooltip content="Additional context or rationale for this assumption." /></span>
<textarea className="app-input min-h-24" value={assumption.notes} onChange={(event) => onChange((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, notes: event.target.value } : item))} />
</label>
</div>
<div className="mt-4 flex justify-end">
@@ -13,9 +13,6 @@ import type { EstimateResourceSnapshotView } from "~/components/estimates/Estima
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { formatMoney } from "~/lib/format.js";
const INPUT_CLS = "app-input";
const LABEL_CLS = "app-label";
function toNumber(value: string) {
const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) ? parsed : 0;
@@ -335,9 +332,9 @@ export function DemandLineEditor({
<div className="mb-4 grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLS}>Linked resource <InfoTooltip content="Link to a CapaKraken resource. Live-linked rates refresh automatically; manual overrides are persisted." /></span>
<span className="app-label">Linked resource <InfoTooltip content="Link to a CapaKraken resource. Live-linked rates refresh automatically; manual overrides are persisted." /></span>
<select
className={INPUT_CLS}
className="app-input"
value={line.resourceId ?? ""}
onChange={(event) => applyResourceSelection(index, event.target.value)}
>
@@ -359,30 +356,30 @@ export function DemandLineEditor({
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<label>
<span className={LABEL_CLS}>Name <InfoTooltip content="Descriptive label for this demand line, e.g. role name or resource name." /></span>
<input className={INPUT_CLS} value={line.name} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, name: event.target.value }))} />
<span className="app-label">Name <InfoTooltip content="Descriptive label for this demand line, e.g. role name or resource name." /></span>
<input className="app-input" value={line.name} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, name: event.target.value }))} />
</label>
<label>
<span className={LABEL_CLS}>Line type <InfoTooltip content="Classification of the demand, typically LABOR. Can also be EXPENSE or SUBCONTRACTOR." /></span>
<input className={INPUT_CLS} value={line.lineType} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, lineType: event.target.value }))} />
<span className="app-label">Line type <InfoTooltip content="Classification of the demand, typically LABOR. Can also be EXPENSE or SUBCONTRACTOR." /></span>
<input className="app-input" value={line.lineType} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, lineType: event.target.value }))} />
</label>
<label>
<span className={LABEL_CLS}>Chapter <InfoTooltip content="Department or cost center. Auto-filled when a resource is linked." /></span>
<input className={INPUT_CLS} value={line.chapter} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, chapter: event.target.value }))} />
<span className="app-label">Chapter <InfoTooltip content="Department or cost center. Auto-filled when a resource is linked." /></span>
<input className="app-input" value={line.chapter} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, chapter: event.target.value }))} />
</label>
<label>
<span className={LABEL_CLS}>Hours <InfoTooltip content="Estimated effort in hours. Cost total = hours x cost rate. Price total = hours x sell rate." /></span>
<input className={INPUT_CLS} value={line.hours} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, hours: event.target.value }))} />
<span className="app-label">Hours <InfoTooltip content="Estimated effort in hours. Cost total = hours x cost rate. Price total = hours x sell rate." /></span>
<input className="app-input" value={line.hours} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, hours: event.target.value }))} />
</label>
<label>
<span className={LABEL_CLS}>Currency <InfoTooltip content="ISO 4217 currency code for this line's rates (e.g. EUR, USD)." /></span>
<input className={INPUT_CLS} maxLength={3} value={line.currency} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, currency: event.target.value.toUpperCase() }))} />
<span className="app-label">Currency <InfoTooltip content="ISO 4217 currency code for this line's rates (e.g. EUR, USD)." /></span>
<input className="app-input" maxLength={3} value={line.currency} onChange={(event) => updateDemandLine(index, (entry) => ({ ...entry, currency: event.target.value.toUpperCase() }))} />
</label>
<label>
<span className={LABEL_CLS}>Cost rate <InfoTooltip content="Internal hourly cost rate. 'Live' syncs from the resource; 'Manual' allows a custom override. Line cost = hours x cost rate." /></span>
<span className="app-label">Cost rate <InfoTooltip content="Internal hourly cost rate. 'Live' syncs from the resource; 'Manual' allows a custom override. Line cost = hours x cost rate." /></span>
<div className="space-y-2">
<select
className={INPUT_CLS}
className="app-input"
value={line.costRateMode}
onChange={(event) =>
setDemandLineRateMode(index, "costRateMode", event.target.value as EstimateDemandLineRateMode)
@@ -392,7 +389,7 @@ export function DemandLineEditor({
<option value="manual">Manual override</option>
</select>
<input
className={INPUT_CLS}
className="app-input"
value={line.costRate}
onChange={(event) =>
updateDemandLine(index, (entry) => ({
@@ -413,10 +410,10 @@ export function DemandLineEditor({
</div>
</label>
<label>
<span className={LABEL_CLS}>Bill rate <InfoTooltip content="Client-facing hourly sell rate. 'Live' syncs from the resource; 'Manual' allows a custom override. Line price = hours x bill rate." /></span>
<span className="app-label">Bill rate <InfoTooltip content="Client-facing hourly sell rate. 'Live' syncs from the resource; 'Manual' allows a custom override. Line price = hours x bill rate." /></span>
<div className="space-y-2">
<select
className={INPUT_CLS}
className="app-input"
value={line.billRateMode}
onChange={(event) =>
setDemandLineRateMode(index, "billRateMode", event.target.value as EstimateDemandLineRateMode)
@@ -426,7 +423,7 @@ export function DemandLineEditor({
<option value="manual">Manual override</option>
</select>
<input
className={INPUT_CLS}
className="app-input"
value={line.billRate}
onChange={(event) =>
updateDemandLine(index, (entry) => ({
@@ -4,9 +4,6 @@ import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { isSpreadsheetFile } from "~/lib/excel.js";
import { parseScopeImport } from "~/lib/scopeImportParser.js";
const INPUT_CLS = "app-input";
const LABEL_CLS = "app-label";
export interface EditableScopeItem {
id?: string;
sequenceNo: string;
@@ -81,26 +78,26 @@ export function ScopeItemEditor({
<div key={item.id ?? `scope-${index}`} className="rounded-3xl border border-gray-200 bg-white p-5 shadow-sm">
<div className="grid gap-4 md:grid-cols-3">
<label>
<span className={LABEL_CLS}>Sequence <InfoTooltip content="Ordering number for this scope item within the estimate." /></span>
<input className={INPUT_CLS} value={item.sequenceNo} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, sequenceNo: event.target.value } : entry))} />
<span className="app-label">Sequence <InfoTooltip content="Ordering number for this scope item within the estimate." /></span>
<input className="app-input" value={item.sequenceNo} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, sequenceNo: event.target.value } : entry))} />
</label>
<label>
<span className={LABEL_CLS}>Scope type <InfoTooltip content="Category of deliverable, e.g. SHOT, SEQUENCE, ASSET, or a custom type." /></span>
<input className={INPUT_CLS} value={item.scopeType} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, scopeType: event.target.value } : entry))} />
<span className="app-label">Scope type <InfoTooltip content="Category of deliverable, e.g. SHOT, SEQUENCE, ASSET, or a custom type." /></span>
<input className="app-input" value={item.scopeType} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, scopeType: event.target.value } : entry))} />
</label>
<label>
<span className={LABEL_CLS}>Package code <InfoTooltip content="Optional reference code for tracking this scope item in external systems." /></span>
<input className={INPUT_CLS} value={item.packageCode} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, packageCode: event.target.value } : entry))} />
<span className="app-label">Package code <InfoTooltip content="Optional reference code for tracking this scope item in external systems." /></span>
<input className="app-input" value={item.packageCode} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, packageCode: event.target.value } : entry))} />
</label>
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-[minmax(0,0.9fr),minmax(0,1.1fr)]">
<label>
<span className={LABEL_CLS}>Name <InfoTooltip content="Short descriptive name for this deliverable or work package." /></span>
<input className={INPUT_CLS} value={item.name} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, name: event.target.value } : entry))} />
<span className="app-label">Name <InfoTooltip content="Short descriptive name for this deliverable or work package." /></span>
<input className="app-input" value={item.name} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, name: event.target.value } : entry))} />
</label>
<label>
<span className={LABEL_CLS}>Description <InfoTooltip content="Detailed description of what this scope item includes." /></span>
<textarea className={`${INPUT_CLS} min-h-24`} value={item.description} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, description: event.target.value } : entry))} />
<span className="app-label">Description <InfoTooltip content="Detailed description of what this scope item includes." /></span>
<textarea className="app-input min-h-24" value={item.description} onChange={(event) => onChange((current) => current.map((entry, entryIndex) => entryIndex === index ? { ...entry, description: event.target.value } : entry))} />
</label>
</div>
<div className="mt-4 flex justify-end">
@@ -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">
@@ -5,8 +5,6 @@ import { FieldType } from "@capakraken/shared";
import type { BlueprintFieldDefinition } from "@capakraken/shared";
import { trpc } from "~/lib/trpc/client.js";
const INPUT_CLS = "app-input";
interface Props {
selectedIds: string[];
fieldDefs: BlueprintFieldDefinition[];
@@ -136,7 +134,7 @@ function FieldInput({ field, value, onChange }: { field: BlueprintFieldDefinitio
if (field.type === FieldType.BOOLEAN) {
return (
<select value={str} onChange={(e) => onChange(e.target.value === "true")} className={INPUT_CLS}>
<select value={str} onChange={(e) => onChange(e.target.value === "true")} className="app-input">
<option value=""> select </option>
<option value="true">Yes</option>
<option value="false">No</option>
@@ -146,7 +144,7 @@ function FieldInput({ field, value, onChange }: { field: BlueprintFieldDefinitio
if (field.type === FieldType.SELECT && field.options) {
return (
<select value={str} onChange={(e) => onChange(e.target.value)} className={INPUT_CLS}>
<select value={str} onChange={(e) => onChange(e.target.value)} className="app-input">
<option value=""> select </option>
{field.options.map((o) => <option key={o.value} value={o.value}>{o.label || o.value}</option>)}
</select>
@@ -160,13 +158,13 @@ function FieldInput({ field, value, onChange }: { field: BlueprintFieldDefinitio
value={str}
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : "")}
placeholder={field.placeholder}
className={INPUT_CLS}
className="app-input"
/>
);
}
if (field.type === FieldType.DATE) {
return <input type="date" value={str} onChange={(e) => onChange(e.target.value)} className={INPUT_CLS} />;
return <input type="date" value={str} onChange={(e) => onChange(e.target.value)} className="app-input" />;
}
if (field.type === FieldType.TEXTAREA) {
@@ -175,7 +173,7 @@ function FieldInput({ field, value, onChange }: { field: BlueprintFieldDefinitio
value={str}
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
className={`${INPUT_CLS} w-full resize-none`}
className="app-input resize-none"
rows={3}
/>
);
@@ -187,7 +185,7 @@ function FieldInput({ field, value, onChange }: { field: BlueprintFieldDefinitio
value={str}
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
className={`${INPUT_CLS} w-full`}
className="app-input"
/>
);
}
@@ -11,8 +11,6 @@ interface Props {
onSetFilter: (key: string, value: string, type: FieldType) => void;
}
const INPUT_CLS = "app-input";
export function CustomFieldFilterBar({ filterableFields, activeFilters, onSetFilter }: Props) {
if (filterableFields.length === 0) return null;
@@ -32,7 +30,7 @@ export function CustomFieldFilterBar({ filterableFields, activeFilters, onSetFil
key={field.key}
value={value}
onChange={(e) => onSetFilter(field.key, e.target.value, field.type)}
className={INPUT_CLS}
className="app-input"
aria-label={field.label}
>
<option value="">{field.label}: any</option>
@@ -48,7 +46,7 @@ export function CustomFieldFilterBar({ filterableFields, activeFilters, onSetFil
key={field.key}
value={value}
onChange={(e) => onSetFilter(field.key, e.target.value, field.type)}
className={INPUT_CLS}
className="app-input"
aria-label={field.label}
>
<option value="">{field.label}: any</option>
@@ -67,7 +65,7 @@ export function CustomFieldFilterBar({ filterableFields, activeFilters, onSetFil
value={value}
onChange={(e) => onSetFilter(field.key, e.target.value, field.type)}
placeholder={field.label}
className={`${INPUT_CLS} w-32`}
className="app-input w-32"
aria-label={field.label}
/>
);
@@ -80,7 +78,7 @@ export function CustomFieldFilterBar({ filterableFields, activeFilters, onSetFil
type="date"
value={value}
onChange={(e) => onSetFilter(field.key, e.target.value, field.type)}
className={INPUT_CLS}
className="app-input"
aria-label={field.label}
/>
);
@@ -94,7 +92,7 @@ export function CustomFieldFilterBar({ filterableFields, activeFilters, onSetFil
value={value}
onChange={(e) => onSetFilter(field.key, e.target.value, field.type)}
placeholder={field.label}
className={`${INPUT_CLS} w-40`}
className="app-input w-40"
aria-label={field.label}
/>
);
@@ -831,7 +831,7 @@ export function HolidayCalendarEditor() {
type="button"
onClick={() => handleDeleteEntry(entry)}
disabled={deleteEntry.isPending}
className="text-xs font-medium text-red-600 hover:text-red-700"
className="app-action-delete"
>
Entfernen
</button>