fix(ui): correct formula pane positioning, remove excessive tab-panel top padding and add shelf life inline

This commit is contained in:
2026-08-18 08:18:03 -05:00
parent 681955174f
commit e3dad6be97
7 changed files with 390 additions and 13 deletions
+98
View File
@@ -0,0 +1,98 @@
const fs = require('fs');
async function main() {
const pages = await fetch('http://localhost:9222/json').then(r => r.json());
const meezPage = pages.find(p => p.url.includes('getmeez.com/recipes/'));
if (!meezPage) return console.log('No recipe page found');
const ws = new WebSocket(meezPage.webSocketDebuggerUrl);
let id = 1;
const send = (method, params = {}) => new Promise((resolve, reject) => {
const msgId = id++;
const handler = (event) => {
const res = JSON.parse(event.data);
if (res.id === msgId) {
ws.removeEventListener('message', handler);
if (res.error) reject(res.error);
else resolve(res.result);
}
};
ws.addEventListener('message', handler);
ws.send(JSON.stringify({ id: msgId, method, params }));
});
ws.addEventListener('open', async () => {
try {
const evalRes = await send('Runtime.evaluate', {
expression: `(() => {
const metaItem = document.querySelector('[data-testid="metaItem"]');
if (!metaItem) return { error: 'metaItem not found' };
const rect = metaItem.getBoundingClientRect();
const parent = metaItem.parentElement;
const grandParent = parent ? parent.parentElement : null;
// Find recipe title
const title = document.querySelector('h1, [data-testid*="title"], [data-testid*="Title"]');
const titleRect = title ? title.getBoundingClientRect() : null;
// Find ingredients table / container
const ingContainer = document.querySelector('[data-testid*="Ingredients"], [data-testid*="RecipeTable"], table, [role="table"]');
const ingRect = ingContainer ? ingContainer.getBoundingClientRect() : null;
// Collect siblings of metaItem
const siblings = parent ? Array.from(parent.children).map(c => ({
tag: c.tagName,
class: c.className,
testId: c.getAttribute('data-testid'),
rect: c.getBoundingClientRect(),
text: c.innerText?.slice(0, 100)
})) : [];
// Collect ancestors up to body
const ancestors = [];
let cur = metaItem;
while (cur && cur !== document.body) {
ancestors.push({
tag: cur.tagName,
class: cur.className,
testId: cur.getAttribute('data-testid'),
rect: cur.getBoundingClientRect(),
style: {
display: window.getComputedStyle(cur).display,
position: window.getComputedStyle(cur).position,
padding: window.getComputedStyle(cur).padding,
margin: window.getComputedStyle(cur).margin,
gap: window.getComputedStyle(cur).gap
}
});
cur = cur.parentElement;
}
return {
metaItemRect: rect,
titleRect,
ingRect,
titleHtml: title?.outerHTML,
parentHtml: parent?.outerHTML?.slice(0, 500),
siblings,
ancestors
};
})()`,
returnByValue: true
});
fs.writeFileSync('scratch/meez_page_layout.json', JSON.stringify(evalRes.result.value, null, 2));
console.log('Saved to scratch/meez_page_layout.json');
console.log('Ancestors count:', evalRes.result.value.ancestors?.length);
console.log('Siblings:\n', evalRes.result.value.siblings?.map(s => `${s.tag} (${s.testId || s.class}): ${s.text?.slice(0, 40)}`));
ws.close();
} catch (e) {
console.error(e);
ws.close();
}
});
}
main().catch(console.error);
+74
View File
@@ -0,0 +1,74 @@
const fs = require('fs');
async function main() {
const pages = await fetch('http://localhost:9222/json').then(r => r.json());
const meezPage = pages.find(p => p.url.includes('getmeez.com/recipes/'));
if (!meezPage) return console.log('No recipe page found');
const ws = new WebSocket(meezPage.webSocketDebuggerUrl);
let id = 1;
const send = (method, params = {}) => new Promise((resolve, reject) => {
const msgId = id++;
const handler = (event) => {
const res = JSON.parse(event.data);
if (res.id === msgId) {
ws.removeEventListener('message', handler);
if (res.error) reject(res.error);
else resolve(res.result);
}
};
ws.addEventListener('message', handler);
ws.send(JSON.stringify({ id: msgId, method, params }));
});
ws.addEventListener('open', async () => {
try {
const evalRes = await send('Runtime.evaluate', {
expression: `(() => {
// Get the entire recipe layout tree from the main container
const main = document.querySelector('[data-testid="RoutesBase"]') || document.querySelector('main') || document.querySelector('#root');
const header = document.querySelector('[data-testid*="Header"], header');
const title = document.querySelector('h1, [data-testid*="EditableEntityNameBase_name"]');
const titleParent = title ? title.closest('[data-testid*="Header"], div') : null;
return {
titleParentHtml: titleParent ? {
tag: titleParent.tagName,
class: titleParent.className,
testId: titleParent.getAttribute('data-testid'),
html: titleParent.outerHTML,
style: {
padding: window.getComputedStyle(titleParent).padding,
margin: window.getComputedStyle(titleParent).margin
}
} : null,
leftColumnChildren: (() => {
const leftCol = document.querySelector('[data-testid="RoutesBase_section"]');
if (!leftCol) return null;
return Array.from(leftCol.children).map(c => ({
tag: c.tagName,
class: c.className,
testId: c.getAttribute('data-testid'),
text: c.innerText?.slice(0, 80),
html: c.outerHTML.slice(0, 200)
}));
})()
};
})()`,
returnByValue: true
});
fs.writeFileSync('scratch/meez_structure.json', JSON.stringify(evalRes.result.value, null, 2));
console.log('Structure saved.');
console.log(JSON.stringify(evalRes.result.value, null, 2));
ws.close();
} catch (e) {
console.error(e);
ws.close();
}
});
}
main().catch(console.error);
+148
View File
@@ -0,0 +1,148 @@
{
"metaItemRect": {},
"titleRect": {},
"ingRect": {},
"titleHtml": "<h1 class=\"MuiTypography-root MuiTypography-body1 css-1vpmx5a\" data-testid=\"EditableEntityNameBase_name\">Marinated Pork Belly</h1>",
"parentHtml": "<div class=\"MuiGrid2-root MuiGrid2-container MuiGrid2-direction-xs-row jss545 css-wk7muj\"><div class=\"MuiGrid2-root MuiGrid2-container MuiGrid2-direction-xs-row jss837 css-ts6rjh\" data-testid=\"metaItem\"><div class=\"MuiGrid2-root MuiGrid2-container MuiGrid2-direction-xs-row MuiGrid2-grid-sm-auto MuiGrid2-grid-xs-12 jss838 css-19lmllf\"><span class=\"MuiTypography-root MuiTypography-body1 jss839 css-1cmckbm\">Batch Size:</span><button class=\"MuiButtonBase-root MuiButton-root jss851 MuiButton-text Mui",
"siblings": [
{
"tag": "DIV",
"class": "MuiGrid2-root MuiGrid2-container MuiGrid2-direction-xs-row jss837 css-ts6rjh",
"testId": "metaItem",
"rect": {},
"text": "Batch Size:\n1x\nYield:\n\n2000\n\ng"
}
],
"ancestors": [
{
"tag": "DIV",
"class": "MuiGrid2-root MuiGrid2-container MuiGrid2-direction-xs-row jss837 css-ts6rjh",
"testId": "metaItem",
"rect": {},
"style": {
"display": "flex",
"position": "static",
"padding": "0px",
"margin": "0px 0px 32px",
"gap": "0px"
}
},
{
"tag": "DIV",
"class": "MuiGrid2-root MuiGrid2-container MuiGrid2-direction-xs-row jss545 css-wk7muj",
"testId": null,
"rect": {},
"style": {
"display": "flex",
"position": "static",
"padding": "0px",
"margin": "0px",
"gap": "0px"
}
},
{
"tag": "DIV",
"class": "MuiGrid2-root MuiGrid2-direction-xs-row MuiGrid2-grid-xs-12 css-j5005a",
"testId": "SummaryComponent",
"rect": {},
"style": {
"display": "block",
"position": "static",
"padding": "0px",
"margin": "0px",
"gap": "normal"
}
},
{
"tag": "DIV",
"class": "jss414",
"testId": "ItemsBase",
"rect": {},
"style": {
"display": "block",
"position": "static",
"padding": "0px",
"margin": "0px",
"gap": "normal"
}
},
{
"tag": "DIV",
"class": "jss345 jss346 jss343 jss356",
"testId": "RoutesBase_section",
"rect": {},
"style": {
"display": "flex",
"position": "relative",
"padding": "0px 36px 35px",
"margin": "0px",
"gap": "normal"
}
},
{
"tag": "DIV",
"class": "MuiGrid2-root MuiGrid2-container MuiGrid2-direction-xs-row MuiGrid2-grid-md-6 jss358 jss359 css-1tpndcg",
"testId": null,
"rect": {},
"style": {
"display": "flex",
"position": "static",
"padding": "16px 0px 0px",
"margin": "0px",
"gap": "0px"
}
},
{
"tag": "DIV",
"class": "MuiGrid2-root MuiGrid2-container MuiGrid2-direction-xs-row jss344 css-1f11l0v",
"testId": "RoutesBase",
"rect": {},
"style": {
"display": "flex",
"position": "static",
"padding": "0px 0px 16px",
"margin": "0px",
"gap": "0px"
}
},
{
"tag": "DIV",
"class": "MuiGrid2-root MuiGrid2-container MuiGrid2-direction-xs-row css-8sx0qx",
"testId": null,
"rect": {},
"style": {
"display": "flex",
"position": "static",
"padding": "0px",
"margin": "0px",
"gap": "0px"
}
},
{
"tag": "DIV",
"class": "",
"testId": null,
"rect": {},
"style": {
"display": "flex",
"position": "static",
"padding": "0px",
"margin": "0px",
"gap": "normal"
}
},
{
"tag": "DIV",
"class": "",
"testId": null,
"rect": {},
"style": {
"display": "flex",
"position": "static",
"padding": "0px",
"margin": "0px",
"gap": "normal"
}
}
]
}
+21
View File
@@ -0,0 +1,21 @@
{
"titleParentHtml": {
"tag": "DIV",
"class": "MuiGrid2-root MuiGrid2-direction-xs-row css-1n5khr6",
"testId": "RecipeName",
"html": "<div class=\"MuiGrid2-root MuiGrid2-direction-xs-row css-1n5khr6\" data-testid=\"RecipeName\"><h1 class=\"MuiTypography-root MuiTypography-body1 css-1vpmx5a\" data-testid=\"EditableEntityNameBase_name\">Marinated Pork Belly</h1><span class=\"MuiTypography-root MuiTypography-body1 jss543 css-svpgt\" data-testid=\"EditableEntityName_text\">nicholas ward</span></div>",
"style": {
"padding": "0px",
"margin": "0px"
}
},
"leftColumnChildren": [
{
"tag": "DIV",
"class": "jss414",
"testId": "ItemsBase",
"text": "Batch Size:\n1x\nYield:\n\n2000\n\ng\n\nCut & Portion the Pork\n\n\n\n1800\n\ng\n\n\tSous Vide Po",
"html": "<div data-testid=\"ItemsBase\" class=\"jss414\"><div class=\"MuiGrid2-root MuiGrid2-direction-xs-row MuiGrid2-grid-xs-12 css-j5005a\" id=\"recipe-summary\" data-testid=\"SummaryComponent\"><div class=\"MuiGrid2-"
}
]
}
+1 -1
View File
@@ -239,7 +239,7 @@ const recipeTabIcon=(name:string)=>`<span class="recipe-tab-icon"><svg viewBox="
) : (
<section class="recipe-view-formula-pane recipe-tab-panel active" data-recipe-panel="formula">
<div class="recipe-view-formula">
<RecipeCalculator client:load components={calculatorComponents} units={Object.fromEntries(unitMap)} basisUnitId={domainRecipe.scaling?.basis_amount?.unit_id??domainRecipe.yield.amount.unit_id} yieldQuantity={domainRecipe.yield.amount.quantity} yieldUnitId={domainRecipe.yield.amount.unit_id} yieldConversions={domainRecipe.measure_conversions??[]} mode={domainRecipe.scaling?.mode??"amount"} nutrition={nutrition} cost={cost} servings={domainRecipe.yield.servings} showDerived={false} showPercentControls={false}/>
<RecipeCalculator client:load components={calculatorComponents} units={Object.fromEntries(unitMap)} basisUnitId={domainRecipe.scaling?.basis_amount?.unit_id??domainRecipe.yield.amount.unit_id} yieldQuantity={domainRecipe.yield.amount.quantity} yieldUnitId={domainRecipe.yield.amount.unit_id} yieldConversions={domainRecipe.measure_conversions??[]} mode={domainRecipe.scaling?.mode??"amount"} nutrition={nutrition} cost={cost} servings={domainRecipe.yield.servings} showDerived={false} showPercentControls={false} shelfLife={shelfLife}/>
</div>
</section>
)}
+26 -6
View File
@@ -19,6 +19,7 @@ type Props = {
showDerived?: boolean;
showPercentControls?: boolean;
yieldConversions?: CalculatorItem["measureConversions"];
shelfLife?: string | { duration?: { quantity?: number; unit_id?: string }; storage_condition?: string };
};
function itemUnits(item: CalculatorItem, units: Record<string, Unit>) {
@@ -47,13 +48,26 @@ function convertItem(quantity: number, fromUnitId: string, toUnitId: string, ite
throw new Error(`No reviewed equivalency from ${fromUnitId} to ${toUnitId}`);
}
export default function RecipeCalculator({ components, units, basisUnitId, yieldQuantity, yieldUnitId, nutrition, cost, servings, showDerived = true, showPercentControls = true, yieldConversions = [] }: Props) {
export default function RecipeCalculator({ components, units, basisUnitId, yieldQuantity, yieldUnitId, nutrition, cost, servings, showDerived = true, showPercentControls = true, yieldConversions = [], shelfLife }: Props) {
const [factor, setFactor] = useState(1);
const [calculatePercent,setCalculatePercent]=useState(showPercentControls);
const [percentMode,setPercentMode]=useState<"standard"|"bakers">("standard");
const [yieldDisplayUnitId, setYieldDisplayUnitId] = useState(yieldUnitId);
const [lineUnits, setLineUnits] = useState<Record<string, string>>({});
const validFactor = Number.isFinite(factor) && factor >= 0 ? factor : 0;
const shelfLifeText = useMemo(() => {
if (!shelfLife) return null;
if (typeof shelfLife === "string") return shelfLife.trim() || null;
const q = shelfLife.duration?.quantity;
const u = shelfLife.duration?.unit_id;
if (q != null && u) {
const unitLabel = q === 1 ? u : `${u}s`;
return `${q} ${unitLabel}`;
}
return null;
}, [shelfLife]);
useEffect(() => { window.dispatchEvent(new CustomEvent("recipe-scale-change", { detail:validFactor })); }, [validFactor]);
const itemWeight=(item:CalculatorItem)=>{try{return convertItem(item.amount.quantity,item.amount.unit_id,"gram",item,units);}catch{return undefined;}};
const total = useMemo(
@@ -77,8 +91,8 @@ export default function RecipeCalculator({ components, units, basisUnitId, yield
return (
<section class="calculator" aria-labelledby="formula-heading">
<div class="calculator-heading">
<label class="basis-input batch-multiplier-field">
<div class="calculator-heading" data-testid="metaItem">
<div class="basis-input batch-multiplier-field">
<span class="scale-label">Batch Size:</span>
<span class="recipe-scale-control batch-size-control">
<input
@@ -91,8 +105,8 @@ export default function RecipeCalculator({ components, units, basisUnitId, yield
/>
<span class="batch-unit-suffix">x</span>
</span>
</label>
<label class="basis-input finished-yield-field">
</div>
<div class="basis-input finished-yield-field">
<span class="scale-label">Yield:</span>
<span class="recipe-scale-control quantity-control">
<input
@@ -123,7 +137,13 @@ export default function RecipeCalculator({ components, units, basisUnitId, yield
</svg>
</button>
</span>
</label>
</div>
{shelfLifeText && (
<div class="basis-input shelf-life-field">
<span class="scale-label">Shelf Life:</span>
<span class="shelf-life-value">{shelfLifeText}</span>
</div>
)}
{showPercentControls && (
<div class="calculator-percent-controls">
{calculatePercent && (
+22 -6
View File
@@ -2878,13 +2878,21 @@ input[type="search"]::-webkit-search-results-decoration,
border: 0 !important;
}
.application-body .recipe-view-details > .recipe-tab-panel.active,
.application-body .ingredient-view-details > .entity-tab-panel.active,
.application-body .recipe-tab-panel.active,
.application-body .entity-tab-panel.active {
.application-body .ingredient-view-details > .entity-tab-panel.active {
display: block;
padding-top: 30px !important;
margin: 0 !important;
}
.application-body .recipe-view-formula-pane.recipe-tab-panel.active,
.application-body .ingredient-main-content.entity-tab-panel.active,
.application-body .recipe-tab-panel.active,
.application-body .entity-tab-panel.active {
display: block;
margin: 0 !important;
}
.application-body .recipe-view-formula-pane.recipe-tab-panel.active {
padding-top: 0 !important;
}
.application-body .immersive-main .recipe-read-shell .recipe-view-formula .calculator { margin-top:0; }
/* Formula meta row: Batch and Yield controls matching Meez style. */
@@ -2895,8 +2903,8 @@ input[type="search"]::-webkit-search-results-decoration,
align-items: center !important;
justify-content: flex-start !important;
flex-wrap: nowrap !important;
gap: 24px !important;
margin: 0 0 28px 0 !important;
gap: 16px !important;
margin: 0 0 32px 0 !important;
padding: 0 !important;
width: 100% !important;
overflow-x: auto !important;
@@ -2910,7 +2918,7 @@ input[type="search"]::-webkit-search-results-decoration,
display: inline-flex !important;
align-items: center !important;
gap: 0 !important;
margin: 0 !important;
margin: 0 12px 0 0 !important;
padding: 2px 6px 2px 0 !important;
color: #050841 !important;
font-family: var(--meez-font-sans) !important;
@@ -2922,6 +2930,14 @@ input[type="search"]::-webkit-search-results-decoration,
flex-shrink: 0 !important;
}
.application-body .recipe-view-formula .calculator-heading .shelf-life-value {
font-family: var(--meez-font-sans) !important;
font-size: 14px !important;
font-weight: 400 !important;
color: #050841 !important;
white-space: nowrap !important;
}
.application-body .recipe-view-formula .calculator-heading .scale-label,
.application-body .immersive-main .recipe-read-shell .recipe-view-formula .calculator-heading .scale-label {
display: inline-block !important;