export function kitchenRemodelCalculator(form: FormTs) {
// Cabinet prices, stock line with shaker doors painted white (per linear foot / each)
const cabinetRates = {
baseLf: 260,
upperLf: 190,
tallEach: 1400
};
// Cabinet line surcharges (% on the cabinets)
type LineId = 'stock' | 'semi' | 'custom';
const linePct: Record<LineId, number> = { 'stock': 0, 'semi': 60, 'custom': 140 };
const lineNames: Record<LineId, string> = { 'stock': 'Stock', 'semi': 'Semi-custom', 'custom': 'Custom' };
// Semi-custom and custom lines include soft-close; custom adds lead time
const lineIncludesSoftClose: Record<LineId, boolean> = { 'stock': false, 'semi': true, 'custom': true };
// Door style surcharges (% on the cabinets)
type DoorId = 'shaker' | 'flat' | 'raised' | 'inset';
const doorPct: Record<DoorId, number> = { 'shaker': 0, 'flat': 0, 'raised': 8, 'inset': 22 };
const doorNames: Record<DoorId, string> = { 'shaker': 'Shaker', 'flat': 'Flat panel', 'raised': 'Raised panel', 'inset': 'Inset' };
// Finish surcharges (% on the cabinets)
type FinishId = 'white' | 'gray' | 'navy' | 'green' | 'oak' | 'walnut';
const finishPct: Record<FinishId, number> = { 'white': 0, 'gray': 6, 'navy': 6, 'green': 6, 'oak': 4, 'walnut': 40 };
const finishNames: Record<FinishId, string> = { 'white': 'Painted white', 'gray': 'Painted gray', 'navy': 'Painted navy', 'green': 'Painted green', 'oak': 'Stained oak', 'walnut': 'Walnut' };
// Layouts: an island or a peninsula takes a share of the base feet
type LayoutId = 'galley' | 'l' | 'u' | 'island' | 'peninsula';
const layoutNames: Record<LayoutId, string> = { 'galley': 'Galley', 'l': 'L-shape', 'u': 'U-shape', 'island': 'L with island', 'peninsula': 'L with peninsula' };
const layoutMaxIslandFt: Record<LayoutId, number> = { 'galley': 0, 'l': 0, 'u': 0, 'island': 10, 'peninsula': 8 };
const ISLAND_SHARE = 0.3;
// Cabinet extras
const extrasPrices = {
softCloseLf: 14, // per ft of base + wall cabinets, stock line only
rolloutEach: 180,
islandPanels: 850, // finished panels, toe kick and the code outlet
seatingOverhang: 450,
lightingLf: 42 // per ft of wall cabinets
};
// Countertops and backsplash ($/sq ft, installed)
type CounterId = 'quartz' | 'granite' | 'butcher' | 'laminate';
const counterRates: Record<CounterId, number> = { 'quartz': 78, 'granite': 68, 'butcher': 48, 'laminate': 32 };
const counterNames: Record<CounterId, string> = { 'quartz': 'Quartz', 'granite': 'Granite', 'butcher': 'Butcher block', 'laminate': 'Laminate' };
type BacksplashId = 'none' | 'tile' | 'slab';
const backsplashRates: Record<BacksplashId, number> = { 'none': 0, 'tile': 32, 'slab': 95 };
const backsplashNames: Record<BacksplashId, string> = { 'none': 'Paint only', 'tile': 'Subway tile', 'slab': 'Countertop slab' };
const SINK_AND_FAUCET = 900;
const COUNTER_DEPTH_FT = 25.5 / 12; // 25.5-inch countertop over 24-inch base cabinets
const SPLASH_HEIGHT_FT = 1.5; // 18 inches between the counter and the wall cabinets
const OVERHANG_FT = 1; // 12-inch seating overhang
// Appliances, mid-range models, delivered and installed
const appliancePrices: Record<string, number> = {
'range': 2200,
'cooktop': 1500,
'wallOven': 2600,
'hood': 900,
'dishwasher': 1100,
'fridge': 2900,
'microwave': 600
};
const applianceNames: Record<string, string> = {
'range': 'Range',
'cooktop': 'Cooktop',
'wallOven': 'Wall oven',
'hood': 'Range hood',
'dishwasher': 'Dishwasher',
'fridge': 'Refrigerator',
'microwave': 'Microwave'
};
// The project: installation, tear-out, hookups, relocation
const projectPrices = {
installPct: 18, // % of the cabinet price: delivery, setting, leveling, hardware
tearOut: 1800,
hookupsInPlace: 650,
plumbingRelocation: 1600,
electricalRelocation: 1900,
permit: 450
};
// Result
const RANGE_PCT = 10; // the estimate is shown as a range ±10%
const MINIMUM_PROJECT = 6000;
const PROMO_PCT = 0; // promotional discount; 0 hides the banner
const PROMO_TEXT = 'Fall remodel special';
const LEAD_WEEKS = 6; // cabinets from the deposit, stock and semi-custom
const CUSTOM_EXTRA_WEEKS = 4;
const MEASURE_DAYS = 5; // business days until the in-home measure
const INSTALL_DAYS = 5; // working days the crew is in the house
const money = (value: number) => (value < 0 ? '-$' : '$') + Math.round(Math.abs(value)).toLocaleString('en-US');
const feet = (value: number) => (Math.round(value * 10) / 10) + ' ft';
const sqft = (value: number) => Math.round(value) + ' sq ft';
const pct = (value: number) => value > 0 ? ' (+' + value + '%)' : '';
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Kitchen Remodel Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addRow(row => {
row.addTextPanel('intro', {
computedValue: () => 'A ballpark estimate from the linear feet of cabinets. After the free in-home measure a designer draws the kitchen and gives you the final price, usually inside the range you see below.',
customStyles: { 'font-size': '0.95rem', 'color': '#475569' }
});
});
form.addSpacer({ height: 20 });
// Layout Section
const layoutSection = form.addSubform('layout', { title: '📐 Layout' });
layoutSection.addRow(row => {
row.addRadioButton('layout', {
label: 'Kitchen layout',
options: [
{ id: 'galley', name: 'Galley - two walls facing each other' },
{ id: 'l', name: 'L-shape - two walls in a corner (most common)' },
{ id: 'u', name: 'U-shape - three walls' },
{ id: 'island', name: 'L with island (+$' + extrasPrices.islandPanels + ' panels and outlet)' },
{ id: 'peninsula', name: 'L with peninsula (+$' + extrasPrices.islandPanels + ' panels and outlet)' }
],
defaultValue: 'l',
orientation: 'vertical',
isRequired: true
});
});
layoutSection.addRow(row => {
row.addDecimal('baseFeet', {
label: 'Base cabinets (linear ft)',
min: 2,
max: 40,
step: 0.5,
defaultValue: 12,
tooltip: 'Linear feet along the walls, island included',
isRequired: true
}, '1fr');
row.addDecimal('upperFeet', {
label: 'Wall cabinets (linear ft)',
min: 0,
max: 40,
step: 0.5,
defaultValue: 8,
tooltip: '0 for open shelves',
isRequired: true
}, '1fr');
row.addInteger('tallCabinets', {
label: 'Tall cabinets',
min: 0,
max: 6,
defaultValue: 1,
tooltip: 'Pantry, wall oven, refrigerator cabinet'
}, '1fr');
});
const getLayout = (): LayoutId => {
const value = layoutSection.radioButton('layout')?.value();
return value === 'galley' || value === 'u' || value === 'island' || value === 'peninsula' ? value : 'l';
};
const getBaseFeet = () => layoutSection.decimal('baseFeet')?.value() || 12;
const getUpperFeet = () => layoutSection.decimal('upperFeet')?.value() || 0;
const getTallCabinets = () => layoutSection.integer('tallCabinets')?.value() || 0;
const hasIsland = () => layoutMaxIslandFt[getLayout()] > 0;
const getIslandFeet = () => hasIsland() ? Math.min(layoutMaxIslandFt[getLayout()], Math.max(4, getBaseFeet() * ISLAND_SHARE)) : 0;
const islandWord = () => getLayout() === 'peninsula' ? 'peninsula' : 'island';
layoutSection.addRow(row => {
row.addCheckbox('seating', {
label: 'Seating overhang with room for counter stools (+$' + extrasPrices.seatingOverhang + ')',
defaultValue: false,
isVisible: () => hasIsland()
});
});
layoutSection.addRow(row => {
row.addTextPanel('islandInfo', {
computedValue: () => 'The ' + islandWord() + ' takes about ' + feet(getIslandFeet()) + ' of the base cabinets. Keep at least 42 inches of walkway between the ' + islandWord() + ' and the wall cabinets; the designer checks it at the measure.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => hasIsland()
});
});
const hasSeating = () => hasIsland() && layoutSection.checkbox('seating')?.value() === true;
// Cabinets Section
const cabinetsSection = form.addSubform('cabinets', { title: '🚪 Cabinets & Doors' });
cabinetsSection.addRow(row => {
row.addRadioButton('line', {
label: 'Cabinet line',
options: [
{ id: 'stock', name: 'Stock - standard sizes, quick from the warehouse (base price)' },
{ id: 'semi', name: 'Semi-custom - plywood boxes, more sizes, soft-close included' + pct(linePct['semi']) },
{ id: 'custom', name: 'Custom - built in our shop to the inch' + pct(linePct['custom']) }
],
defaultValue: 'semi',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('door', {
label: 'Door style',
options: [
{ id: 'shaker', name: 'Shaker - flat center panel in a square frame (included)' },
{ id: 'flat', name: 'Flat panel - plain slab, clean and modern (same price)' },
{ id: 'raised', name: 'Raised panel - profiled center panel, traditional' + pct(doorPct['raised']) },
{ id: 'inset', name: 'Inset - doors set flush inside a face frame' + pct(doorPct['inset']) }
],
defaultValue: 'shaker',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
const getLine = (): LineId => {
const value = cabinetsSection.radioButton('line')?.value();
return value === 'stock' || value === 'custom' ? value : 'semi';
};
const getDoor = (): DoorId => {
const value = cabinetsSection.radioButton('door')?.value();
return value === 'flat' || value === 'raised' || value === 'inset' ? value : 'shaker';
};
cabinetsSection.addRow(row => {
row.addDropdown('finish', {
label: 'Finish',
options: [
{ id: 'white', name: 'Painted white (included)' },
{ id: 'gray', name: 'Painted gray' + pct(finishPct['gray']) },
{ id: 'navy', name: 'Painted navy' + pct(finishPct['navy']) },
{ id: 'green', name: 'Painted green' + pct(finishPct['green']) },
{ id: 'oak', name: 'Stained white oak' + pct(finishPct['oak']) },
{ id: 'walnut', name: 'Walnut' + pct(finishPct['walnut']) }
],
defaultValue: 'white',
tooltip: 'Painted doors are sprayed in the shop; oak and walnut are stained and clear-coated',
isRequired: true
}, '1fr');
row.addInteger('rollouts', {
label: 'Roll-out trays (+$' + extrasPrices.rolloutEach + ' each)',
min: 0,
max: 12,
defaultValue: 0,
tooltip: 'Pull-out shelves in base and pantry cabinets'
}, '1fr');
});
const getFinish = (): FinishId => {
const value = cabinetsSection.dropdown('finish')?.value();
return value === 'gray' || value === 'navy' || value === 'green' || value === 'oak' || value === 'walnut' ? value : 'white';
};
cabinetsSection.addRow(row => {
row.addCheckbox('softClose', {
label: 'Soft-close hinges and drawer glides (+$' + extrasPrices.softCloseLf + '/ft)',
defaultValue: false,
isVisible: () => !lineIncludesSoftClose[getLine()]
}, '1fr');
row.addCheckbox('lighting', {
label: 'Under-cabinet LED lighting (+$' + extrasPrices.lightingLf + '/ft of wall cabinets)',
defaultValue: false,
isVisible: () => getUpperFeet() > 0
}, '1fr');
});
cabinetsSection.addRow(row => {
row.addTextPanel('softCloseInfo', {
computedValue: () => 'Soft-close hinges and drawer glides are included in the ' + lineNames[getLine()].toLowerCase() + ' line.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' },
isVisible: () => lineIncludesSoftClose[getLine()]
});
});
const hasSoftClose = () => lineIncludesSoftClose[getLine()] || cabinetsSection.checkbox('softClose')?.value() === true;
const hasLighting = () => getUpperFeet() > 0 && cabinetsSection.checkbox('lighting')?.value() === true;
const getRollouts = () => cabinetsSection.integer('rollouts')?.value() || 0;
// Countertops Section
const countersSection = form.addSubform('countertops', { title: '🪨 Countertops & Backsplash' });
countersSection.addRow(row => {
row.addRadioButton('counter', {
label: 'Countertop',
options: [
{ id: 'quartz', name: 'Quartz - no sealing, consistent pattern ($' + counterRates['quartz'] + '/sq ft)' },
{ id: 'granite', name: 'Granite - natural stone, sealed once a year ($' + counterRates['granite'] + '/sq ft)' },
{ id: 'butcher', name: 'Butcher block - maple, warm, oiled ($' + counterRates['butcher'] + '/sq ft)' },
{ id: 'laminate', name: 'Laminate - budget-friendly, many patterns ($' + counterRates['laminate'] + '/sq ft)' }
],
defaultValue: 'quartz',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('backsplash', {
label: 'Backsplash',
options: [
{ id: 'none', name: 'Paint only - we leave the wall for the painter' },
{ id: 'tile', name: 'Subway tile - 3 x 6 ceramic, 18 in high ($' + backsplashRates['tile'] + '/sq ft)' },
{ id: 'slab', name: 'Countertop slab - the counter runs up the wall ($' + backsplashRates['slab'] + '/sq ft)' }
],
defaultValue: 'none',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
countersSection.addRow(row => {
row.addCheckbox('sink', {
label: 'New stainless undermount sink with pull-down faucet (+$' + SINK_AND_FAUCET + ')',
defaultValue: false
});
});
const getCounter = (): CounterId => {
const value = countersSection.radioButton('counter')?.value();
return value === 'granite' || value === 'butcher' || value === 'laminate' ? value : 'quartz';
};
const getBacksplash = (): BacksplashId => {
const value = countersSection.radioButton('backsplash')?.value();
return value === 'tile' || value === 'slab' ? value : 'none';
};
const hasSink = () => countersSection.checkbox('sink')?.value() === true;
const getCounterSqft = () => getBaseFeet() * COUNTER_DEPTH_FT + (hasSeating() ? getIslandFeet() * OVERHANG_FT : 0);
// Backsplash goes on the wall runs only: the island or the peninsula has none
const getBacksplashSqft = () => getBacksplash() === 'none' ? 0 : Math.max(0, getBaseFeet() - getIslandFeet()) * SPLASH_HEIGHT_FT;
countersSection.addRow(row => {
row.addTextPanel('counterInfo', {
computedValue: () => 'About ' + sqft(getCounterSqft()) + ' of countertop'
+ (getBacksplashSqft() > 0 ? ' and ' + sqft(getBacksplashSqft()) + ' of backsplash' : '')
+ ', counted from the base feet at 25.5 inches deep' + (hasSeating() ? ' with the 12-inch seating overhang' : '')
+ '. The fabricator templates the exact slabs after the cabinets are in.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
// Appliances Section
const appliancesSection = form.addSubform('appliances', { title: '🍳 Appliances' });
appliancesSection.addRow(row => {
row.addRadioButton('plan', {
label: 'Appliances',
options: [
{ id: 'own', name: 'I keep or buy my own - we leave the openings and connect them' },
{ id: 'ours', name: 'New from you - delivered, installed, connected' }
],
defaultValue: 'own',
orientation: 'vertical',
isRequired: true
});
});
const suppliesAppliances = () => appliancesSection.radioButton('plan')?.value() === 'ours';
appliancesSection.addRow(row => {
row.addCheckboxList('appliances', {
label: () => suppliesAppliances() ? 'What we supply and install (mid-range models)' : 'What goes into the layout',
options: () => Object.keys(applianceNames).map(id => ({
id,
name: (applianceNames[id] || id) + (suppliesAppliances() ? ' (+$' + (appliancePrices[id] || 0).toLocaleString('en-US') + ')' : '')
})),
defaultValue: ['range', 'dishwasher'],
orientation: 'vertical'
});
});
const getAppliances = () => (appliancesSection.checkboxList('appliances')?.value() || []).filter(id => id in applianceNames);
appliancesSection.addRow(row => {
row.addTextPanel('ovenInfo', {
computedValue: () => 'A wall oven needs a tall cabinet. Add one in the Layout section, or pick a range instead: the oven sits under the cooktop and no tall cabinet is needed.',
customStyles: { 'font-size': '0.9rem', 'color': '#b45309', 'background': '#fef3c7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => getAppliances().includes('wallOven') && getTallCabinets() === 0
});
});
// Project Section
const projectSection = form.addSubform('project', { title: '🏗️ Tear-out & Timing' });
projectSection.addRow(row => {
row.addRadioButton('existing', {
label: 'The kitchen today',
options: [
{ id: 'remove', name: 'Tear out the old kitchen - we remove and haul away (+$' + projectPrices.tearOut.toLocaleString('en-US') + ')' },
{ id: 'gutted', name: 'Already gutted - the room is empty' },
{ id: 'new', name: 'New construction - nothing to remove' }
],
defaultValue: 'remove',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('hookups', {
label: 'Plumbing and electrical',
options: [
{ id: 'same', name: 'Sink and range stay put - reconnect in place (+$' + projectPrices.hookupsInPlace + ')' },
{ id: 'move', name: 'Sink or range moves - new lines, circuits and a permit (+$' + (projectPrices.plumbingRelocation + projectPrices.electricalRelocation + projectPrices.permit).toLocaleString('en-US') + ')' }
],
defaultValue: 'same',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
const needsTearOut = () => (projectSection.radioButton('existing')?.value() || 'remove') === 'remove';
const movesHookups = () => projectSection.radioButton('hookups')?.value() === 'move';
const getLeadWeeks = () => LEAD_WEEKS + (getLine() === 'custom' ? CUSTOM_EXTRA_WEEKS : 0);
projectSection.addRow(row => {
row.addTextPanel('permitInfo', {
computedValue: () => 'Moving the sink or the range means a permit: new supply and drain lines ($' + projectPrices.plumbingRelocation.toLocaleString('en-US') + '), new circuits ($' + projectPrices.electricalRelocation.toLocaleString('en-US') + ') and a city inspection ($' + projectPrices.permit + '). An island with an outlet needs a circuit too.',
customStyles: { 'font-size': '0.9rem', 'color': '#b45309', 'background': '#fef3c7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => movesHookups()
});
});
projectSection.addRow(row => {
row.addDatepicker('startDate', {
label: () => 'Preferred start (cabinets take ' + getLeadWeeks() + ' weeks from the deposit)',
isRequired: false
}, '1fr');
row.addTextarea('notes', {
label: 'Anything else we should know?',
placeholder: 'E.g. the window over the sink stays, the wall to the dining room comes down, we have the floor plan',
rows: 2,
maxLength: 1500
}, '1fr');
});
form.addSpacer({ height: 20, showLine: true, lineStyle: 'dashed' });
// Pricing helpers
const getCabinetsBase = () => getBaseFeet() * cabinetRates.baseLf + getUpperFeet() * cabinetRates.upperLf + getTallCabinets() * cabinetRates.tallEach;
const getCabinets = () => getCabinetsBase() * (1 + linePct[getLine()] / 100) * (1 + doorPct[getDoor()] / 100) * (1 + finishPct[getFinish()] / 100);
const getIslandExtra = () => (hasIsland() ? extrasPrices.islandPanels : 0) + (hasSeating() ? extrasPrices.seatingOverhang : 0);
const getSoftCloseCost = () => !lineIncludesSoftClose[getLine()] && hasSoftClose() ? (getBaseFeet() + getUpperFeet()) * extrasPrices.softCloseLf : 0;
const getRolloutCost = () => getRollouts() * extrasPrices.rolloutEach;
const getLightingCost = () => hasLighting() ? getUpperFeet() * extrasPrices.lightingLf : 0;
const getExtras = () => getIslandExtra() + getSoftCloseCost() + getRolloutCost() + getLightingCost();
const getInstall = () => getCabinets() * projectPrices.installPct / 100;
const getCounters = () => getCounterSqft() * counterRates[getCounter()];
const getBacksplashCost = () => getBacksplashSqft() * backsplashRates[getBacksplash()];
const getSinkCost = () => hasSink() ? SINK_AND_FAUCET : 0;
const getApplianceCost = () => suppliesAppliances() ? getAppliances().reduce((sum, id) => sum + (appliancePrices[id] || 0), 0) : 0;
const getSiteCost = () => (needsTearOut() ? projectPrices.tearOut : 0)
+ (movesHookups() ? projectPrices.plumbingRelocation + projectPrices.electricalRelocation + projectPrices.permit : projectPrices.hookupsInPlace);
const getBeforePromo = () => getCabinets() + getExtras() + getInstall() + getCounters() + getBacksplashCost() + getSinkCost() + getApplianceCost() + getSiteCost();
const getPromo = () => Math.round(getBeforePromo() * PROMO_PCT / 100);
// The minimum holds last: a promotion may never take a project below what the shop costs
const getTotal = () => Math.max(Math.round(getBeforePromo() - getPromo()), MINIMUM_PROJECT);
const getPriceFrom = () => Math.round(getTotal() * (1 - RANGE_PCT / 100));
const getPriceTo = () => Math.round(getTotal() * (1 + RANGE_PCT / 100));
const applianceLine = () => getAppliances().map(id => applianceNames[id] || id).join(', ') || 'none';
// Estimate Section
const summarySection = form.addSubform('summary', { title: '💰 Your Estimate', isCollapsible: false });
summarySection.addRow(row => {
row.addTextPanel('promoBanner', {
computedValue: () => PROMO_TEXT + ': the ' + PROMO_PCT + '% discount is already in the range, ' + money(getPromo()) + ' less on this kitchen.',
customStyles: { 'font-size': '0.95rem', 'font-weight': '500', 'color': '#166534', 'background': '#dcfce7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => getPromo() > 0
});
});
summarySection.addRow(row => {
row.addPriceDisplay('cabinetsCost', {
label: () => lineNames[getLine()] + ' cabinets (' + feet(getBaseFeet()) + ' base, ' + feet(getUpperFeet()) + ' wall, ' + getTallCabinets() + ' tall)',
computedValue: () => Math.round(getCabinets()),
decimals: 0,
variant: 'default'
}, '1fr');
row.addPriceDisplay('extrasCost', {
label: 'Cabinet extras',
computedValue: () => Math.round(getExtras()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('installCost', {
label: 'Installation (' + projectPrices.installPct + '% of the cabinets)',
computedValue: () => Math.round(getInstall()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('countersCost', {
label: () => counterNames[getCounter()] + ' countertop, ' + sqft(getCounterSqft()),
computedValue: () => Math.round(getCounters()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('backsplashSinkCost', {
label: 'Backsplash and sink',
computedValue: () => Math.round(getBacksplashCost() + getSinkCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('applianceCost', {
label: 'Appliances',
computedValue: () => Math.round(getApplianceCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('siteCost', {
label: 'Tear-out, hookups and permit',
computedValue: () => Math.round(getSiteCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('promoDiscount', {
label: PROMO_TEXT,
computedValue: () => -getPromo(),
decimals: 0,
variant: 'success',
isVisible: () => getPromo() > 0
}, '1fr');
});
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
summarySection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Middle of the range',
computedValue: () => getTotal(),
decimals: 0,
variant: 'large'
});
});
summarySection.addRow(row => {
row.addTextPanel('timeline', {
computedValue: () => 'A designer measures within ' + MEASURE_DAYS + ' business days. Cabinets arrive ' + getLeadWeeks() + ' weeks after the deposit, and the crew is in your kitchen about ' + INSTALL_DAYS + ' working days, plus a week or two for the countertop template and install.',
customStyles: { 'font-size': '0.9rem', 'color': '#059669', 'font-weight': '500' }
});
});
// Sticky Summary Section
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
finalSection.addRow(row => {
row.addPriceDisplay('priceRange', {
label: 'Estimated kitchen',
computedValue: () => getPriceFrom(),
decimals: 0,
variant: 'large',
suffix: () => 'to ' + money(getPriceTo())
});
});
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Range ±' + RANGE_PCT + '%, before sales tax. Minimum project $' + MINIMUM_PROJECT.toLocaleString('en-US') + '. Final price confirmed after the in-home measure.',
customStyles: { 'font-size': '0.85rem', 'color': '#94a3b8', 'font-style': 'italic' }
});
});
// Contact Section
const contactSection = form.addSubform('contact', { title: '📋 Book Your Free In-Home Measure' });
contactSection.addRow(row => {
row.addTextbox('name', {
label: 'Full Name',
placeholder: 'John Smith',
isRequired: true
}, '1fr');
row.addTextbox('phone', {
label: 'Phone',
placeholder: '(555) 123-4567',
isRequired: true
}, '1fr');
});
contactSection.addRow(row => {
row.addEmail('email', {
label: 'Email Address',
placeholder: 'john@example.com',
tooltip: 'Where we send the PDF estimate',
isRequired: true
}, '1fr');
row.addTextbox('zip', {
label: 'ZIP Code',
placeholder: '30062',
tooltip: 'Where we come to measure',
isRequired: true
}, '1fr');
});
// PDF estimate
form.configurePdf('estimate', pdf => {
pdf.configure({
filename: 'kitchen-estimate',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the estimate as PDF',
header: {
title: 'Kitchen Estimate',
subtitle: layoutNames[getLayout()] + ', ' + feet(getBaseFeet()) + ' of base cabinets, ' + lineNames[getLine()].toLowerCase() + ' line'
},
footer: {
text: 'Estimate based on the configuration from the form. A binding quote follows the in-home measure within ' + MEASURE_DAYS + ' business days.',
showPageNumbers: true
}
});
pdf.addSection('Your Kitchen', section => {
section.addRow(row => {
row.addField('Layout', layoutNames[getLayout()] + (hasSeating() ? ', seating overhang' : ''));
row.addField('Base cabinets', feet(getBaseFeet()));
row.addField('Wall cabinets', feet(getUpperFeet()));
row.addField('Tall cabinets', String(getTallCabinets()));
});
section.addRow(row => {
row.addField('Cabinet line', lineNames[getLine()]);
row.addField('Door style', doorNames[getDoor()]);
row.addField('Finish', finishNames[getFinish()]);
row.addField('Hardware', [hasSoftClose() ? 'soft-close' : 'standard hinges', getRollouts() > 0 ? getRollouts() + ' roll-out trays' : '', hasLighting() ? 'LED lighting' : ''].filter(part => part !== '').join(', '));
});
section.addRow(row => {
row.addField('Countertop', counterNames[getCounter()] + ', ' + sqft(getCounterSqft()));
row.addField('Backsplash', getBacksplash() === 'none' ? 'paint only' : backsplashNames[getBacksplash()] + ', ' + sqft(getBacksplashSqft()));
row.addField('Sink and faucet', hasSink() ? 'new' : 'existing');
row.addField('Appliances', (suppliesAppliances() ? 'new from us: ' : 'customer\'s own: ') + applianceLine());
});
section.addRow(row => {
row.addField('Existing kitchen', needsTearOut() ? 'tear-out and haul-away' : 'nothing to remove');
row.addField('Plumbing and electrical', movesHookups() ? 'sink or range moves, permit' : 'hookups in place');
row.addField('Preferred start', projectSection.datepicker('startDate')?.value() || 'flexible');
row.addField('ZIP code', contactSection.textbox('zip')?.value() || '');
});
});
pdf.addSection('Estimate', section => {
const rows: string[][] = [
[lineNames[getLine()] + ' cabinets', feet(getBaseFeet()) + ' base, ' + feet(getUpperFeet()) + ' wall, ' + getTallCabinets() + ' tall; ' + doorNames[getDoor()].toLowerCase() + ', ' + finishNames[getFinish()].toLowerCase(), money(getCabinets())]
];
if (getExtras() > 0) {
rows.push(['Cabinet extras', [
hasIsland() ? islandWord() + ' panels' : '',
hasSeating() ? 'seating overhang' : '',
getSoftCloseCost() > 0 ? 'soft-close' : '',
getRollouts() > 0 ? getRollouts() + ' roll-out trays' : '',
hasLighting() ? 'under-cabinet lighting, ' + feet(getUpperFeet()) : ''
].filter(part => part !== '').join(', '), money(getExtras())]);
}
rows.push(['Installation', projectPrices.installPct + '% of the cabinets: delivery, setting, leveling, hardware', money(getInstall())]);
rows.push([counterNames[getCounter()] + ' countertop', sqft(getCounterSqft()) + ' at $' + counterRates[getCounter()] + '/sq ft, installed', money(getCounters())]);
if (getBacksplashCost() > 0) rows.push([backsplashNames[getBacksplash()] + ' backsplash', sqft(getBacksplashSqft()) + ' at $' + backsplashRates[getBacksplash()] + '/sq ft', money(getBacksplashCost())]);
if (getSinkCost() > 0) rows.push(['Sink and faucet', 'stainless undermount, pull-down faucet', money(getSinkCost())]);
if (getApplianceCost() > 0) rows.push(['Appliances', applianceLine() + ', installed', money(getApplianceCost())]);
rows.push(['Tear-out, hookups and permit', (needsTearOut() ? 'tear-out and haul-away, ' : '') + (movesHookups() ? 'plumbing and electrical relocation, permit' : 'hookups in place'), money(getSiteCost())]);
rows.push(['Total before discounts', '', money(getBeforePromo())]);
if (getPromo() > 0) rows.push([PROMO_TEXT, PROMO_PCT + '%', '-' + money(getPromo())]);
section.addTable(['Item', 'Details', 'Amount'], rows);
section.addSpacer(8);
section.addText('Estimate: ' + money(getPriceFrom()) + ' to ' + money(getPriceTo()) + ' (range ±' + RANGE_PCT + '%, sales tax as applicable, price confirmed after the in-home measure).');
});
pdf.addSection('What Happens Next', section => {
section.addText('In-home measure within ' + MEASURE_DAYS + ' business days: a designer measures the room and draws the kitchen.');
section.addText('Cabinets arrive about ' + getLeadWeeks() + ' weeks after the deposit' + (getLine() === 'custom' ? ' (custom line)' : '') + '.');
section.addText('Installation about ' + INSTALL_DAYS + ' working days, then the countertop template and install.');
const notes = projectSection.textarea('notes')?.value();
if (notes) {
section.addDivider();
section.addText('Notes: ' + notes);
}
});
});
form.configureSubmitButton({
label: 'Send and Book the Measure'
});
form.configureCompletionScreen({
type: 'text',
title: 'Thank you!',
message: 'Your estimate is ready to download. A designer will call to schedule the free in-home measure.'
});
}