Compare commits

...
48 Commits
Author SHA1 Message Date
nicholas 1be6be2be8 fix(recipe-editor): allow decimal input for quantities and percentages; fix workspace filter alignment and mobile bottom-sheet modal 2026-08-18 16:58:45 -05:00
nicholas 7c6563deda fix(filter): eliminate overflow scroll for filter popout on smaller screens 2026-08-18 14:11:03 -05:00
nicholas 5dbdf1edde style(home): polish pill and filter hover effects across workspaces, filter menu, rule chips, and options 2026-08-18 13:45:16 -05:00
nicholas 8c3926e6c7 feat(filter): remove created by and prep stations, implement ingredient filter with recipe counts and search 2026-08-18 13:40:48 -05:00
nicholas 974d99141e fix(filter): ensure tag filtering with empty search or root URL properly displays matching items 2026-08-18 13:34:14 -05:00
nicholas d28faf59a1 fix(filter): ensure empty filter chips disappear on close and persist only with active selections 2026-08-18 13:30:22 -05:00
nicholas 1aaab51dc4 feat(filter): wire up tag filtering with counts and search for recipes and ingredients 2026-08-18 13:26:12 -05:00
nicholas 15afcc5c60 feat(filter): implement full Meez filter system with dynamic filter rule chips and popovers 2026-08-18 13:19:38 -05:00
nicholas 162c180ce4 feat(filter): update filter SVG with exact 3-path live Meez vector and computed styles 2026-08-18 13:11:08 -05:00
nicholas 9062a013dd feat(filter): implement unified single Meez filter with exact context-aware options and styling 2026-08-18 12:58:53 -05:00
nicholas c07342eb38 fix(directory): reduce 3-dot menu size and unify single filter button with exact Meez SVG 2026-08-18 12:48:14 -05:00
nicholas 189fed7168 feat(home): place archive and data management links above workspace pills 2026-08-18 12:41:03 -05:00
nicholas 063b0ca591 fix(layout): fix data management header obscuring heading and place secondary links below workspace pills 2026-08-18 12:39:31 -05:00
nicholas affdfc06c1 chore: clean up unimported utility-bar.css and ignore scratch test artifacts 2026-08-18 12:32:14 -05:00
nicholas 04dd5d2ed1 fix(icons): restore exact Meez apple SVG glyph and viewBox for mobile formula tab 2026-08-18 12:25:38 -05:00
nicholas 24ddfe8418 fix(tabs): eliminate layout shift on tab hover by fixing selector specificity 2026-08-18 12:21:22 -05:00
nicholas 4a9fa03797 fix(styles): optimize tab padding and remove legacy overrides for 13in laptop screens 2026-08-18 12:19:08 -05:00
nicholas 6a70b3c2c2 fix(icons): update nutrition tab icon to Meez heart-with-pulse and formula to apple 2026-08-18 12:11:40 -05:00
nicholas 0afedefad0 fix(icons, tooltip): standardize nutrition 24x24 heart icon and position UoM tooltip to stay on-page 2026-08-18 12:07:00 -05:00
nicholas f010ae60a0 feat(icons): extract exact SVG icons for tabs, entity types, and actions into centralized icons module 2026-08-18 12:02:40 -05:00
nicholas 3840200a7c fix(scaling): set scale labels to grey and restore clean grey input baseline underlines matching Meez 2026-08-18 11:56:52 -05:00
nicholas 3f040bab23 fix(scaling): remove blue underlines on yield/batch inputs and remove dropdown arrow on unit select 2026-08-18 09:30:00 -05:00
nicholas 56db8d9278 fix(cost): render SVG ingredient and sub-recipe circular badge icons in Recipe Cost ledger 2026-08-18 09:27:19 -05:00
nicholas d39962797b refactor(css): refine modular domain stylesheets with verified pixel-perfect parity 2026-08-18 09:23:12 -05:00
nicholas 632ade0ff5 refactor(css): consolidate and modularize stylesheets with 100% pixel-perfect verified parity 2026-08-18 09:17:48 -05:00
nicholas eaff204c3a fix(tabs): maintain constant icon color (#050841) across all tab states matching Meez 2026-08-18 09:00:29 -05:00
nicholas 506bd20c21 fix(method): compact prep method step height and list density in view and edit modes to match Meez 2026-08-18 08:59:21 -05:00
nicholas fcb7d52b3b chore: remove scratch png 2026-08-18 08:56:38 -05:00
nicholas 045cc254c2 fix(tabs): ensure Prep Method is selected on initial recipe load and replicate Meez dark tab hover styling 2026-08-18 08:56:35 -05:00
nicholas bebab112fc feat(ui): auto-size batch size and yield inputs snug to content with click-to-highlight and pointer cursor 2026-08-18 08:50:31 -05:00
nicholas 0e3ff8f74f fix(costing): remove spacer columns and make Ingredient / Sub-Recipe ( Expand all | Collapse all ) header inline 2026-08-18 08:48:05 -05:00
nicholas 8757a66edd feat(scaling): implement Meez-styled batch size select dropdown with presets and custom multiplier option 2026-08-18 08:46:25 -05:00
nicholas 9feb81957c fix(costing): hide edit links and pencil icon in cost ledger during view mode 2026-08-18 08:43:52 -05:00
nicholas 1c9941ecd2 fix(ui): ensure white-space normal and word wrapping on tooltips to prevent text leaking 2026-08-18 08:42:54 -05:00
nicholas be80a41504 fix(ui): remove overflow-x auto from calculator heading, anchor scale tooltip cleanly, and match Meez round cost warning icons 2026-08-18 08:41:10 -05:00
nicholas fe4ac5c637 feat(ui): add rich Meez-styled cost calculation and line item tooltips, fix scale tooltip positioning 2026-08-18 08:37:53 -05:00
nicholas 13379102a8 chore: remove inspection script 2026-08-18 08:32:57 -05:00
nicholas 3e9354febf feat(ui): implement Meez-styled interactive tooltips for scaling help, attention warnings, and UoM equivalency 2026-08-18 08:32:54 -05:00
nicholas 6d46241de6 chore: clean up obsolete CSS rules, remove debug scratch files, and fix MCP type check 2026-08-18 08:24:16 -05:00
nicholas ec3428417f fix(ui): remove legacy display:none on first-child and fix inline flow of Batch Size and Yield row 2026-08-18 08:21:53 -05:00
nicholas e3dad6be97 fix(ui): correct formula pane positioning, remove excessive tab-panel top padding and add shelf life inline 2026-08-18 08:18:03 -05:00
nicholas 681955174f feat(ui): match Meez inline Batch Size, Yield row and HelperTooltipBase_help_icon 2026-08-18 07:59:30 -05:00
nicholas 7c35b1d4d4 style(ui): match Meez Batch size and Yield area row layout, underline inputs, and calculate toggle 2026-08-18 07:55:09 -05:00
nicholas 82d7f51914 fix(ui): add 16px horizontal margins and padding for ingredient view mode on mobile 2026-08-18 07:49:47 -05:00
nicholas 37e73497b8 refactor(ui): remove avatar icon and unused avatar styles across all views 2026-08-18 07:46:11 -05:00
nicholas 03d3e04e96 fix(ui): remove double left padding on detail-context in mobile navigation bar 2026-08-18 07:42:51 -05:00
nicholas d039c97b53 fix(ui): restore and style search bar and new actions menu in detail utility navigation on mobile 2026-08-18 07:38:57 -05:00
nicholas 301357eaf1 feat(ingest): extract unnumbered procedure headings and complete multi-stage steps including Fig Bars assembly 2026-08-18 07:09:31 -05:00
30 changed files with 7879 additions and 5039 deletions
+2
View File
@@ -36,3 +36,5 @@ Thumbs.db
# Local application databases, backups, logs, and SQLite sidecars
/var/
file.json
database.sqlite
/scratch/
+51
View File
@@ -0,0 +1,51 @@
import fs from "node:fs";
const doc = JSON.parse(fs.readFileSync("file.json", "utf8"));
function isProcedureHeader(text, label) {
if (label !== "section_header") return false;
const s = text.trim();
if (/^chef's notes?:?$/i.test(s)) return false;
if (/^table of contents|foreword|introduction|acknowledgements|how to use/i.test(s)) return false;
return /procedure|assembly|variations?|baking|shaping|finishing|glaz|infusion/i.test(s) || s.endsWith(":");
}
function cleanProcedureHeader(raw) {
const t = raw.trim().replace(/^['"]+|['":]+$/g, "").trim();
if (/^procedure$/i.test(t)) return "";
const cleaned = t.replace(/\s+procedure$/i, "").trim();
return cleaned ? `${cleaned}:` : "";
}
function testPages(pages, title) {
const texts = doc.texts.filter(t => pages.includes(t.prov?.[0]?.page_no));
console.log(`\nTesting on ${title} (pages ${pages.join("-")}):`);
let inProcedure = false;
const steps = [];
for (const t of texts) {
const text = t.text.trim();
if (/^chef's notes?:?$/i.test(text)) {
inProcedure = false;
continue;
}
if (isProcedureHeader(text, t.label)) {
inProcedure = true;
const heading = cleanProcedureHeader(text);
if (heading) {
steps.push({ id: `step_${steps.length + 1}`, order: steps.length + 1, instruction: heading });
}
continue;
}
if (inProcedure && (t.label === "list_item" || t.label === "text") && !/^\d+$/.test(text) && !/^yields?:/i.test(text)) {
steps.push({ id: `step_${steps.length + 1}`, order: steps.length + 1, instruction: text });
}
}
console.log("Extracted steps count:", steps.length);
steps.forEach(s => console.log(" " + (s.instruction.endsWith(":") ? "📂 " : " • ") + s.instruction));
}
testPages([312, 313], "Fig Bars");
testPages([26, 27], "Chocolate Puff Pastry");
+49 -22
View File
@@ -544,8 +544,23 @@ function parseYieldServings(yieldText) {
return null;
}
function isProcedureHeader(text, label, recipeTitle) {
if (label !== "section_header") return false;
const s = text.trim();
if (recipeTitle && s.toLowerCase() === recipeTitle.toLowerCase()) return false;
if (/^chef's notes?:?$/i.test(s)) return false;
if (/^table of contents|foreword|introduction|acknowledgements|how to use/i.test(s)) return false;
return /procedure|assembly|variations?|baking|shaping|finishing|glaz|infusion/i.test(s) || s.endsWith(":");
}
function cleanProcedureHeader(raw) {
const t = raw.trim().replace(/^['"]+|['":]+$/g, "").trim();
if (/^procedure$/i.test(t)) return "";
const cleaned = t.replace(/\s+procedure$/i, "").trim();
return cleaned ? `${cleaned}:` : "";
}
const steps = [];
let currentSectionPrefix = "";
let inProcedure = false;
let inChefNotes = false;
let yieldDescription = null;
@@ -560,30 +575,35 @@ function parseYieldServings(yieldText) {
continue;
}
// Match procedure section headers like "Dough Packet Procedure:", "Assembly Procedure:", "Procedure:"
if (/procedure:?$/i.test(text)) {
inProcedure = true;
inChefNotes = false;
currentSectionPrefix = text.replace(/procedure:?$/i, "").trim();
continue;
}
if (/^chef's notes:?$/i.test(text)) {
if (/^chef's notes?:?$/i.test(text)) {
inProcedure = false;
inChefNotes = true;
continue;
}
if (isProcedureHeader(text, textNode.label, title)) {
inProcedure = true;
inChefNotes = false;
const heading = cleanProcedureHeader(text);
if (heading) {
steps.push({
id: `step_${steps.length + 1}`,
order: steps.length + 1,
instruction: heading,
equipment_ids: [],
});
}
continue;
}
if (inProcedure) {
if (textNode.label === "list_item" || textNode.label === "text") {
if (!/^\d+$/.test(text)) {
const prefix = currentSectionPrefix ? `[${currentSectionPrefix}] ` : "";
steps.push({
id: `step_${steps.length + 1}`,
order: steps.length + 1,
instruction: `${prefix}${text}`,
equipment_ids: inferEquipment(text),
});
}
if ((textNode.label === "list_item" || textNode.label === "text") && !/^\d+$/.test(text) && !/^yields?:/i.test(text)) {
steps.push({
id: `step_${steps.length + 1}`,
order: steps.length + 1,
instruction: text,
equipment_ids: inferEquipment(text),
});
}
} else if (inChefNotes) {
if (textNode.label === "list_item") {
@@ -685,10 +705,17 @@ async function main() {
}
}
console.log(`\n Procedure (${recipe.steps.length} steps):`);
console.log(`\n Procedure:`);
let stepNum = 1;
for (const step of recipe.steps) {
const eq = step.equipment_ids.length > 0 ? ` [Equip: ${step.equipment_ids.join(", ")}]` : "";
console.log(` ${step.order}. ${step.instruction}${eq}`);
const isHeading = step.instruction.endsWith(":");
if (isHeading) {
console.log(`\n 📂 ${step.instruction}`);
stepNum = 1;
} else {
const eq = step.equipment_ids.length > 0 ? ` [Equip: ${step.equipment_ids.join(", ")}]` : "";
console.log(` ${stepNum++}. ${step.instruction}${eq}`);
}
}
if (recipe.notes.length > 0) {
+250 -36
View File
@@ -9,6 +9,7 @@ import {
} from "../../../lib/database";
import { titleCase } from "../../../lib/format";
import { readOnlyMode } from "../../../lib/runtime";
import { TYPE_ICONS, TYPE_ICON_TRANSFORMS, type EntityIconKey } from "../../../lib/icons";
const database = openDatabase({ readOnly: false });
if (!database) return Astro.redirect("/app/", 303);
@@ -138,14 +139,6 @@ function formatDeleteDate(dateStr: string | null) {
<header class="archive-header">
<div class="archive-header-left">
<nav class="archive-breadcrumbs">
<a href="/app/">
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
</svg>
<span>All items</span>
</a>
</nav>
<h1>Archive</h1>
<p class="archive-subtitle">
Restore recipes, ingredients, recipe books, and purchase items or permanently purge them.
@@ -211,26 +204,12 @@ function formatDeleteDate(dateStr: string | null) {
<div class="archive-type-cell">
<span class:list={["workspace-pill-icon", item.type]}>
{item.type === "recipe" && (
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M13.125 0C13.6428 0 14.0625 0.419733 14.0625 0.9375V14.0625C14.0625 14.5803 13.6428 15 13.125 15H0.9375C0.419733 15 0 14.5803 0 14.0625V0.9375C0 0.419733 0.419733 0 0.9375 0H13.125ZM12.1875 1.875H1.875V13.125H12.1875V1.875ZM11.25 9.375V11.25H5.625V9.375H11.25ZM4.6875 9.375V11.25H2.8125V9.375H4.6875ZM11.25 6.5625V8.4375H5.625V6.5625H11.25ZM4.6875 6.5625V8.4375H2.8125V6.5625H4.6875ZM11.25 3.75V5.625H5.625V3.75H11.25ZM4.6875 3.75V5.625H2.8125V3.75H4.6875Z" transform="scale(1.1, 1.1) translate(4px, 3.5px)"/>
</svg>
)}
{item.type === "ingredient" && (
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M7.57975 2.24434L7.65872 2.13161C8.13511 1.47697 8.63143 1.08241 9.16762 0.957718C9.57278 0.863497 9.96486 1.18135 10.0434 1.66766C10.1219 2.15397 9.85705 2.62459 9.45189 2.71881C9.18697 2.78042 8.8382 3.1513 8.44252 3.84957C8.78173 3.85543 9.10757 3.88576 9.42032 3.94078C11.9002 4.37705 13.577 7.45745 12.4331 11.6424C11.496 15.0703 9.68081 16.5977 7.229 15.682C6.85528 15.5611 6.58673 15.5046 6.4542 15.5046C6.40147 15.5046 6.36601 15.5163 6.32134 15.5491L6.15949 15.644C3.74207 16.7473 1.8122 15.2446 0.553846 11.7108C-0.274666 9.38411 -0.146354 7.48777 0.75651 6.03344C1.3667 5.05056 2.12543 4.48603 3.08872 4.01846C3.29048 3.92053 3.51746 3.85185 3.7715 3.81087C3.24172 3.20799 2.94401 2.30051 2.83382 1.12442L2.74286 0.153477L3.63312 0.0415436C5.52154 -0.195889 6.8746 0.590513 7.57975 2.24434ZM3.88234 5.71589C3.2053 6.04451 2.69949 6.42087 2.31601 7.03856C1.73369 7.97655 1.64642 9.26637 2.2885 11.0695C3.21226 13.6636 4.17073 14.4453 5.3377 13.9576C5.67319 13.7379 6.05207 13.6244 6.4542 13.6244C6.81471 13.6244 7.24894 13.7157 7.82562 13.9033C9.09594 14.3772 9.97129 13.6407 10.6554 11.1379C11.5187 7.9797 10.4582 6.03137 9.10636 5.79357C8.42613 5.6739 7.60929 5.71807 6.65655 5.93628L6.446 5.9845L6.2363 5.93257C4.98214 5.62199 4.16221 5.58004 3.88234 5.71589ZM4.69764 1.88235C4.80315 2.16337 4.92885 2.34891 5.06454 2.4469C5.26392 2.5909 5.53003 2.73516 5.86115 2.87726C5.58815 2.34716 5.20687 2.02464 4.69764 1.88235Z" transform="scale(1.3, 1.3) translate(2.5px, 1px)"/>
</svg>
)}
{item.type === "book" && (
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M14.0347 0V16H3.12658C1.50098 16 0.527344 15.0264 0.527344 13.4008V2.59923C0.527344 0.97364 1.50098 0 3.12658 0H14.0347ZM12.1594 12.6763L3.95747 12.6765C2.75227 12.6765 2.40226 12.9098 2.40226 13.4008C2.40226 13.9909 2.53647 14.1251 3.12658 14.1251H12.1598L12.1594 12.6763ZM12.1598 1.87492H3.12658C2.53647 1.87492 2.40226 2.00913 2.40226 2.59923L2.40185 10.9999C2.85223 10.8678 3.37428 10.8015 3.95747 10.8015L12.1594 10.8014L12.1598 1.87492ZM10.0768 3.80157V5.67649H4.45204V3.80157H10.0768Z" transform="scale(1.1, 1.1) translate(3.5px, 2.5px)"/>
</svg>
)}
{item.type === "purchase" && (
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M19.5 3.5 18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2 4.5 3.5 3 2v20l1.5-1.5L6 22l1.5-1.5L9 22l1.5-1.5L12 22l1.5-1.5L15 22l1.5-1.5L18 22l1.5-1.5L21 22V2l-1.5 1.5zM19 19.09H5V4.91h14v14.18zM6 15h12v2H6zm0-4h12v2H6zm0-4h12v2H6z"/>
</svg>
)}
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path
d={TYPE_ICONS[item.type as EntityIconKey]}
style={TYPE_ICON_TRANSFORMS[item.type as EntityIconKey] ? { transform: TYPE_ICON_TRANSFORMS[item.type as EntityIconKey] } : undefined}
/>
</svg>
</span>
</div>
@@ -350,6 +329,32 @@ function formatDeleteDate(dateStr: string | null) {
</script>
<style>
.archive-page {
background: #ffffff;
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
}
.archive-page :global(.detail-utility) {
position: sticky !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
z-index: 30 !important;
background: #ffffff !important;
border-bottom: 1px solid #edf0f5 !important;
}
.archive-workspace {
width: min(1120px, calc(100% - 48px));
margin: 0 auto;
padding: 32px 0 64px;
box-sizing: border-box;
}
.archive-error-banner {
background: #fef2f2;
color: #991b1b;
@@ -360,43 +365,215 @@ function formatDeleteDate(dateStr: string | null) {
font-size: 14px;
font-weight: 500;
}
.archive-table-head,
.archive-header {
margin-bottom: 24px;
}
.archive-header h1 {
margin: 0 0 6px;
color: #050841;
font-size: 28px;
font-weight: 700;
letter-spacing: -0.015em;
}
.archive-subtitle {
color: #8b93a7;
font-size: 14px;
margin: 0;
}
.archive-toolbar {
margin-bottom: 20px;
}
.archive-filter-chips {
display: flex;
align-items: center;
gap: 8px;
overflow-x: auto;
scrollbar-width: none;
}
.archive-chip {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 36px;
padding: 6px 14px;
background: #fff;
border: 1px solid #ececec;
border-radius: 999px;
color: rgba(0, 0, 0, 0.87);
font-size: 14px;
font-weight: 400;
text-decoration: none;
white-space: nowrap;
transition: all 0.15s ease;
}
.archive-chip:hover {
background: #fff;
border-color: #cfd5fa;
}
.archive-chip.active {
background: #DBE4FF;
border-color: #DBE4FF;
}
.archive-chip .chip-count {
color: #8b93a7;
font-size: 12px;
font-weight: 500;
}
.archive-table {
background: #fff;
border: 1px solid #eef0f5;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.archive-table-head {
display: grid;
grid-template-columns: 36px 40px minmax(0, 1fr) 180px 140px;
align-items: center;
min-height: 48px;
padding: 0 20px;
background: #ffffff;
border-bottom: 1px solid #edf0f5;
color: #8b93a7;
font-size: 13px;
font-weight: 500;
}
.archive-table-row {
display: grid;
grid-template-columns: 32px 34px minmax(16rem, 1fr) 10rem 140px;
grid-template-columns: 36px 40px minmax(0, 1fr) 180px 140px;
align-items: center;
gap: 16px;
min-height: 58px;
padding: 0 20px;
border-bottom: 1px solid #edf0f5;
transition: background-color 0.12s ease;
}
.archive-table-row:last-child {
border-bottom: 0;
}
.archive-table-row:hover {
background: #f8faff;
}
.archive-select-cell,
.head-col.select {
display: flex;
align-items: center;
justify-content: center;
}
.row-single-actions {
.archive-type-cell {
display: flex;
align-items: center;
}
.archive-name-cell {
display: flex;
flex-direction: column;
min-width: 0;
gap: 2px;
}
.archive-item-title {
color: #050841;
font-size: 15px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.archive-item-type {
color: #8b93a7;
font-size: 12px;
text-transform: capitalize;
}
.archive-date-cell {
color: #8b93a7;
font-size: 13px;
}
.archive-action-cell {
display: flex;
align-items: center;
gap: 8px;
justify-content: flex-end;
}
.archive-restore-btn {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 32px;
padding: 0 14px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 999px;
color: #3d5df6;
font-family: var(--meez-font-sans);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.archive-restore-btn:hover {
background: #f1f5fe;
border-color: #3d5df6;
}
.archive-delete-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid var(--line);
border: 1px solid #dfe3ec;
background: white;
color: var(--muted);
color: #8b93a7;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
}
.archive-delete-btn:hover {
border-color: #fca5a5;
background: #fef2f2;
color: #dc2626;
}
.workspace-pill-icon {
display: grid;
place-items: center;
width: 24px;
height: 24px;
background: #3c4679;
border-radius: 50%;
color: #fff;
}
.workspace-pill-icon.ingredient,
.workspace-pill-icon.purchase {
background: #3f908a;
}
.workspace-pill-icon.book {
background: #f3a642;
}
/* Floating Bar */
.archive-floating-bar {
position: fixed;
@@ -415,17 +592,20 @@ function formatDeleteDate(dateStr: string | null) {
font-size: 13px;
font-weight: 600;
}
.archive-floating-bar.hidden {
display: none;
}
.floating-bar-actions {
display: flex;
align-items: center;
gap: 10px;
}
.batch-restore-btn {
padding: 6px 14px;
background: var(--blue);
background: #3d5df6;
color: white;
border: none;
border-radius: 999px;
@@ -433,9 +613,11 @@ function formatDeleteDate(dateStr: string | null) {
font-size: 12px;
cursor: pointer;
}
.batch-restore-btn:hover {
background: var(--blue-hover);
background: #2b49db;
}
.batch-delete-btn {
padding: 6px 14px;
background: #dc2626;
@@ -446,7 +628,39 @@ function formatDeleteDate(dateStr: string | null) {
font-size: 12px;
cursor: pointer;
}
.batch-delete-btn:hover {
background: #b91c1c;
}
.archive-empty-state {
text-align: center;
padding: 64px 20px;
background: #fff;
border: 1px solid #eef0f5;
border-radius: 8px;
}
.empty-icon-circle {
display: grid;
place-items: center;
width: 56px;
height: 56px;
margin: 0 auto 16px;
background: #f1f5fe;
border-radius: 50%;
color: #3d5df6;
}
.archive-back-btn {
display: inline-block;
margin-top: 16px;
padding: 8px 18px;
background: #3d5df6;
color: white;
border-radius: 6px;
text-decoration: none;
font-weight: 500;
font-size: 14px;
}
</style>
+197 -45
View File
@@ -2,20 +2,8 @@
export const prerender = false;
import BaseLayout from "../../../layouts/BaseLayout.astro";
import EntityDirectory from "../../../components/EntityDirectory";
const TYPE_ICONS:Record<"recipe"|"ingredient"|"book"|"purchase"|"inventory",string> = {
recipe:"M13.125 0C13.6428 0 14.0625 0.419733 14.0625 0.9375V14.0625C14.0625 14.5803 13.6428 15 13.125 15H0.9375C0.419733 15 0 14.5803 0 14.0625V0.9375C0 0.419733 0.419733 0 0.9375 0H13.125ZM12.1875 1.875H1.875V13.125H12.1875V1.875ZM11.25 9.375V11.25H5.625V9.375H11.25ZM4.6875 9.375V11.25H2.8125V9.375H4.6875ZM11.25 6.5625V8.4375H5.625V6.5625H11.25ZM4.6875 6.5625V8.4375H2.8125V6.5625H4.6875ZM11.25 3.75V5.625H5.625V3.75H11.25ZM4.6875 3.75V5.625H2.8125V3.75H4.6875Z",
ingredient:"M7.57975 2.24434L7.65872 2.13161C8.13511 1.47697 8.63143 1.08241 9.16762 0.957718C9.57278 0.863497 9.96486 1.18135 10.0434 1.66766C10.1219 2.15397 9.85705 2.62459 9.45189 2.71881C9.18697 2.78042 8.8382 3.1513 8.44252 3.84957C8.78173 3.85543 9.10757 3.88576 9.42032 3.94078C11.9002 4.37705 13.577 7.45745 12.4331 11.6424C11.496 15.0703 9.68081 16.5977 7.229 15.682C6.85528 15.5611 6.58673 15.5046 6.4542 15.5046C6.40147 15.5046 6.36601 15.5163 6.32134 15.5491L6.15949 15.644C3.74207 16.7473 1.8122 15.2446 0.553846 11.7108C-0.274666 9.38411 -0.146354 7.48777 0.75651 6.03344C1.3667 5.05056 2.12543 4.48603 3.08872 4.01846C3.29048 3.92053 3.51746 3.85185 3.7715 3.81087C3.24172 3.20799 2.94401 2.30051 2.83382 1.12442L2.74286 0.153477L3.63312 0.0415436C5.52154 -0.195889 6.8746 0.590513 7.57975 2.24434ZM3.88234 5.71589C3.2053 6.04451 2.69949 6.42087 2.31601 7.03856C1.73369 7.97655 1.64642 9.26637 2.2885 11.0695C3.21226 13.6636 4.17073 14.4453 5.3377 13.9576C5.67319 13.7379 6.05207 13.6244 6.4542 13.6244C6.81471 13.6244 7.24894 13.7157 7.82562 13.9033C9.09594 14.3772 9.97129 13.6407 10.6554 11.1379C11.5187 7.9797 10.4582 6.03137 9.10636 5.79357C8.42613 5.6739 7.60929 5.71807 6.65655 5.93628L6.446 5.9845L6.2363 5.93257C4.98214 5.62199 4.16221 5.58004 3.88234 5.71589ZM4.69764 1.88235C4.80315 2.16337 4.92885 2.34891 5.06454 2.4469C5.26392 2.5909 5.53003 2.73516 5.86115 2.87726C5.58815 2.34716 5.20687 2.02464 4.69764 1.88235Z",
book:"M14.0347 0V16H3.12658C1.50098 16 0.527344 15.0264 0.527344 13.4008V2.59923C0.527344 0.97364 1.50098 0 3.12658 0H14.0347ZM12.1594 12.6763L3.95747 12.6765C2.75227 12.6765 2.40226 12.9098 2.40226 13.4008C2.40226 13.9909 2.53647 14.1251 3.12658 14.1251H12.1598L12.1594 12.6763ZM12.1598 1.87492H3.12658C2.53647 1.87492 2.40226 2.00913 2.40226 2.59923L2.40185 10.9999C2.85223 10.8678 3.37428 10.8015 3.95747 10.8015L12.1594 10.8014L12.1598 1.87492ZM10.0768 3.80157V5.67649H4.45204V3.80157H10.0768Z",
purchase:"M19.5 3.5 18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2 4.5 3.5 3 2v20l1.5-1.5L6 22l1.5-1.5L9 22l1.5-1.5L12 22l1.5-1.5L15 22l1.5-1.5L18 22l1.5-1.5L21 22V2l-1.5 1.5zM19 19.09H5V4.91h14v14.18zM6 15h12v2H6zm0-4h12v2H6zm0-4h12v2H6z",
inventory:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM7 10h2v7H7zm4-3h2v10h-2zm4 6h2v4h-2z"
};
const TYPE_ICON_TRANSFORMS:Record<"recipe"|"ingredient"|"book"|"purchase"|"inventory",string|undefined> = {
recipe:"scale(1.1, 1.1) translate(4px, 3.5px)",
ingredient:"scale(1.3, 1.3) translate(2.5px, 1px)",
book:"scale(1.1, 1.1) translate(3.5px, 2.5px)",
purchase:undefined,
inventory:undefined
};
import WorkspaceFilterBar from "../../../components/WorkspaceFilterBar";
import { TYPE_ICONS, TYPE_ICON_TRANSFORMS } from "../../../lib/icons";
import { openDatabase } from "../../../lib/database";
import { readOnlyMode } from "../../../lib/runtime";
import { titleCase } from "../../../lib/format";
@@ -28,11 +16,11 @@ import type {
const database = openDatabase();
if (!database) return new Response("Database unavailable", { status: 503 });
const recipes = database.prepare(`SELECT r.id,r.title,r.yield_quantity,r.yield_unit_id,
const recipes = database.prepare(`SELECT r.id,r.title,r.yield_quantity,r.yield_unit_id,r.tags_json,r.categories_json,r.station,
(SELECT count(*) FROM recipe_items ri WHERE ri.recipe_id=r.id) item_count,
(SELECT count(*) FROM recipe_steps rs WHERE rs.recipe_id=r.id AND rs.instruction LIKE 'TODO:%') placeholder_count
FROM recipes r WHERE r.deleted_at IS NULL ORDER BY r.title`).all() as unknown as DirectoryRecipeRow[];
const ingredients = database.prepare(`SELECT i.id,i.name,i.status,
const ingredients = database.prepare(`SELECT i.id,i.name,i.status,i.tags_json,i.categories_json,
(SELECT count(*) FROM recipe_items r WHERE r.ingredient_id=i.id) recipe_count,
(SELECT count(*) FROM price_observations po JOIN purchase_items p ON p.id=po.purchase_item_id WHERE p.ingredient_id=i.id) price_count,
(SELECT count(*) FROM source_mappings m WHERE m.subject_type='ingredient' AND m.subject_id=i.id AND m.mapping_type='nutrition' AND m.status='reviewed') nutrition_count
@@ -41,22 +29,183 @@ const books = database.prepare("SELECT c.id,c.name,c.description,(SELECT count(*
const purchases = database.prepare(`SELECT p.id,p.ingredient_id,p.name,p.supplier_id,p.status,i.name ingredient_name,p.package_quantity,p.package_unit_id,
(SELECT amount FROM price_observations x WHERE x.purchase_item_id=p.id ORDER BY effective_at DESC LIMIT 1) latest_price
FROM purchase_items p JOIN ingredients i ON i.id=p.ingredient_id ORDER BY p.name`).all() as unknown as DirectoryPurchaseRow[];
const recipeItemRows = database.prepare("SELECT recipe_id, ingredient_id FROM recipe_items WHERE ingredient_id IS NOT NULL").all() as { recipe_id: string; ingredient_id: string }[];
const inventoryCounts = database.prepare("SELECT count(*) as c FROM inventory_counts WHERE deleted_at IS NULL").get() as { c: number } | undefined;
database.close();
const recipeIngredientsMap = new Map<string, Set<string>>();
const ingredientRecipeCounts = new Map<string, number>();
recipeItemRows.forEach(({ recipe_id, ingredient_id }) => {
if (!recipeIngredientsMap.has(recipe_id)) {
recipeIngredientsMap.set(recipe_id, new Set());
}
const set = recipeIngredientsMap.get(recipe_id)!;
if (!set.has(ingredient_id)) {
set.add(ingredient_id);
ingredientRecipeCounts.set(ingredient_id, (ingredientRecipeCounts.get(ingredient_id) || 0) + 1);
}
});
const requested=Astro.url.searchParams.get("type");
if (requested === "inventory") return Astro.redirect("/app/inventory/", 303);
const type=["recipe","ingredient","book","purchase"].includes(requested??"")?requested:undefined;
const query=(Astro.url.searchParams.get("q")??"").trim();
const normalizedQuery=query.toLocaleLowerCase();
const validSearchTypes=["recipe","ingredient","book","purchase"];
const selectedSearchTypes=Astro.url.searchParams.getAll("item_type").filter((value)=>validSearchTypes.includes(value));
const filteringSearchTypes=selectedSearchTypes.length>0;
const attention=Astro.url.searchParams.get("attention")==="1", missingCost=Astro.url.searchParams.get("missing_cost")==="1", noUsda=Astro.url.searchParams.get("no_usda")==="1",unused=Astro.url.searchParams.get("unused")==="1",emptyRecipe=Astro.url.searchParams.get("empty_recipe")==="1",placeholderSteps=Astro.url.searchParams.get("placeholder_steps")==="1";
const filtering=attention||missingCost||noUsda||unused||emptyRecipe||placeholderSteps;
const filteredIngredients=ingredients.filter((ingredient)=>{if(!filtering)return true;const selected=[missingCost&&ingredient.price_count===0,noUsda&&ingredient.nutrition_count===0,unused&&ingredient.recipe_count===0].filter(Boolean);return missingCost||noUsda||unused?selected.length>0:ingredient.price_count===0||ingredient.nutrition_count===0||ingredient.recipe_count===0;});
const filteredRecipes=recipes.filter(recipe=>!filtering||(emptyRecipe&&recipe.item_count===0)||(placeholderSteps&&recipe.placeholder_count>0)||(!emptyRecipe&&!placeholderSteps&&(recipe.item_count===0||recipe.placeholder_count>0)));
const FILTER_ICON_PATH="M18 6.0201C18 4.81608 17.1873 3.79266 16.0736 3.46156L16.0736 0.722412C16.0736 0.301005 15.7425 -9.86801e-08 15.3211 -1.171e-07C14.8996 -1.35521e-07 14.5685 0.331105 14.5685 0.752512L14.5685 3.49166C13.4548 3.79266 12.6421 4.84618 12.6421 6.0502C12.6421 7.28432 13.4548 8.30774 14.5685 8.63884L14.5685 19.7459C14.5685 20.1673 14.8996 20.4984 15.3211 20.4984C15.7425 20.4984 16.0736 20.1673 16.0736 19.7459L16.0736 8.63884C17.1873 8.27764 18 7.25422 18 6.0201ZM16.495 6.0502C16.495 6.68231 15.9833 7.22412 15.3211 7.22412C14.6588 7.22412 14.1471 6.68231 14.1471 6.0502C14.1471 5.41809 14.6588 4.87628 15.3211 4.87628C15.9833 4.87628 16.495 5.41809 16.495 6.0502Z";
const selectedTags=Astro.url.searchParams.getAll("tag").map(t=>t.trim().toLowerCase()).filter(Boolean);
const selectedIngredients=Astro.url.searchParams.getAll("ingredient").map(i=>i.trim().toLowerCase()).filter(Boolean);
// Default to "recipe" when not in a global search
const type = ["recipe","ingredient","book","purchase"].includes(requested ?? "")
? requested
: (query ? undefined : "recipe");
// Recipe attention filters (Meez spec)
const emptyRecipe=Astro.url.searchParams.get("empty_recipe")==="1";
const missingYield=Astro.url.searchParams.get("missing_yield")==="1";
const placeholderSteps=Astro.url.searchParams.get("placeholder_steps")==="1";
const attentionRecipe=Astro.url.searchParams.get("attention")==="1";
const recipeFilterActive=emptyRecipe||missingYield||placeholderSteps||attentionRecipe;
// Ingredient attention filters (Meez spec)
const unused=Astro.url.searchParams.get("unused")==="1";
const missingCost=Astro.url.searchParams.get("missing_cost")==="1";
const noPurchase=Astro.url.searchParams.get("no_purchase")==="1" || Astro.url.searchParams.get("no_usda")==="1";
const attentionIngredient=Astro.url.searchParams.get("attention")==="1";
const ingredientFilterActive=unused||missingCost||noPurchase||attentionIngredient;
const recipeAttentionCounts = {
empty: recipes.filter(r => r.item_count === 0).length,
missingYield: recipes.filter(r => !r.yield_quantity || Number(r.yield_quantity) <= 0 || !r.yield_unit_id).length,
placeholder: recipes.filter(r => r.placeholder_count > 0).length
};
const ingredientAttentionCounts = {
unused: ingredients.filter(i => i.recipe_count === 0).length,
missingCost: ingredients.filter(i => i.price_count === 0).length,
noPurchase: ingredients.filter(i => i.price_count === 0 || i.nutrition_count === 0).length
};
const searchItemTypeCounts = {
recipe: recipes.filter(item => `${item.title} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery)).length,
ingredient: ingredients.filter(item => `${item.name} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery)).length,
book: books.filter(item => `${item.name} ${item.description??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery)).length,
purchase: purchases.filter(item => `${item.name} ${item.ingredient_name} ${item.supplier_id??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery)).length
};
function parseItemTags(tagsJson?: string, categoriesJson?: string): string[] {
const set = new Set<string>();
if (tagsJson) {
try {
const arr = JSON.parse(tagsJson);
if (Array.isArray(arr)) arr.forEach((t: string) => t && set.add(String(t).trim().toLowerCase()));
} catch {}
}
if (categoriesJson) {
try {
const arr = JSON.parse(categoriesJson);
if (Array.isArray(arr)) arr.forEach((c: string) => c && set.add(String(c).trim().toLowerCase()));
} catch {}
}
return Array.from(set);
}
// Compute tag counts
const activeDataset = type === "ingredient" ? ingredients : recipes;
const tagCountsMap = new Map<string, number>();
activeDataset.forEach((item) => {
const tags = parseItemTags(item.tags_json, item.categories_json);
tags.forEach((t) => {
tagCountsMap.set(t, (tagCountsMap.get(t) || 0) + 1);
});
});
const tagOptions = Array.from(tagCountsMap.entries())
.map(([tag, count]) => ({
id: tag,
name: titleCase(tag.replace(/_/g, " ")),
count,
checked: selectedTags.includes(tag)
}))
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
const ingredientOptions = ingredients
.filter(i => (ingredientRecipeCounts.get(i.id) || 0) > 0)
.map(i => ({
id: i.id,
name: titleCase(i.name),
count: ingredientRecipeCounts.get(i.id) || 0,
checked: selectedIngredients.includes(i.id)
}))
.sort((a, b) => (b.count ?? 0) - (a.count ?? 0) || a.name.localeCompare(b.name));
const currentParams = {
type: type ?? "",
q: query,
empty_recipe: Astro.url.searchParams.get("empty_recipe") ?? "",
missing_yield: Astro.url.searchParams.get("missing_yield") ?? "",
placeholder_steps: Astro.url.searchParams.get("placeholder_steps") ?? "",
unused: Astro.url.searchParams.get("unused") ?? "",
missing_cost: Astro.url.searchParams.get("missing_cost") ?? "",
no_purchase: Astro.url.searchParams.get("no_purchase") ?? "",
no_usda: Astro.url.searchParams.get("no_usda") ?? "",
item_type: selectedSearchTypes,
tag: selectedTags,
ingredient: selectedIngredients
};
const filteredIngredients=ingredients.filter((ingredient)=>{
if (ingredientFilterActive) {
const conditions = [
unused && ingredient.recipe_count === 0,
missingCost && ingredient.price_count === 0,
noPurchase && (ingredient.price_count === 0 || ingredient.nutrition_count === 0)
];
const matchesAttention = (unused || missingCost || noPurchase)
? conditions.some(Boolean)
: (ingredient.recipe_count === 0 || ingredient.price_count === 0 || ingredient.nutrition_count === 0);
if (!matchesAttention) return false;
}
if (selectedTags.length > 0) {
const tags = parseItemTags(ingredient.tags_json, ingredient.categories_json);
const matchesTags = selectedTags.some(t => tags.includes(t));
if (!matchesTags) return false;
}
return true;
});
const filteredRecipes=recipes.filter((recipe)=>{
if (recipeFilterActive) {
const isMissingYield = !recipe.yield_quantity || Number(recipe.yield_quantity) <= 0 || !recipe.yield_unit_id;
const conditions = [
emptyRecipe && recipe.item_count === 0,
missingYield && isMissingYield,
placeholderSteps && recipe.placeholder_count > 0
];
const matchesAttention = (emptyRecipe || missingYield || placeholderSteps)
? conditions.some(Boolean)
: (recipe.item_count === 0 || isMissingYield || recipe.placeholder_count > 0);
if (!matchesAttention) return false;
}
if (selectedTags.length > 0) {
const tags = parseItemTags(recipe.tags_json, recipe.categories_json);
const matchesTags = selectedTags.some(t => tags.includes(t));
if (!matchesTags) return false;
}
if (selectedIngredients.length > 0) {
const ingIds = recipeIngredientsMap.get(recipe.id) ?? new Set();
const matchesIngredients = selectedIngredients.some(id => ingIds.has(id));
if (!matchesIngredients) return false;
}
return true;
});
const tabs:Array<{type:string;label:string;count:number;kind:"recipe"|"ingredient"|"book"|"purchase"|"inventory";href?:string}>=[
{type:"recipe",label:"Recipes",count:recipes.length,kind:"recipe"},
{type:"ingredient",label:"Ingredients",count:ingredients.length,kind:"ingredient"},
@@ -65,10 +214,10 @@ const tabs:Array<{type:string;label:string;count:number;kind:"recipe"|"ingredien
{type:"inventory",label:"Inventory",count:inventoryCounts?.c ?? 0,kind:"inventory",href:"/app/inventory/"},
];
const allSearchResults=normalizedQuery ? [
...recipes.filter((item)=>`${item.title} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery)).map((item)=>({id:item.id,kind:"recipe" as const,label:"Recipe",name:item.title,detail:`${item.yield_quantity} ${item.yield_unit_id}`,href:`/app/recipes/${item.id}/`})),
...ingredients.filter((item)=>`${item.name} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery)).map((item)=>({id:item.id,kind:"ingredient" as const,label:"Ingredient",name:titleCase(item.name),detail:`Used in ${item.recipe_count} ${item.recipe_count===1?"recipe":"recipes"}`,href:`/app/ingredients/${item.id}/`})),
...books.filter((item)=>`${item.name} ${item.description??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery)).map((item)=>({id:item.id,kind:"book" as const,label:"Recipe book",name:item.name,detail:`${item.recipe_count} ${item.recipe_count===1?"recipe":"recipes"}`,href:`/app/recipe-books/${item.id}/`})),
...purchases.filter((item)=>`${item.name} ${item.ingredient_name} ${item.supplier_id??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery)).map((item)=>({id:item.id,kind:"purchase" as const,label:"Purchase item",name:item.name,detail:titleCase(item.ingredient_name),href:`/app/ingredients/${item.ingredient_id}/#costs`})),
...recipes.filter((item)=>`${item.title} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && (selectedTags.length === 0 || parseItemTags(item.tags_json, item.categories_json).some(t => selectedTags.includes(t))) && (selectedIngredients.length === 0 || Array.from(recipeIngredientsMap.get(item.id) ?? []).some((id: string) => selectedIngredients.includes(id)))).map((item)=>({id:item.id,kind:"recipe" as const,label:"Recipe",name:item.title,detail:`${item.yield_quantity} ${item.yield_unit_id}`,href:`/app/recipes/${item.id}/`})),
...ingredients.filter((item)=>`${item.name} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && (selectedTags.length === 0 || parseItemTags(item.tags_json, item.categories_json).some(t => selectedTags.includes(t)))).map((item)=>({id:item.id,kind:"ingredient" as const,label:"Ingredient",name:titleCase(item.name),detail:`Used in ${item.recipe_count} ${item.recipe_count===1?"recipe":"recipes"}`,href:`/app/ingredients/${item.id}/`})),
...books.filter((item)=>`${item.name} ${item.description??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && selectedTags.length === 0).map((item)=>({id:item.id,kind:"book" as const,label:"Recipe book",name:item.name,detail:`${item.recipe_count} ${item.recipe_count===1?"recipe":"recipes"}`,href:`/app/recipe-books/${item.id}/`})),
...purchases.filter((item)=>`${item.name} ${item.ingredient_name} ${item.supplier_id??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && selectedTags.length === 0).map((item)=>({id:item.id,kind:"purchase" as const,label:"Purchase item",name:item.name,detail:titleCase(item.ingredient_name),href:`/app/ingredients/${item.ingredient_id}/#costs`})),
].sort((a,b)=>a.name.localeCompare(b.name)):[];
const searchResults=filteringSearchTypes?allSearchResults.filter((result)=>selectedSearchTypes.includes(result.kind)):allSearchResults;
const searchRows=searchResults.map(({id,kind,name,href,label,detail})=>({id,name,href,kind,detail:`${label} · ${detail}`}));
@@ -81,20 +230,12 @@ const purchaseRows=purchases.map(item=>({id:item.id,name:item.name,href:`/app/in
<form class="workspace-global-search" method="get" action="/app/" role="search">
<input type="hidden" name="type" value={type??""}/>
{selectedSearchTypes.map((selectedType)=><input type="hidden" name="item_type" value={selectedType}/>)}
{selectedTags.map((tag)=><input type="hidden" name="tag" value={tag}/>)}
{selectedIngredients.map((ing)=><input type="hidden" name="ingredient" value={ing}/>)}
<svg class="search-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="search" name="q" value={query} placeholder="Search " aria-label="Search all items" autofocus={Boolean(query)}/>
{query&&<a href={type?`/app/?type=${type}`:"/app/"} aria-label="Clear search">×</a>}
</form>
<details class="search-type-filter" open={filteringSearchTypes}>
<summary><svg class="filter-icon" viewBox="0 0 18 21" aria-hidden="true"><path fill="currentColor" d={FILTER_ICON_PATH}/></svg>Item type{filteringSearchTypes?` · ${selectedSearchTypes.length}`:""}</summary>
<form method="get" action="/app/"><input type="hidden" name="type" value={type??""}/><input type="hidden" name="q" value={query}/>
<label><input type="checkbox" name="item_type" value="recipe" checked={selectedSearchTypes.includes("recipe")}/> Recipes</label>
<label><input type="checkbox" name="item_type" value="ingredient" checked={selectedSearchTypes.includes("ingredient")}/> Ingredients</label>
<label><input type="checkbox" name="item_type" value="book" checked={selectedSearchTypes.includes("book")}/> Recipe books</label>
<label><input type="checkbox" name="item_type" value="purchase" checked={selectedSearchTypes.includes("purchase")}/> Purchase items</label>
<div><button>Apply</button>{filteringSearchTypes&&<a href={type?`/app/?type=${type}&q=${encodeURIComponent(query)}`:`/app/?q=${encodeURIComponent(query)}`}>All types</a>}</div>
</form>
</details>
{!readOnlyMode&&<details class="workspace-new-menu">
<summary><span class="new-trigger-plus" aria-hidden="true"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg></span><span class="new-trigger-label">New</span></summary>
<nav aria-label="Create new item">
@@ -105,15 +246,26 @@ const purchaseRows=purchases.map(item=>({id:item.id,name:item.name,href:`/app/in
</div>
</div>
<section class="shell directory-workspace">
<nav class="workspace-pills" aria-label="Workspaces">
{tabs.map((tab)=><a class:list={{active:type===tab.type}} href={tab.href ?? `/app/?type=${tab.type}`}><span class={`workspace-pill-icon ${tab.kind}`}><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d={TYPE_ICONS[tab.kind]} style={TYPE_ICON_TRANSFORMS[tab.kind]?{transform:TYPE_ICON_TRANSFORMS[tab.kind]}:undefined}/></svg></span><span>{tab.label}</span><small>{tab.count}</small></a>)}
{type==="ingredient"&&<details class="filter-menu" open={filtering}><summary><svg class="filter-icon" viewBox="0 0 18 21" aria-hidden="true"><path fill="currentColor" d={FILTER_ICON_PATH}/></svg>Filter{filtering?` · ${filteredIngredients.length}`:""}</summary><form method="get"><input type="hidden" name="type" value="ingredient"/><label><input type="checkbox" name="attention" value="1" checked={attention}/> Needs attention</label><fieldset><legend>Attention reasons</legend><label><input type="checkbox" name="missing_cost" value="1" checked={missingCost}/> Missing cost</label><label><input type="checkbox" name="no_usda" value="1" checked={noUsda}/> No USDA map</label><label><input type="checkbox" name="unused" value="1" checked={unused}/> Unused ingredient</label></fieldset><div><button>Apply filter</button>{filtering&&<a href="/app/?type=ingredient">Clear</a>}</div></form></details>}
{type==="recipe"&&<details class="filter-menu" open={filtering}><summary><svg class="filter-icon" viewBox="0 0 18 21" aria-hidden="true"><path fill="currentColor" d={FILTER_ICON_PATH}/></svg>Filter{filtering?` · ${filteredRecipes.length}`:""}</summary><form method="get"><input type="hidden" name="type" value="recipe"/><label><input type="checkbox" name="attention" value="1" checked={attention}/> Needs attention</label><fieldset><legend>Attention reasons</legend><label><input type="checkbox" name="empty_recipe" value="1" checked={emptyRecipe}/> Empty recipe</label><label><input type="checkbox" name="placeholder_steps" value="1" checked={placeholderSteps}/> Placeholder instructions</label></fieldset><div><button>Apply filter</button>{filtering&&<a href="/app/?type=recipe">Clear</a>}</div></form></details>}
{!readOnlyMode&&<>
<a class="archive-link" href="/app/archive/">Archive</a>
<a class="archive-link" href="/app/settings/">Data Management</a>
</>}
</nav>
<div class="workspace-nav-block">
{!readOnlyMode&&<nav class="workspace-utility-nav" aria-label="Secondary tools">
<a class="workspace-utility-link" href="/app/archive/">Archive</a>
<a class="workspace-utility-link" href="/app/settings/">Data Management</a>
</nav>}
<nav class="workspace-pills" aria-label="Workspaces">
{tabs.map((tab)=><a class:list={{active:type===tab.type}} href={tab.href ?? `/app/?type=${tab.type}`}><span class={`workspace-pill-icon ${tab.kind}`}><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d={TYPE_ICONS[tab.kind]} style={TYPE_ICON_TRANSFORMS[tab.kind]?{transform:TYPE_ICON_TRANSFORMS[tab.kind]}:undefined}/></svg></span><span>{tab.label}</span><small>{tab.count}</small></a>)}
<WorkspaceFilterBar
client:load
type={type ?? undefined}
query={query}
recipeCounts={recipeAttentionCounts}
ingredientCounts={ingredientAttentionCounts}
itemTypeCounts={searchItemTypeCounts}
tagOptions={tagOptions}
ingredientOptions={ingredientOptions}
currentParams={currentParams}
/>
</nav>
</div>
{query?<section class="workspace-search-results" aria-live="polite"><p><strong>{searchResults.length}</strong> {searchResults.length===1?"result":"results"} for “{query}”{filteringSearchTypes&&` · ${selectedSearchTypes.length} item ${selectedSearchTypes.length===1?"type":"types"}`}</p>{searchRows.length?<EntityDirectory client:load rows={searchRows} emptyMessage="No items of the selected types match this search." readOnly={readOnlyMode}/>:<div class="empty-state">No items of the selected types match this search.</div>}</section>:<>
{type==="ingredient"&&<EntityDirectory client:load rows={ingredientRows} entityType="ingredient" emptyMessage="No ingredients match these filters." readOnly={readOnlyMode}/>}
{type==="recipe"&&<EntityDirectory client:load rows={recipeRows} entityType="recipe" emptyMessage="No recipes yet." readOnly={readOnlyMode}/>}
@@ -7,6 +7,7 @@ import { bestUsdaPortions, fetchUsdaFood, usdaNutrition } from "../../../../lib/
import { number, titleCase } from "../../../../lib/format";
import PurchaseItemForm from "../../../../components/PurchaseItemForm.astro";
import DetailUtility from "../../../../components/DetailUtility.astro";
import { getTabIconHtml, type TabIconKey } from "../../../../lib/icons";
const id = Astro.params.id!;
const editing = !readOnlyMode && Astro.url.searchParams.get("edit") === "1";
const database = openDatabase({ readOnly: false });
@@ -126,14 +127,8 @@ const prepDisplay = prep.map((row) => {
return measure ? `${number(measure.quantity)} ${unitById.get(measure.unit_id)?.symbol ?? measure.unit_id}` : "—";
};
return {...row,weight:showMeasure("mass"),volume:showMeasure("volume"),each:showMeasure("count")};
});const RECIPE_TAB_ICONS = {
overview: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z",
costing: "M11.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4",
equivalencies: "M19.4 3.3h-6.6v-.5c0-.4-.3-.7-.8-.7-.4 0-.8.3-.8.7v.5H4.6L0 14s.2 3.8 4.7 3.8S9.4 14 9.4 14L6.1 6.2h5.1v15.7h1.5V6.2h5.1L14.6 14s.2 3.8 4.7 3.8S24 14 24 14L19.4 3.3zM7.7 14H1.5l3.1-7.4L7.7 14zm8.5 0l3.1-7.4 3.1 7.4h-6.2z",
nutrition: "M9.42859 2.37431L9.80926 2.82134L10.1899 2.37431C11.1674 1.22652 12.668 0.5 14.2234 0.5C16.9685 0.5 19.1185 2.64998 19.1185 5.3951C19.1185 7.0848 18.3631 8.65707 16.9325 10.4062C15.4961 12.1623 13.4317 14.0352 10.8957 16.3348L10.895 16.3354L9.80799 17.325L8.72319 16.345L8.72211 16.344L8.71119 16.3341C6.18062 14.0344 4.12043 12.1623 2.68618 10.4075C1.25541 8.6571 0.5 7.08481 0.5 5.3951C0.5 2.64998 2.64998 0.5 5.3951 0.5C6.95051 0.5 8.45117 1.22652 9.42859 2.37431ZM3.70568 10.127C5.0829 11.7363 7.04455 13.5134 9.36637 15.6157L9.45571 15.7051L9.80926 16.0586L10.1628 15.7051L10.2522 15.6157C12.574 13.5134 14.5356 11.7363 15.9128 10.127C17.287 8.52131 18.1567 6.99709 18.1567 5.3951C18.1567 3.1571 16.4614 1.46185 14.2234 1.46185C12.6415 1.46185 11.0895 2.39876 10.4049 3.77684H9.22149C8.52967 2.40009 6.97866 1.46185 5.3951 1.46185C3.1571 1.46185 1.46185 3.1571 1.46185 5.3951C1.46185 6.99709 2.3315 8.52131 3.70568 10.127Z"
};
const RECIPE_TAB_VIEWBOX = { overview: "0 0 24 24", costing: "0 0 24 24", equivalencies: "0 0 24 24", nutrition: "0 0 20 18" };
const recipeTabIcon = (name: string) => `<span class="recipe-tab-icon"><svg viewBox="${RECIPE_TAB_VIEWBOX[name as keyof typeof RECIPE_TAB_VIEWBOX]}" aria-hidden="true"><path fill="currentColor" d="${RECIPE_TAB_ICONS[name as keyof typeof RECIPE_TAB_ICONS]}"/></svg></span>`;
});
const recipeTabIcon = (name: string) => getTabIconHtml(name as TabIconKey);
database.close();
---
<BaseLayout title={editing ? `Edit ${titleCase(ingredient.name)}` : titleCase(ingredient.name)} immersive>
@@ -309,8 +304,8 @@ database.close();
<div class="recipe-read-right ingredient-read-right">
<div class="recipe-workspace-tabs recipe-read-tabs entity-tabs" aria-label="Ingredient details">
<button class="mobile-only-tab active" type="button" data-tab="overview"><Fragment set:html={recipeTabIcon("overview")}/><span class="recipe-tab-label">Details</span></button>
<button type="button" data-tab="costs"><Fragment set:html={recipeTabIcon("costing")}/><span class="recipe-tab-label">Cost</span></button>
<button class="mobile-only-tab" type="button" data-tab="overview"><Fragment set:html={recipeTabIcon("overview")}/><span class="recipe-tab-label">Details</span></button>
<button class="active" type="button" data-tab="costs"><Fragment set:html={recipeTabIcon("costing")}/><span class="recipe-tab-label">Cost</span></button>
<button type="button" data-tab="equivalencies"><Fragment set:html={recipeTabIcon("equivalencies")}/><span class="recipe-tab-label">UoM Equivalency</span></button>
<button type="button" data-tab="nutrition"><Fragment set:html={recipeTabIcon("nutrition")}/><span class="recipe-tab-label">Nutrition</span></button>
</div>
@@ -508,6 +503,8 @@ database.close();
if (target) target.click();
} else if (window.innerWidth <= 900) {
switchIngredientTab('overview');
} else {
switchIngredientTab('costs');
}
</script>
</BaseLayout>
@@ -4,6 +4,7 @@ import BaseLayout from "../../../../layouts/BaseLayout.astro";
import DetailUtility from "../../../../components/DetailUtility.astro";
import { readOnlyMode } from "../../../../lib/runtime";
import { openDatabase, refreshSiteProjection } from "../../../../lib/database";
import { TYPE_ICONS, TYPE_ICON_TRANSFORMS } from "../../../../lib/icons";
const id = Astro.params.id!;
const editing = !readOnlyMode && Astro.url.searchParams.get("edit") === "1";
@@ -170,7 +171,7 @@ database.close();
/>
<span class="book-recipe-icon">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M13.125 0C13.6428 0 14.0625 0.419733 14.0625 0.9375V14.0625C14.0625 14.5803 13.6428 15 13.125 15H0.9375C0.419733 15 0 14.5803 0 14.0625V0.9375C0 0.419733 0.419733 0 0.9375 0H13.125ZM12.1875 1.875H1.875V13.125H12.1875V1.875ZM11.25 9.375V11.25H5.625V9.375H11.25ZM4.6875 9.375V11.25H2.8125V9.375H4.6875ZM11.25 6.5625V8.4375H5.625V6.5625H11.25ZM4.6875 6.5625V8.4375H2.8125V6.5625H4.6875ZM11.25 3.75V5.625H5.625V3.75H11.25ZM4.6875 3.75V5.625H2.8125V3.75H4.6875Z" transform="scale(1.1, 1.1) translate(4px, 3.5px)"/>
<path d={TYPE_ICONS.recipe} style={TYPE_ICON_TRANSFORMS.recipe ? { transform: TYPE_ICON_TRANSFORMS.recipe } : undefined}/>
</svg>
</span>
<span class="book-checklist-label">
@@ -199,7 +200,7 @@ database.close();
<div class="book-recipe-row">
<span class="book-recipe-icon">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M13.125 0C13.6428 0 14.0625 0.419733 14.0625 0.9375V14.0625C14.0625 14.5803 13.6428 15 13.125 15H0.9375C0.419733 15 0 14.5803 0 14.0625V0.9375C0 0.419733 0.419733 0 0.9375 0H13.125ZM12.1875 1.875H1.875V13.125H12.1875V1.875ZM11.25 9.375V11.25H5.625V9.375H11.25ZM4.6875 9.375V11.25H2.8125V9.375H4.6875ZM11.25 6.5625V8.4375H5.625V6.5625H11.25ZM4.6875 6.5625V8.4375H2.8125V6.5625H4.6875ZM11.25 3.75V5.625H5.625V3.75H11.25ZM4.6875 3.75V5.625H2.8125V3.75H4.6875Z" transform="scale(1.1, 1.1) translate(4px, 3.5px)"/>
<path d={TYPE_ICONS.recipe} style={TYPE_ICON_TRANSFORMS.recipe ? { transform: TYPE_ICON_TRANSFORMS.recipe } : undefined}/>
</svg>
</span>
<div class="book-recipe-info">
+17 -17
View File
@@ -18,6 +18,7 @@ import type { Ingredient, Recipe } from "../../../../lib/types";
import DetailUtility from "../../../../components/DetailUtility.astro";
import { convertWithIngredientMeasures } from "../../../../lib/measurement";
import { readOnlyMode } from "../../../../lib/runtime";
import { getTabIconHtml, type TabIconKey } from "../../../../lib/icons";
const id = Astro.params.id!;
const editing = !readOnlyMode && Astro.url.searchParams.get("edit") === "1";
@@ -168,15 +169,7 @@ database.close();
const categories = JSON.parse(recipe.categories_json).join(", ");
const tags = JSON.parse(recipe.tags_json).join(", ");
const saved = Astro.url.searchParams.get("saved") === "1";
const RECIPE_TAB_ICONS={
formula:"M7.57975 2.24434L7.65872 2.13161C8.13511 1.47697 8.63143 1.08241 9.16762 0.957718C9.57278 0.863497 9.96486 1.18135 10.0434 1.66766C10.1219 2.15397 9.85705 2.62459 9.45189 2.71881C9.18697 2.78042 8.8382 3.1513 8.44252 3.84957C8.78173 3.85543 9.10757 3.88576 9.42032 3.94078C11.9002 4.37705 13.577 7.45745 12.4331 11.6424C11.496 15.0703 9.68081 16.5977 7.229 15.682C6.85528 15.5611 6.58673 15.5046 6.4542 15.5046C6.40147 15.5046 6.36601 15.5163 6.32134 15.5491L6.15949 15.644C3.74207 16.7473 1.8122 15.2446 0.553846 11.7108C-0.274666 9.38411 -0.146354 7.48777 0.75651 6.03344C1.3667 5.05056 2.12543 4.48603 3.08872 4.01846C3.29048 3.92053 3.51746 3.85185 3.7715 3.81087C3.24172 3.20799 2.94401 2.30051 2.83382 1.12442L2.74286 0.153477L3.63312 0.0415436C5.52154 -0.195889 6.8746 0.590513 7.57975 2.24434ZM3.88234 5.71589C3.2053 6.04451 2.69949 6.42087 2.31601 7.03856C1.73369 7.97655 1.64642 9.26637 2.2885 11.0695C3.21226 13.6636 4.17073 14.4453 5.3377 13.9576C5.67319 13.7379 6.05207 13.6244 6.4542 13.6244C6.81471 13.6244 7.24894 13.7157 7.82562 13.9033C9.09594 14.3772 9.97129 13.6407 10.6554 11.1379C11.5187 7.9797 10.4582 6.03137 9.10636 5.79357C8.42613 5.6739 7.60929 5.71807 6.65655 5.93628L6.446 5.9845L6.2363 5.93257C4.98214 5.62199 4.16221 5.58004 3.88234 5.71589ZM4.69764 1.88235C4.80315 2.16337 4.92885 2.34891 5.06454 2.4469C5.26392 2.5909 5.53003 2.73516 5.86115 2.87726C5.58815 2.34716 5.20687 2.02464 4.69764 1.88235Z",
method:"M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5m0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5m0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5M7 19h14v-2H7zm0-6h14v-2H7zm0-8v2h14V5z",
costing:"M11.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4",
equivalencies:"M19.4 3.3h-6.6v-.5c0-.4-.3-.7-.8-.7-.4 0-.8.3-.8.7v.5H4.6L0 14s.2 3.8 4.7 3.8S9.4 14 9.4 14L6.1 6.2h5.1v15.7h1.5V6.2h5.1L14.6 14s.2 3.8 4.7 3.8S24 14 24 14L19.4 3.3zM7.7 14H1.5l3.1-7.4L7.7 14zm8.5 0l3.1-7.4 3.1 7.4h-6.2z",
nutrition:"M9.42859 2.37431L9.80926 2.82134L10.1899 2.37431C11.1674 1.22652 12.668 0.5 14.2234 0.5C16.9685 0.5 19.1185 2.64998 19.1185 5.3951C19.1185 7.0848 18.3631 8.65707 16.9325 10.4062C15.4961 12.1623 13.4317 14.0352 10.8957 16.3348L10.895 16.3354L9.80799 17.325L8.72319 16.345L8.72211 16.344L8.71119 16.3341C6.18062 14.0344 4.12043 12.1623 2.68618 10.4075C1.25541 8.6571 0.5 7.08481 0.5 5.3951C0.5 2.64998 2.64998 0.5 5.3951 0.5C6.95051 0.5 8.45117 1.22652 9.42859 2.37431ZM3.70568 10.127C5.0829 11.7363 7.04455 13.5134 9.36637 15.6157L9.45571 15.7051L9.80926 16.0586L10.1628 15.7051L10.2522 15.6157C12.574 13.5134 14.5356 11.7363 15.9128 10.127C17.287 8.52131 18.1567 6.99709 18.1567 5.3951C18.1567 3.1571 16.4614 1.46185 14.2234 1.46185C12.6415 1.46185 11.0895 2.39876 10.4049 3.77684H9.22149C8.52967 2.40009 6.97866 1.46185 5.3951 1.46185C3.1571 1.46185 1.46185 3.1571 1.46185 5.3951C1.46185 6.99709 2.3315 8.52131 3.70568 10.127Z"
};
const RECIPE_TAB_VIEWBOX={formula:"0 0 14 16",method:"0 0 24 24",costing:"0 0 24 24",equivalencies:"0 0 24 24",nutrition:"0 0 20 18"};
const recipeTabIcon=(name:string)=>`<span class="recipe-tab-icon"><svg viewBox="${RECIPE_TAB_VIEWBOX[name as keyof typeof RECIPE_TAB_VIEWBOX]}" aria-hidden="true"><path fill="currentColor" d="${RECIPE_TAB_ICONS[name as keyof typeof RECIPE_TAB_ICONS]}"/></svg></span>`;
const recipeTabIcon = (name: string) => getTabIconHtml(name as TabIconKey);
---
<BaseLayout title={editing?`Edit ${recipe.title}`:recipe.title} immersive>
<section class="recipe-detail-shell recipe-read-shell">
@@ -239,7 +232,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>
)}
@@ -247,8 +240,8 @@ const recipeTabIcon=(name:string)=>`<span class="recipe-tab-icon"><svg viewBox="
<div class="recipe-read-right">
<div class="recipe-workspace-tabs recipe-read-tabs">
<button class="mobile-only-tab active" type="button" data-recipe-tab="formula"><Fragment set:html={recipeTabIcon("formula")}/><span class="recipe-tab-label">Ingredients</span></button>
<button type="button" data-recipe-tab="method"><Fragment set:html={recipeTabIcon("method")}/><span class="recipe-tab-label">Prep Method</span></button>
<button class="mobile-only-tab" type="button" data-recipe-tab="formula"><Fragment set:html={recipeTabIcon("formula")}/><span class="recipe-tab-label">Ingredients</span></button>
<button class="active" type="button" data-recipe-tab="method"><Fragment set:html={recipeTabIcon("method")}/><span class="recipe-tab-label">Prep Method</span></button>
<button type="button" data-recipe-tab="costing"><Fragment set:html={recipeTabIcon("costing")}/><span class="recipe-tab-label">Cost</span></button>
<button type="button" data-recipe-tab="equivalencies"><Fragment set:html={recipeTabIcon("equivalencies")}/><span class="recipe-tab-label">UoM Equivalency</span></button>
<button type="button" data-recipe-tab="nutrition"><Fragment set:html={recipeTabIcon("nutrition")}/><span class="recipe-tab-label">Nutrition</span></button>
@@ -314,11 +307,16 @@ const recipeTabIcon=(name:string)=>`<span class="recipe-tab-icon"><svg viewBox="
<p class="uom-equation-title">XX Weight = XX Volume = XX Each</p>
<p class="uom-helper-text">
If you would like to use this recipe by weight, volume, and even by the portion - you can customize that here.
<button class="uom-help-btn" type="button" aria-label="Help on UoM Equivalency">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 16h-2v-2h2v2zm1.07-7.75l-.9.92C12.45 11.9 12 12.5 12 14h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H7c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.04-.42 1.99-1.07 2.75z"/>
</svg>
</button>
<span class="uom-help-wrap">
<button class="uom-help-btn" type="button" aria-label="Help on UoM Equivalency">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 16h-2v-2h2v2zm1.07-7.75l-.9.92C12.45 11.9 12 12.5 12 14h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H7c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.04-.42 1.99-1.07 2.75z"/>
</svg>
</button>
<span class="uom-tooltip-popper meez-tooltip-card" role="tooltip">
For example, if your recipe yields 1 quart, weighs 850 grams, and each portion is 85 grams, toggle off the standard conversion and enter '850 grams = 1 quart = 10 portions'.
</span>
</span>
</p>
<div class="uom-toggle-row">
@@ -471,6 +469,8 @@ const recipeTabIcon=(name:string)=>`<span class="recipe-tab-icon"><svg viewBox="
}
} else if (window.innerWidth <= 900) {
switchRecipeTab('formula');
} else {
switchRecipeTab('method');
}
</script>
{editing&&<script is:inline>
@@ -343,6 +343,18 @@ try {
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
}
.settings-page :global(.detail-utility) {
position: sticky !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
z-index: 30 !important;
background: #ffffff !important;
border-bottom: 1px solid #edf0f5 !important;
}
.settings-workspace {
-1
View File
@@ -41,7 +41,6 @@ const resolvedHref = sectionHref ?? inferredHref;
<nav class="detail-utility" aria-label="Entity navigation">
<div class="detail-context">
<a class="detail-back" href={resolvedHref} aria-label={`Back to ${resolvedSection??"home"}`} data-back-link></a>
<a class="detail-avatar" href="/app/" aria-label="Recipe Book home">RB</a>
<ol class="detail-breadcrumbs">
<li><a href="/app/">Home</a></li>
{resolvedSection&&<li aria-current="page"><a href={resolvedHref}>{resolvedSection}</a></li>}
+1 -12
View File
@@ -1,20 +1,9 @@
import { useRef,useState } from "preact/hooks";
import { TYPE_ICONS, TYPE_ICON_TRANSFORMS } from "../lib/icons";
export type DirectoryRow = {
id:string; name:string; href?:string; kind:"recipe"|"ingredient"|"book"|"purchase"; detail?:string;
};
const TYPE_ICONS:Record<DirectoryRow["kind"],string> = {
recipe:"M13.125 0C13.6428 0 14.0625 0.419733 14.0625 0.9375V14.0625C14.0625 14.5803 13.6428 15 13.125 15H0.9375C0.419733 15 0 14.5803 0 14.0625V0.9375C0 0.419733 0.419733 0 0.9375 0H13.125ZM12.1875 1.875H1.875V13.125H12.1875V1.875ZM11.25 9.375V11.25H5.625V9.375H11.25ZM4.6875 9.375V11.25H2.8125V9.375H4.6875ZM11.25 6.5625V8.4375H5.625V6.5625H11.25ZM4.6875 6.5625V8.4375H2.8125V6.5625H4.6875ZM11.25 3.75V5.625H5.625V3.75H11.25ZM4.6875 3.75V5.625H2.8125V3.75H4.6875Z",
ingredient:"M7.57975 2.24434L7.65872 2.13161C8.13511 1.47697 8.63143 1.08241 9.16762 0.957718C9.57278 0.863497 9.96486 1.18135 10.0434 1.66766C10.1219 2.15397 9.85705 2.62459 9.45189 2.71881C9.18697 2.78042 8.8382 3.1513 8.44252 3.84957C8.78173 3.85543 9.10757 3.88576 9.42032 3.94078C11.9002 4.37705 13.577 7.45745 12.4331 11.6424C11.496 15.0703 9.68081 16.5977 7.229 15.682C6.85528 15.5611 6.58673 15.5046 6.4542 15.5046C6.40147 15.5046 6.36601 15.5163 6.32134 15.5491L6.15949 15.644C3.74207 16.7473 1.8122 15.2446 0.553846 11.7108C-0.274666 9.38411 -0.146354 7.48777 0.75651 6.03344C1.3667 5.05056 2.12543 4.48603 3.08872 4.01846C3.29048 3.92053 3.51746 3.85185 3.7715 3.81087C3.24172 3.20799 2.94401 2.30051 2.83382 1.12442L2.74286 0.153477L3.63312 0.0415436C5.52154 -0.195889 6.8746 0.590513 7.57975 2.24434ZM3.88234 5.71589C3.2053 6.04451 2.69949 6.42087 2.31601 7.03856C1.73369 7.97655 1.64642 9.26637 2.2885 11.0695C3.21226 13.6636 4.17073 14.4453 5.3377 13.9576C5.67319 13.7379 6.05207 13.6244 6.4542 13.6244C6.81471 13.6244 7.24894 13.7157 7.82562 13.9033C9.09594 14.3772 9.97129 13.6407 10.6554 11.1379C11.5187 7.9797 10.4582 6.03137 9.10636 5.79357C8.42613 5.6739 7.60929 5.71807 6.65655 5.93628L6.446 5.9845L6.2363 5.93257C4.98214 5.62199 4.16221 5.58004 3.88234 5.71589ZM4.69764 1.88235C4.80315 2.16337 4.92885 2.34891 5.06454 2.4469C5.26392 2.5909 5.53003 2.73516 5.86115 2.87726C5.58815 2.34716 5.20687 2.02464 4.69764 1.88235Z",
book:"M14.0347 0V16H3.12658C1.50098 16 0.527344 15.0264 0.527344 13.4008V2.59923C0.527344 0.97364 1.50098 0 3.12658 0H14.0347ZM12.1594 12.6763L3.95747 12.6765C2.75227 12.6765 2.40226 12.9098 2.40226 13.4008C2.40226 13.9909 2.53647 14.1251 3.12658 14.1251H12.1598L12.1594 12.6763ZM12.1598 1.87492H3.12658C2.53647 1.87492 2.40226 2.00913 2.40226 2.59923L2.40185 10.9999C2.85223 10.8678 3.37428 10.8015 3.95747 10.8015L12.1594 10.8014L12.1598 1.87492ZM10.0768 3.80157V5.67649H4.45204V3.80157H10.0768Z",
purchase:"M19.5 3.5 18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2 4.5 3.5 3 2v20l1.5-1.5L6 22l1.5-1.5L9 22l1.5-1.5L12 22l1.5-1.5L15 22l1.5-1.5L18 22l1.5-1.5L21 22V2l-1.5 1.5zM19 19.09H5V4.91h14v14.18zM6 15h12v2H6zm0-4h12v2H6zm0-4h12v2H6z"
};
const TYPE_ICON_TRANSFORMS:Record<DirectoryRow["kind"],string|undefined> = {
recipe:"scale(1.1, 1.1) translate(4px, 3.5px)",
ingredient:"scale(1.3, 1.3) translate(2.5px, 1px)",
book:"scale(1.1, 1.1) translate(3.5px, 2.5px)",
purchase:undefined
};
type Props={ rows:DirectoryRow[]; entityType?:DirectoryRow["kind"]; emptyMessage:string; readOnly?:boolean };
export default function EntityDirectory({rows,entityType,emptyMessage,readOnly=false}:Props) {
+69
View File
@@ -0,0 +1,69 @@
import {
TAB_ICONS,
TAB_VIEWBOXES,
TYPE_ICONS,
TYPE_ICON_TRANSFORMS,
UI_ICONS,
type TabIconKey,
type EntityIconKey,
type UiIconKey
} from "../lib/icons";
interface TabIconProps {
name: TabIconKey;
className?: string;
size?: number;
}
export function TabIcon({ name, className = "recipe-tab-icon", size }: TabIconProps) {
const path = TAB_ICONS[name];
const viewBox = TAB_VIEWBOXES[name] || "0 0 24 24";
if (!path) return null;
return (
<span class={className} aria-hidden="true">
<svg viewBox={viewBox} width={size} height={size} aria-hidden="true">
<path fill="currentColor" d={path} />
</svg>
</span>
);
}
interface TypeIconProps {
kind: EntityIconKey;
className?: string;
size?: number;
}
export function TypeIcon({ kind, className, size = 20 }: TypeIconProps) {
const path = TYPE_ICONS[kind];
const transform = TYPE_ICON_TRANSFORMS[kind];
if (!path) return null;
const wrapperClass = className ?? `workspace-pill-icon ${kind}`;
return (
<span class={wrapperClass} aria-hidden="true">
<svg viewBox="0 0 24 24" width={size} height={size} aria-hidden="true">
<path fill="currentColor" d={path} style={transform ? { transform } : undefined} />
</svg>
</span>
);
}
interface UiIconProps {
name: UiIconKey;
size?: number;
className?: string;
}
export function UiIcon({ name, size = 16, className }: UiIconProps) {
const path = UI_ICONS[name];
if (!path) return null;
return (
<svg viewBox="0 0 24 24" width={size} height={size} class={className} aria-hidden="true">
<path fill="currentColor" d={path} />
</svg>
);
}
+313 -46
View File
@@ -5,6 +5,7 @@ import type { NutritionResult } from "../lib/nutrition";
import type { CostLine, CostResult } from "../lib/costing";
import NutritionPanel from "./NutritionPanel";
import { number, roundForDisplay } from "../lib/format";
import { TYPE_ICONS, TYPE_ICON_TRANSFORMS } from "../lib/icons";
type Props = {
components: CalculatorComponent[];
@@ -19,6 +20,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 +49,94 @@ 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) {
const SCALE_OPTIONS = [
{ value: 0.5, label: "1/2x" },
{ value: 1, label: "1x" },
{ value: 2, label: "2x" },
{ value: 3, label: "3x" },
{ value: 4, label: "4x" },
{ value: 5, label: "5x" },
];
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 [isCustomScale, setIsCustomScale] = useState(false);
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 [showScaleTooltip, setShowScaleTooltip] = useState(false);
const [scaleTooltipTimer, setScaleTooltipTimer] = useState<any>(null);
const [activeAttentionId, setActiveAttentionId] = useState<string | null>(null);
const validFactor = Number.isFinite(factor) && factor >= 0 ? factor : 0;
const matchedScaleOption = SCALE_OPTIONS.find((opt) => Math.abs(opt.value - factor) < 0.001);
const showCustomInput = isCustomScale || !matchedScaleOption;
const handleScaleSelectChange = (e: Event) => {
const val = (e.currentTarget as HTMLSelectElement).value;
if (val === "custom") {
setIsCustomScale(true);
} else {
setIsCustomScale(false);
setFactor(Number(val));
}
};
const handleScaleTooltipEnter = () => {
if (scaleTooltipTimer) clearTimeout(scaleTooltipTimer);
setShowScaleTooltip(true);
};
const handleScaleTooltipLeave = () => {
const timer = setTimeout(() => {
setShowScaleTooltip(false);
}, 150);
setScaleTooltipTimer(timer);
};
const handleScaleTooltipToggle = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setShowScaleTooltip((prev) => !prev);
};
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement | null;
if (!target?.closest(".helper-tooltip-wrap")) {
setShowScaleTooltip(false);
}
if (!target?.closest(".ingredient-attention-wrapper")) {
setActiveAttentionId(null);
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setShowScaleTooltip(false);
setActiveAttentionId(null);
}
};
document.addEventListener("click", handleClickOutside);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("click", handleClickOutside);
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
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(
@@ -64,49 +147,141 @@ export default function RecipeCalculator({ components, units, basisUnitId, yield
[components, validFactor, basisUnitId, units],
);
const unconvertedCount=useMemo(()=>components.flatMap((component)=>component.items).filter((item)=>{try{convertItem(item.amount.quantity,item.amount.unit_id,basisUnitId,item,units);return false;}catch{return true;}}).length,[components,basisUnitId,units]);
const percentageBase=components.flatMap(component=>component.items).filter(item=>percentMode==="standard"||item.basisMember).reduce((sum,item)=>sum+(itemWeight(item)??0),0);
const percentageBase=useMemo(()=>{if(!calculatePercent)return 0;if(percentMode==="standard")return total;return components.flatMap(c=>c.items).filter(item=>item.basisMember).reduce((sum,item)=>{const w=itemWeight(item);return w!=null?sum+w*validFactor:sum;},0);},[calculatePercent,percentMode,total,components,units,validFactor]);
const baseItems=components.flatMap(component=>component.items).filter(item=>item.basisMember);
const basisUnit = units[basisUnitId];
const yieldItem = { id:"yield", label:"Yield", amount:{quantity:yieldQuantity,unit_id:yieldUnitId}, measureConversions:yieldConversions } as CalculatorItem;
const yieldUnits = itemUnits(yieldItem,units);
const scaledYield = convertItem(yieldQuantity * validFactor,yieldUnitId,yieldDisplayUnitId,yieldItem,units);
const changeYield = (value: number) => {
const canonicalQuantity = convertItem(value,yieldDisplayUnitId,yieldUnitId,yieldItem,units);
setFactor(yieldQuantity > 0 ? canonicalQuantity / yieldQuantity : 1);
const yieldUnits = useMemo(() => itemUnits(yieldItem,units), [yieldItem, units]);
const scaledYield = useMemo(() => {
try { return convertItem(yieldQuantity * validFactor, yieldUnitId, yieldDisplayUnitId, yieldItem, units); }
catch { return yieldQuantity * validFactor; }
}, [yieldQuantity, validFactor, yieldUnitId, yieldDisplayUnitId, yieldItem, units]);
const changeYield = (nextYield: number) => {
if (!Number.isFinite(nextYield) || nextYield <= 0 || yieldQuantity <= 0) return;
try {
const canonicalNextYield = convertItem(nextYield, yieldDisplayUnitId, yieldUnitId, yieldItem, units);
setFactor(canonicalNextYield / yieldQuantity);
} catch {
setFactor(nextYield / yieldQuantity);
}
};
const currentScaleLabel = !showCustomInput && matchedScaleOption ? matchedScaleOption.label : "Custom";
const yieldUnitSymbol = yieldUnits.find((u) => u.id === yieldDisplayUnitId)?.symbol ?? yieldDisplayUnitId;
const yieldQtyString = String(roundForDisplay(scaledYield));
const customFactorString = String(roundForDisplay(factor));
return (
<section class="calculator" aria-labelledby="formula-heading">
<div class="calculator-heading">
<label class="basis-input batch-multiplier-field">
<span class="scale-label">Batch:</span>
<section class="calculator" aria-label="Recipe scaling calculator">
<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
type="number"
min="0"
step="any"
aria-label="Batch multiplier"
value={roundForDisplay(factor)}
onInput={(event) => setFactor(Number((event.currentTarget as HTMLInputElement).value))}
/>
<select
aria-label="Batch size multiplier"
class="meez-scale-select"
data-testid="ScaleSelectorBase_scale_select"
style={{ width: `${currentScaleLabel.length + 2.4}ch` }}
value={!showCustomInput && matchedScaleOption ? String(matchedScaleOption.value) : "custom"}
onChange={handleScaleSelectChange}
>
{SCALE_OPTIONS.map((opt) => (
<option key={opt.value} value={String(opt.value)}>{opt.label}</option>
))}
<option value="custom">Custom</option>
</select>
{showCustomInput && (
<span class="custom-scale-wrap">
<input
type="text"
inputMode="decimal"
aria-label="Custom batch multiplier"
class="custom-scale-input"
style={{ width: `${Math.max(2, customFactorString.length + 0.8)}ch` }}
value={customFactorString}
onFocus={(event) => (event.currentTarget as HTMLInputElement).select()}
onClick={(event) => (event.currentTarget as HTMLInputElement).select()}
onInput={(event) => {
const val = Number((event.currentTarget as HTMLInputElement).value);
if (!isNaN(val) && val > 0) setFactor(val);
}}
autoFocus
/>
<span class="batch-unit-suffix">x</span>
</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
type="number"
min="0"
step="any"
type="text"
inputMode="decimal"
aria-label="Finished yield quantity"
value={roundForDisplay(scaledYield)}
style={{ width: `${Math.max(2, yieldQtyString.length + 0.5)}ch` }}
value={yieldQtyString}
onFocus={(event) => (event.currentTarget as HTMLInputElement).select()}
onClick={(event) => (event.currentTarget as HTMLInputElement).select()}
onInput={(event) => changeYield(Number((event.currentTarget as HTMLInputElement).value))}
/>
<select aria-label="Finished yield unit" value={yieldDisplayUnitId} onChange={(event) => setYieldDisplayUnitId((event.currentTarget as HTMLSelectElement).value)}>
<select
aria-label="Finished yield unit"
value={yieldDisplayUnitId}
style={{ width: `${yieldUnitSymbol.length + 0.8}ch` }}
onChange={(event) => setYieldDisplayUnitId((event.currentTarget as HTMLSelectElement).value)}
>
{yieldUnits.map((unit) => <option value={unit.id} key={unit.id}>{unit.symbol}</option>)}
</select>
</span>
</label>
<span
class="helper-tooltip-wrap"
data-testid="HelperTextTooltipBase"
onMouseEnter={handleScaleTooltipEnter}
onMouseLeave={handleScaleTooltipLeave}
>
<button
class="HelperTooltipBase_help_icon"
type="button"
data-testid="HelperTooltipBase_help_icon"
aria-label="Yield calculation help"
aria-expanded={showScaleTooltip}
onClick={handleScaleTooltipToggle}
>
<svg class="helper-tooltip-svg" viewBox="-1 0 20 18" width="18" height="18" fill="none" data-testid="TooltipWithIcon_IconDefault">
<path d="M17.376 17.8138C17.3808 17.7971 17.3855 17.7806 17.3862 17.7631C17.3865 17.7589 17.3887 17.7554 17.3887 17.7512C17.3887 17.7377 17.3833 17.7262 17.3813 17.7137C17.3793 17.7012 17.3813 17.689 17.3773 17.6768L14.2885 7.8178C14.2873 7.8138 14.284 7.81131 14.283 7.80731C14.2768 7.79059 14.2673 7.77611 14.2576 7.76114C14.2496 7.74816 14.2426 7.73493 14.2324 7.72395C14.2219 7.71272 14.2092 7.70448 14.1967 7.6955C14.1832 7.68526 14.1707 7.67478 14.1557 7.66779C14.143 7.6618 14.1293 7.65955 14.1153 7.65581C14.0976 7.65082 14.0801 7.64583 14.0614 7.64508C14.0574 7.64483 14.0542 7.64283 14.0502 7.64283H11.9598V5.60864C14.2521 4.65244 16.0574 2.69088 16.8052 0.324972C16.8092 0.312492 16.8072 0.300262 16.8092 0.288032C16.8114 0.275053 16.8169 0.263322 16.8169 0.249844C16.8169 0.245851 16.8147 0.242356 16.8144 0.238363C16.8137 0.220392 16.8089 0.204168 16.8042 0.187196C16.8002 0.17247 16.798 0.157744 16.7915 0.144266C16.7847 0.129789 16.7745 0.118058 16.765 0.10483C16.7555 0.0921004 16.7475 0.0788719 16.7358 0.0678898C16.7248 0.0576564 16.7111 0.0509174 16.6986 0.0426808C16.6836 0.0331962 16.6699 0.0237114 16.6529 0.0174715C16.6492 0.015974 16.6467 0.0129789 16.6427 0.0117309C16.6297 0.00748782 16.617 0.00948452 16.6043 0.00748777C16.5918 0.00549101 16.5806 0 16.5676 0H0.820666C0.806939 0 0.795208 0.00549111 0.782478 0.00773746C0.769999 0.00973422 0.757519 0.00773741 0.745289 0.0117309C0.741545 0.0129789 0.739049 0.0159741 0.735305 0.0172221C0.718333 0.0234619 0.704106 0.0331962 0.68913 0.0426808C0.676401 0.0509174 0.663172 0.0574067 0.65244 0.0676401C0.640709 0.0786222 0.632223 0.0921005 0.622738 0.105079C0.613253 0.117809 0.60327 0.129789 0.596531 0.144016C0.590041 0.157494 0.587795 0.17222 0.583801 0.186946C0.579059 0.203919 0.574317 0.220392 0.573318 0.238113C0.573069 0.242107 0.571072 0.245351 0.571072 0.249345C0.571072 0.262573 0.576563 0.274304 0.57856 0.287033C0.580557 0.299763 0.57856 0.312243 0.582803 0.324722C1.34432 2.72507 3.10645 4.63946 5.42818 5.60589V7.64233H3.33882C3.33458 7.64233 3.33134 7.64458 3.32709 7.64458C3.30887 7.64533 3.29215 7.65032 3.27468 7.65506C3.26045 7.65905 3.24622 7.6613 3.23325 7.66729C3.21802 7.67453 3.20554 7.68501 3.19206 7.695C3.17983 7.70423 3.1671 7.71197 3.15662 7.7232C3.14614 7.73418 3.1394 7.74791 3.13091 7.76089C3.12168 7.77586 3.11219 7.78984 3.10595 7.80656C3.10445 7.81031 3.10146 7.81305 3.10021 7.81705L0.0114815 17.676C0.00748795 17.6885 0.00948452 17.7007 0.00748777 17.713C0.00549101 17.7257 0 17.7372 0 17.7504C0 17.7547 0.00224641 17.7581 0.002496 17.7624C0.00324478 17.7801 0.00823653 17.7963 0.0127292 17.8133C0.0167227 17.828 0.0192187 17.8428 0.0257081 17.8565C0.0324472 17.871 0.0424311 17.8824 0.0519157 17.8954C0.0614003 17.9084 0.0696368 17.9216 0.0816173 17.9326C0.0923498 17.9426 0.105578 17.9491 0.118308 17.9573C0.133283 17.9668 0.14776 17.9765 0.164732 17.983C0.168476 17.9843 0.171222 17.9875 0.175215 17.9885C0.199925 17.9963 0.224884 17.9998 0.249345 18H17.1387C17.1634 18 17.1883 17.9963 17.2133 17.9885C17.2173 17.9873 17.2198 17.9843 17.2235 17.983C17.2407 17.9768 17.2552 17.9671 17.2704 17.9573C17.2829 17.9493 17.2961 17.9426 17.3069 17.9326C17.3189 17.9216 17.3273 17.9081 17.3368 17.8952C17.3461 17.8822 17.3563 17.8707 17.3628 17.8565C17.3698 17.8435 17.3723 17.8285 17.376 17.8138ZM1.17185 0.498939H16.2166C15.4372 2.63023 13.7414 4.37739 11.6196 5.21103C11.4731 5.26819 11.3136 5.32435 11.1446 5.37801C9.57419 5.88544 7.82004 5.88644 6.2441 5.37751C6.07987 5.32585 5.92487 5.27094 5.76937 5.20879C3.61587 4.36191 1.95807 2.65568 1.17185 0.498939ZM5.92737 5.79433C5.98278 5.8133 6.03594 5.83527 6.09285 5.85299C6.92799 6.1228 7.80357 6.25933 8.69537 6.25933C9.58742 6.25933 10.463 6.12255 11.2969 5.85324C11.3531 5.83552 11.4065 5.8168 11.4609 5.79833V7.64208H5.92737V5.79433Z" fill="currentColor"/>
<path d="M12.3787 12.9467C12.3787 14.9766 10.7271 16.6282 8.69714 16.6282C6.66719 16.6282 5.01562 14.9766 5.01562 12.9467C5.01562 10.9167 6.66719 9.26514 8.69714 9.26514C10.7271 9.26514 12.3787 10.9167 12.3787 12.9467Z" fill="white"/>
<path d="M8.69464 13.1962C8.75479 13.1962 8.81519 13.1745 8.86337 13.1303L10.6312 11.5079C10.7328 11.4148 10.7398 11.2568 10.6465 11.1555C10.5534 11.0537 10.3949 11.0469 10.294 11.1403L8.52616 12.7626C8.42458 12.8557 8.41784 13.0137 8.51094 13.1151C8.55986 13.1687 8.62725 13.1962 8.69464 13.1962Z" fill="currentColor"/>
<path d="M8.81381 6.49281C13.1681 6.49281 16.6979 3.58588 16.6979 0H0.929688C0.929688 3.58588 4.45953 6.49281 8.81381 6.49281Z" fill="currentColor"/>
</svg>
</button>
{showScaleTooltip && (
<div class="helper-tooltip-popper meez-tooltip-card" role="tooltip">
<div class="tooltip-section">
<b>Ways to scale your recipe:</b>
<ul>
<li>Click on an ingredient's quantity and update it</li>
<li>Click on the recipe's <b>Total Yield</b> and update it</li>
<li>Change the <b>Batch Size</b> of the recipe</li>
</ul>
</div>
<div class="tooltip-section">
<b>How to convert units of measure:</b>
<ul>
<li>Click on an ingredient's unit of measure &amp; change to the desired unit - the quantity will adjust automatically</li>
</ul>
</div>
</div>
)}
</span>
</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 && (
@@ -124,7 +299,7 @@ export default function RecipeCalculator({ components, units, basisUnitId, yield
)}
</div>
{calculatePercent&&percentMode==="bakers"&&baseItems.length>0&&<section class="bakers-base-summary"><strong>Base</strong><div>{baseItems.map(item=>{const displayUnitId=lineUnits[item.id]??item.amount.unit_id;const displayQuantity=convertItem(item.amount.quantity*validFactor,item.amount.unit_id,displayUnitId,item,units);return <p><span><input aria-label={`${item.label} base quantity`} type="number" min="0" step="any" value={roundForDisplay(displayQuantity)} onInput={(event)=>{const canonicalQuantity=convertItem(Number(event.currentTarget.value),displayUnitId,item.amount.unit_id,item,units);setFactor(item.amount.quantity>0?canonicalQuantity/item.amount.quantity:1)}}/><select aria-label={`${item.label} base unit`} value={displayUnitId} onChange={(event)=>setLineUnits((current)=>({...current,[item.id]:event.currentTarget.value}))}>{itemUnits(item,units).map(unit=><option value={unit.id}>{unit.symbol}</option>)}</select></span><a href={item.href}>{item.label}</a></p>})}</div></section>}
{calculatePercent&&percentMode==="bakers"&&baseItems.length>0&&<section class="bakers-base-summary"><strong>Base</strong><div>{baseItems.map(item=>{const displayUnitId=lineUnits[item.id]??item.amount.unit_id;const displayQuantity=convertItem(item.amount.quantity*validFactor,item.amount.unit_id,displayUnitId,item,units);return <p><span><input aria-label={`${item.label} base quantity`} type="text" inputMode="decimal" style={{ width: `${Math.max(2, String(roundForDisplay(displayQuantity)).length + 0.5)}ch` }} value={roundForDisplay(displayQuantity)} onFocus={(event) => (event.currentTarget as HTMLInputElement).select()} onClick={(event) => (event.currentTarget as HTMLInputElement).select()} onInput={(event)=>{const canonicalQuantity=convertItem(Number(event.currentTarget.value),displayUnitId,item.amount.unit_id,item,units);setFactor(item.amount.quantity>0?canonicalQuantity/item.amount.quantity:1)}}/><select aria-label={`${item.label} base unit`} value={displayUnitId} onChange={(event)=>setLineUnits((current)=>({...current,[item.id]:event.currentTarget.value}))}>{itemUnits(item,units).map(unit=><option value={unit.id}>{unit.symbol}</option>)}</select></span><a href={item.href}>{item.label}</a></p>})}</div></section>}
{components.map((component) => (
<div class="formula-component" key={component.id}>
{Boolean(component.name?.trim()) && <h3 class="formula-component-heading">{component.name}</h3>}
@@ -145,9 +320,38 @@ export default function RecipeCalculator({ components, units, basisUnitId, yield
};
return (
<tr key={item.id} class={item.basisMember ? "basis-row" : ""}>
<td><span class="quantity-control line-quantity"><input aria-label={`${item.label} quantity`} type="number" min="0" step="any" value={roundForDisplay(displayQuantity)} onInput={(event) => changeLineQuantity(Number(event.currentTarget.value))}/><select aria-label={`${item.label} unit`} value={displayUnitId} style={{width:`${(compatibleUnits.find((unit) => unit.id === displayUnitId)?.symbol ?? displayUnitId).length + 0.75}ch`}} onChange={(event) => { const next = event.currentTarget.value; try { convertItem(item.amount.quantity * validFactor, item.amount.unit_id, next, item, units); } catch { event.currentTarget.value = displayUnitId; return; } setLineUnits((current) => ({ ...current, [item.id]: next })); }}>{compatibleUnits.map((unit) => <option value={unit.id} key={unit.id}>{unit.symbol}</option>)}</select></span></td>
<td><span class="quantity-control line-quantity"><input aria-label={`${item.label} quantity`} type="text" inputMode="decimal" style={{ width: `${Math.max(2, String(roundForDisplay(displayQuantity)).length + 0.5)}ch` }} value={roundForDisplay(displayQuantity)} onFocus={(event) => (event.currentTarget as HTMLInputElement).select()} onClick={(event) => (event.currentTarget as HTMLInputElement).select()} onInput={(event) => changeLineQuantity(Number(event.currentTarget.value))}/><select aria-label={`${item.label} unit`} value={displayUnitId} style={{width:`${(compatibleUnits.find((unit) => unit.id === displayUnitId)?.symbol ?? displayUnitId).length + 0.75}ch`}} onChange={(event) => { const next = event.currentTarget.value; try { convertItem(item.amount.quantity * validFactor, item.amount.unit_id, next, item, units); } catch { event.currentTarget.value = displayUnitId; return; } setLineUnits((current) => ({ ...current, [item.id]: next })); }}>{compatibleUnits.map((unit) => <option value={unit.id} key={unit.id}>{unit.symbol}</option>)}</select></span></td>
<td>
<span class="calculator-ingredient-name">{item.href ? <a href={item.href}>{item.label}</a> : item.label}{item.attention&&<span class="ingredient-attention-icon" role="img" aria-label={item.attentionMessage??"Needs attention"} title={item.attentionMessage??"Needs attention"}><svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true"><path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg></span>}</span>
<span class="calculator-ingredient-name">
{item.href ? <a href={item.href}>{item.label}</a> : item.label}
{item.attention && (
<span
class="ingredient-attention-wrapper"
onMouseEnter={() => setActiveAttentionId(item.id)}
onMouseLeave={() => setActiveAttentionId((prev) => (prev === item.id ? null : prev))}
>
<button
type="button"
class="ingredient-attention-icon"
aria-label={item.attentionMessage ?? "Needs attention"}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setActiveAttentionId((prev) => (prev === item.id ? null : item.id));
}}
>
<svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true">
<path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
</svg>
</button>
{activeAttentionId === item.id && (
<div class="ingredient-attention-popper meez-tooltip-card" role="tooltip">
<p>{item.attentionMessage ?? "No conversion equivalency available for this unit."}</p>
</div>
)}
</span>
)}
</span>
{item.optional && <span class="muted"> optional</span>}
{item.notes && <small>{item.notes}</small>}
</td>
@@ -172,7 +376,34 @@ export function DerivedValues({ nutrition, cost, servings, factor }: { nutrition
return <div class="derived-grid">
<div id="nutrition"><NutritionPanel nutrition={nutrition} servings={servings} factor={factor} /></div>
<section id="costing" class="derived-card" aria-labelledby="scaled-cost-heading">
<div class="derived-title"><div><p class="eyebrow">Derived estimate</p><h3 id="scaled-cost-heading">Recipe cost</h3></div><strong>{Math.round(cost.completeness * 100)}% priced</strong></div>
<div class="derived-title">
<div>
<p class="eyebrow">Derived estimate</p>
<h3 id="scaled-cost-heading">Recipe cost</h3>
</div>
<div class="derived-cost-status">
<strong>{Math.round(cost.completeness * 100)}% priced</strong>
{cost.completeness < 1 && (
<span class="cost-help-wrap">
<button class="cost-help-btn" type="button" aria-label="Cost calculation details">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8"/>
</svg>
</button>
<span class="cost-tooltip-popper meez-tooltip-card" role="tooltip">
<b>There is not enough information to calculate cost. Please verify the below:</b>
<ul>
<li>This recipe and all its sub-recipes must include Total Yield</li>
<li>All ingredients in this recipe and its sub-recipes must be defined</li>
<li>All ingredients in this recipe and its sub-recipes must have a purchase cost</li>
<li>All ingredients in this recipe and its sub-recipes must have the needed unit conversion to convert from the unit in the recipe to the purchase unit</li>
<li>If this recipe exists in multiple concepts or locations, there is a cost at each concept/location</li>
</ul>
</span>
</span>
)}
</div>
</div>
{cost.batch != null ? <dl>
<div><dt>Scaled batch</dt><dd>{money.format(cost.batch * factor)}</dd></div>
{cost.perServing != null && <div><dt>Per serving</dt><dd>{money.format(cost.perServing)}</dd></div>}
@@ -222,19 +453,40 @@ function CostLedgerLine({ line, factor, currency, editable, expanded }: { line:C
</svg>
)}
</span>
<span class={`cost-subject-icon ${line.kind}`}>{line.kind==="recipe"?"R":""}</span>
<span class={`cost-subject-icon ${line.kind}`} aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path
fill="currentColor"
d={TYPE_ICONS[line.kind as "recipe" | "ingredient"] ?? TYPE_ICONS.ingredient}
style={TYPE_ICON_TRANSFORMS[line.kind as "recipe" | "ingredient"] ? { transform: TYPE_ICON_TRANSFORMS[line.kind as "recipe" | "ingredient"] } : undefined}
/>
</svg>
</span>
<a href={editable ? editHref : viewHref} class="cost-subject-name" onClick={(e) => e.stopPropagation()}>{line.name}</a>
<span class="cost-attention-slot">
{line.completeness < 1 && (
<span class="cost-attention-badge" title="Cost information is incomplete (missing purchase item or price)" aria-label="Cost information is incomplete">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
<span class="cost-line-help-wrap">
<button
type="button"
class="cost-attention-badge"
aria-label="Cost information is incomplete"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8"/>
</svg>
</button>
<span class="cost-line-tooltip meez-tooltip-card" role="tooltip">
{line.purchase ? "Cost information is incomplete" : "This ingredient is not yet set with a purchase cost and unit"}
</span>
</span>
)}
</span>
<span class="cost-line-value">
{line.kind === "ingredient" ? (
{editable && line.kind === "ingredient" ? (
<a href={editHref} class="cost-editable-link" title={line.cost != null ? "Edit purchase cost" : "Add purchase cost"} onClick={(e) => e.stopPropagation()}>
<span>{line.cost != null ? costValue : "Add cost"}</span>
<span class="cost-edit-icon" aria-hidden="true">✎</span>
@@ -279,20 +531,35 @@ export function LiveCostValues({ cost, yieldQuantity, yieldUnit="g", editable=fa
return (
<section class="recipe-cost-ledger">
<header>
<h2>Recipe Cost</h2>
<header class="cost-ledger-header">
<div class="cost-title-row">
<h2>Recipe Cost</h2>
{cost.completeness < 1 && (
<span class="cost-help-wrap">
<button class="cost-help-btn" type="button" aria-label="Cost calculation help">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8"/>
</svg>
</button>
<span class="cost-tooltip-popper meez-tooltip-card" role="tooltip">
<b>There is not enough information to calculate cost. Please verify the below:</b>
<ul>
<li>This recipe and all its sub-recipes must include Total Yield</li>
<li>All ingredients in this recipe and its sub-recipes must be defined</li>
<li>All ingredients in this recipe and its sub-recipes must have a purchase cost</li>
<li>All ingredients in this recipe and its sub-recipes must have the needed unit conversion to convert from the unit in the recipe to the purchase unit</li>
<li>If this recipe exists in multiple concepts or locations, there is a cost at each concept/location</li>
</ul>
</span>
</span>
)}
</div>
<p>{editable?"Update an ingredients shared purchase cost here. The change is reflected in every recipe that uses it.":"Ingredient and sub-recipe costs used to calculate this recipe."}</p>
</header>
<div class="cost-ledger-heading">
<span class="head-spacer" aria-hidden="true"></span>
<span class="head-icon-spacer" aria-hidden="true"></span>
<span class="head-subject">
Ingredient / Sub-Recipe{" "}
<button type="button" onClick={()=>setAll(true)}>Expand all</button>
<i>|</i>
<button type="button" onClick={()=>setAll(false)}>Collapse all</button>
Ingredient / Sub-Recipe ( <button type="button" onClick={()=>setAll(true)}>Expand all</button><i>|</i><button type="button" onClick={()=>setAll(false)}>Collapse all</button> )
</span>
<span class="head-attention-spacer" aria-hidden="true"></span>
<span class="head-cost">Cost</span>
</div>
<div class={expansion.open?"cost-ledger-lines expand-all":"cost-ledger-lines"}>
+620
View File
@@ -0,0 +1,620 @@
import { useState, useEffect, useRef } from "preact/hooks";
import { createPortal } from "preact/compat";
export interface FilterOption {
id: string;
name: string;
count?: number;
checked: boolean;
}
export interface FilterCategoryConfig {
id: string;
label: string;
icon: string;
iconViewBox?: string;
options: FilterOption[];
emptyMessage?: string;
}
interface Props {
type?: string;
query?: string;
recipeCounts: {
empty: number;
missingYield: number;
placeholder: number;
};
ingredientCounts: {
unused: number;
missingCost: number;
noPurchase: number;
};
itemTypeCounts?: {
recipe: number;
ingredient: number;
book: number;
purchase: number;
};
tagOptions?: FilterOption[];
ingredientOptions?: FilterOption[];
currentParams: Record<string, string | string[]>;
}
const ICONS = {
filter: [
"M18 6.0201C18 4.81608 17.1873 3.79266 16.0736 3.46156L16.0736 0.722412C16.0736 0.301005 15.7425 -9.86801e-08 15.3211 -1.171e-07C14.8996 -1.35521e-07 14.5685 0.331105 14.5685 0.752512L14.5685 3.49166C13.4548 3.79266 12.6421 4.84618 12.6421 6.0502C12.6421 7.28432 13.4548 8.30774 14.5685 8.63884L14.5685 19.7459C14.5685 20.1673 14.8996 20.4984 15.3211 20.4984C15.7425 20.4984 16.0736 20.1673 16.0736 19.7459L16.0736 8.63884C17.1873 8.27764 18 7.25422 18 6.0201ZM16.495 6.0502C16.495 6.68231 15.9833 7.22412 15.3211 7.22412C14.6588 7.22412 14.1471 6.68231 14.1471 6.0502C14.1471 5.41809 14.6588 4.87628 15.3211 4.87628C15.9833 4.87628 16.495 5.41809 16.495 6.0502Z",
"M11.9799 15.0803C11.9799 13.8763 11.1671 12.8529 10.0534 12.5218L10.0534 0.752512C10.0534 0.301005 9.72232 -9.86801e-08 9.30091 -1.171e-07C8.87951 -1.35521e-07 8.5484 0.331105 8.5484 0.752512L8.5484 12.5218C7.43468 12.8228 6.62197 13.8763 6.62197 15.0803C6.62197 16.3145 7.43468 17.3379 8.5484 17.669L8.5484 19.7459C8.5484 20.1673 8.87951 20.4984 9.30091 20.4984C9.72232 20.4984 10.0534 20.1673 10.0534 19.7459L10.0534 17.669C11.1671 17.3379 11.9799 16.3145 11.9799 15.0803ZM10.4748 15.1104C10.4748 15.7426 9.96312 16.2844 9.30091 16.2844C8.6387 16.2844 8.12699 15.7426 8.12699 15.1104C8.12699 14.4783 8.6387 13.9365 9.30091 13.9365C9.96312 13.9365 10.4748 14.4482 10.4748 15.1104Z",
"M5.35791 9.63216C5.35791 8.42814 4.5452 7.40472 3.43148 7.07362L3.43148 0.752512C3.43148 0.301005 3.10037 -9.86801e-08 2.67897 -1.171e-07C2.25756 -1.35521e-07 1.92645 0.331105 1.92645 0.752512L1.92645 7.10372C0.812735 7.40472 2.06111e-05 8.45824 2.05585e-05 9.66226C2.05045e-05 10.8964 0.812735 11.9198 1.92645 12.2509L1.92645 19.7459C1.92645 20.1673 2.25756 20.4984 2.67896 20.4984C3.10037 20.4984 3.43148 20.1673 3.43148 19.7459L3.43148 12.2208C4.5452 11.8897 5.35791 10.8663 5.35791 9.63216ZM3.85289 9.66226C3.85289 10.2944 3.34118 10.8362 2.67897 10.8362C2.01675 10.8362 1.50505 10.2944 1.50505 9.66226C1.50505 9.03015 2.01675 8.48834 2.67897 8.48834C3.34118 8.51844 3.85289 9.03015 3.85289 9.66226Z"
],
attention: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 15h-2v-2h2zm0-4h-2V7h2z",
person: "M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4m0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4",
ingredient: "M13.2445 5.63676C12.3794 3.69079 10.9321 2.34779 8.61591 2.62707L7.52332 2.75825L7.6345 3.90089C7.76967 5.28511 8.82346 6.49035 9.62045 7.10812C8.82346 6.94913 8.19509 7.1927 7.94758 7.30796C6.76598 7.85825 5.83492 8.52233 5.08642 9.67906C3.97889 11.3907 3.82218 13.6224 4.83851 16.3608C6.38221 20.52 8.74916 22.2887 11.7148 20.9902L11.9128 20.8786C11.9676 20.84 12.0117 20.8259 12.0764 20.8259C12.239 20.8259 12.5688 20.8933 13.0271 21.0356C16.0346 22.1129 18.2618 20.3151 19.4114 16.2811C20.8147 11.3556 18.5467 7.8667 15.9963 7.25499C15.5181 7.10206 14.7212 6.94913 14.243 7.10206C14.7283 6.28059 15.4292 5.8504 15.7541 5.7779C16.2509 5.66685 16.5761 5.11299 16.4799 4.54086C16.3836 3.96862 15.9758 3.74559 15.4514 3.80162C14.86 4.05145 14.1231 4.4075 13.5387 5.17798L13.2445 5.63676Z",
station: "M18 10V7.17c0-1.62-1.22-3.08-2.84-3.17-1.21-.06-2.27.59-2.8 1.57-.35.65.17 1.43.91 1.43h.01c.34 0 .68-.16.84-.46.16-.32.5-.54.88-.54.55 0 1 .45 1 1v3H8c1.1 0 2-.9 2-2V5c0-.55-.45-1-1-1H5c-.55 0-1 .45-1 1v3c0 1.1.9 2 2 2H3c-.55 0-1 .45-1 1s.45 1 1 1h1v7c0 .55.45 1 1 1h14c.55 0 1-.45 1-1v-7h1c.55 0 1-.45 1-1s-.45-1-1-1zm-5 8h-2v-6h2z",
tag: "M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12z",
plus: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
close: "M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
chevronDown: "M7.41 8.59 12 13.17l4.59-4.58L18 10l-6 6-6-6z",
chevronUp: "M7.41 15.41 12 10.83l4.59 4.58L18 14l-6-6-6 6z",
search: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
};
export default function WorkspaceFilterBar({
type,
query = "",
recipeCounts,
ingredientCounts,
itemTypeCounts,
tagOptions = [],
ingredientOptions = [],
currentParams
}: Props) {
const [menuOpen, setMenuOpen] = useState(false);
const [openChipId, setOpenChipId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
// Mobile detection state
const [activeMobileCatId, setActiveMobileCatId] = useState<string | null>(null);
const [menuAlignRight, setMenuAlignRight] = useState(false);
const [chipAlignRight, setChipAlignRight] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
// Close menus when clicking outside (desktop)
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (window.innerWidth > 980) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setMenuOpen(false);
}
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
const target = e.target as HTMLElement;
if (!target.closest(".filter-rule-chip-main")) {
setOpenChipId(null);
}
}
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleToggleMenu = () => {
if (!menuOpen && menuRef.current) {
const rect = menuRef.current.getBoundingClientRect();
setMenuAlignRight(rect.left + 280 > window.innerWidth - 16);
}
setActiveMobileCatId(null);
setMenuOpen(!menuOpen);
};
// Parse active filter parameters
const isRecipe = type === "recipe";
const isIngredient = type === "ingredient";
const isGlobalSearch = !type || Boolean(query);
const emptyRecipeActive = currentParams["empty_recipe"] === "1";
const missingYieldActive = currentParams["missing_yield"] === "1";
const placeholderStepsActive = currentParams["placeholder_steps"] === "1";
const unusedActive = currentParams["unused"] === "1";
const missingCostActive = currentParams["missing_cost"] === "1";
const noPurchaseActive = currentParams["no_purchase"] === "1" || currentParams["no_usda"] === "1";
const selectedItemTypes = Array.isArray(currentParams["item_type"])
? currentParams["item_type"]
: currentParams["item_type"]
? [currentParams["item_type"] as string]
: [];
const selectedTags = Array.isArray(currentParams["tag"])
? currentParams["tag"]
: currentParams["tag"]
? [currentParams["tag"] as string]
: [];
const selectedIngredients = Array.isArray(currentParams["ingredient"])
? currentParams["ingredient"]
: currentParams["ingredient"]
? [currentParams["ingredient"] as string]
: [];
// Determine configured categories
const categories: FilterCategoryConfig[] = [];
if (isRecipe) {
categories.push({
id: "needs_attention",
label: "Needs Attention",
icon: ICONS.attention,
options: [
{ id: "empty_recipe", name: "Empty recipes", count: recipeCounts.empty, checked: emptyRecipeActive },
{ id: "missing_yield", name: "Missing total yield", count: recipeCounts.missingYield, checked: missingYieldActive },
{ id: "placeholder_steps", name: "Contain undefined ingredients / steps", count: recipeCounts.placeholder, checked: placeholderStepsActive }
]
});
categories.push({ id: "tags", label: "Tags", icon: ICONS.tag, options: tagOptions, emptyMessage: "No additional filter options available." });
categories.push({ id: "ingredients", label: "Ingredients", icon: ICONS.ingredient, options: ingredientOptions, emptyMessage: "No additional filter options available." });
} else if (isIngredient) {
categories.push({
id: "needs_attention",
label: "Needs Attention",
icon: ICONS.attention,
options: [
{ id: "unused", name: "Unused ingredients", count: ingredientCounts.unused, checked: unusedActive },
{ id: "missing_cost", name: "Missing cost", count: ingredientCounts.missingCost, checked: missingCostActive },
{ id: "no_purchase", name: "No vendor purchase associated", count: ingredientCounts.noPurchase, checked: noPurchaseActive }
]
});
categories.push({ id: "tags", label: "Tags", icon: ICONS.tag, options: tagOptions, emptyMessage: "No additional filter options available." });
} else {
categories.push({
id: "item_type",
label: "Item Type",
icon: ICONS.tag,
options: [
{ id: "recipe", name: "Recipes", count: itemTypeCounts?.recipe, checked: selectedItemTypes.includes("recipe") },
{ id: "ingredient", name: "Ingredients", count: itemTypeCounts?.ingredient, checked: selectedItemTypes.includes("ingredient") },
{ id: "book", name: "Recipe books", count: itemTypeCounts?.book, checked: selectedItemTypes.includes("book") },
{ id: "purchase", name: "Purchase items", count: itemTypeCounts?.purchase, checked: selectedItemTypes.includes("purchase") }
]
});
categories.push({ id: "tags", label: "Tags", icon: ICONS.tag, options: tagOptions, emptyMessage: "No additional filter options available." });
categories.push({ id: "ingredients", label: "Ingredients", icon: ICONS.ingredient, options: ingredientOptions, emptyMessage: "No additional filter options available." });
}
// Helper to check if a category has active selected options
const hasActiveOptions = (cat: FilterCategoryConfig) => {
return cat.options.some(opt => opt.checked);
};
const categoriesWithActiveSelections = categories.filter(c => hasActiveOptions(c));
const totalActiveCount = categoriesWithActiveSelections.length;
// The chips visible on screen
const visibleCategoryIds = Array.from(
new Set([
...categoriesWithActiveSelections.map(c => c.id),
...(openChipId ? [openChipId] : [])
])
);
const navigateWithParams = (newParams: URLSearchParams) => {
window.location.href = `${window.location.pathname}?${newParams.toString()}`;
};
const handleAddCategory = (catId: string) => {
if (window.innerWidth <= 980) {
setActiveMobileCatId(catId);
} else {
setOpenChipId(catId);
setMenuOpen(false);
}
setSearchTerm("");
};
const handleRemoveCategory = (catId: string) => {
if (openChipId === catId) setOpenChipId(null);
if (activeMobileCatId === catId) setActiveMobileCatId(null);
const params = new URLSearchParams(window.location.search);
const cat = categories.find(c => c.id === catId);
if (cat) {
if (catId === "item_type") {
params.delete("item_type");
} else if (catId === "tags") {
params.delete("tag");
} else if (catId === "ingredients") {
params.delete("ingredient");
} else {
cat.options.forEach(opt => params.delete(opt.id));
params.delete("attention");
}
navigateWithParams(params);
}
};
const handleClearAll = () => {
setOpenChipId(null);
setMenuOpen(false);
setActiveMobileCatId(null);
const params = new URLSearchParams();
if (type) params.set("type", type);
if (query) params.set("q", query);
navigateWithParams(params);
};
const handleToggleOption = (catId: string, optionId: string, currentChecked: boolean) => {
const params = new URLSearchParams(window.location.search);
if (catId === "item_type") {
const current = params.getAll("item_type");
params.delete("item_type");
if (currentChecked) {
current.filter(v => v !== optionId).forEach(v => params.append("item_type", v));
} else {
[...current, optionId].forEach(v => params.append("item_type", v));
}
} else if (catId === "tags") {
const current = params.getAll("tag");
params.delete("tag");
if (currentChecked) {
current.filter(v => v !== optionId).forEach(v => params.append("tag", v));
} else {
[...current, optionId].forEach(v => params.append("tag", v));
}
} else if (catId === "ingredients") {
const current = params.getAll("ingredient");
params.delete("ingredient");
if (currentChecked) {
current.filter(v => v !== optionId).forEach(v => params.append("ingredient", v));
} else {
[...current, optionId].forEach(v => params.append("ingredient", v));
}
} else {
if (currentChecked) {
params.delete(optionId);
} else {
params.set(optionId, "1");
}
}
navigateWithParams(params);
};
const handleOpenChip = (catId: string, e?: MouseEvent) => {
if (window.innerWidth <= 980) {
setActiveMobileCatId(catId);
setMenuOpen(true);
} else {
const isOpening = openChipId !== catId;
if (isOpening) {
const target = (e?.currentTarget as HTMLElement)?.closest(".filter-rule-item-group");
if (target) {
const rect = target.getBoundingClientRect();
setChipAlignRight(rect.left + 270 > window.innerWidth - 16);
}
}
setOpenChipId(openChipId === catId ? null : catId);
}
setSearchTerm("");
};
const activeMobileCat = activeMobileCatId ? categories.find(c => c.id === activeMobileCatId) : null;
const activeMobileFilteredOptions = activeMobileCat
? activeMobileCat.options.filter(opt => opt.name.toLowerCase().includes(searchTerm.toLowerCase()))
: [];
return (
<div class="workspace-filter-system">
{/* 1. Main Filter Button Trigger in Pills Row */}
<div class="workspace-filter-trigger-wrapper" ref={menuRef}>
<button
type="button"
class={`workspace-filter-btn ${totalActiveCount > 0 ? "active" : ""}`}
onClick={handleToggleMenu}
aria-expanded={menuOpen}
aria-haspopup="menu"
>
<svg class="filter-icon" viewBox="0 0 18 21" aria-hidden="true" focusable="false">
{ICONS.filter.map((d, i) => (
<path key={i} fill="currentColor" d={d} />
))}
</svg>
<span>Filter{totalActiveCount > 0 ? ` (${totalActiveCount})` : ""}</span>
</button>
{/* Desktop Filter Categories Menu */}
{menuOpen && (
<div class={`workspace-filter-menu-popover desktop-only-filter-popover ${menuAlignRight ? "align-right" : "align-left"}`} role="menu">
<div class="filter-menu-header">
<h4>Filter</h4>
</div>
<ul class="filter-menu-list">
{categories.map(cat => {
const isActive = hasActiveOptions(cat);
return (
<li key={cat.id}>
<button
type="button"
class={`filter-menu-item ${isActive ? "active" : ""}`}
onClick={() => handleAddCategory(cat.id)}
>
<div class="filter-item-left">
<span class="filter-item-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={cat.icon} />
</svg>
</span>
<span class="filter-item-label">{cat.label}</span>
</div>
<span class="filter-item-plus" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path fill="currentColor" d={ICONS.plus} />
</svg>
</span>
</button>
</li>
);
})}
</ul>
<div class="filter-menu-footer">
<button
type="button"
class="filter-clear-all-btn"
disabled={totalActiveCount === 0}
onClick={handleClearAll}
>
Clear all
</button>
</div>
</div>
)}
</div>
{/* 2. Mobile Filter & Sort Modal (Meez Parity) */}
{menuOpen && typeof document !== "undefined" && createPortal(
<div class="mobile-filter-backdrop" role="dialog" aria-modal="true" aria-label="Filter and Sort">
<div class="mobile-filter-sheet">
<header class="mobile-filter-header">
{activeMobileCat ? (
<button
type="button"
class="mobile-filter-back-btn"
onClick={() => {
setActiveMobileCatId(null);
setSearchTerm("");
}}
aria-label="Back to categories"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
</button>
) : (
<div class="mobile-filter-header-spacer" />
)}
<h3>{activeMobileCat ? activeMobileCat.label : "Filter & Sort"}</h3>
<button
type="button"
class="mobile-filter-close-btn"
onClick={() => {
setMenuOpen(false);
setActiveMobileCatId(null);
}}
aria-label="Close filter"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={ICONS.close} />
</svg>
</button>
</header>
<div class="mobile-filter-body">
{activeMobileCat ? (
/* Category Options View */
<div class="mobile-filter-category-view">
{activeMobileCat.options.length > 0 ? (
<>
<div class="filter-chip-search">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={ICONS.search} />
</svg>
<input
type="search"
placeholder={`Search ${activeMobileCat.label.toLowerCase()}`}
value={searchTerm}
onInput={e => setSearchTerm((e.target as HTMLInputElement).value)}
autoFocus
/>
</div>
<ul class="filter-chip-options-list mobile-options-list">
{activeMobileFilteredOptions.length > 0 ? (
activeMobileFilteredOptions.map(opt => (
<li key={opt.id}>
<label class="filter-chip-option-label">
<input
type="checkbox"
checked={opt.checked}
onChange={() => handleToggleOption(activeMobileCat.id, opt.id, opt.checked)}
/>
<span class="option-name">{opt.name}</span>
{typeof opt.count === "number" && (
<span class="option-count">({opt.count})</span>
)}
</label>
</li>
))
) : (
<li class="filter-chip-empty">No matching options.</li>
)}
</ul>
</>
) : (
<div class="filter-chip-empty-message">
{activeMobileCat.emptyMessage || "No additional filter options available."}
</div>
)}
</div>
) : (
/* Main Filter & Sort Menu */
<div class="mobile-filter-main-view">
<div class="mobile-filter-section-heading">
<svg viewBox="0 0 18 21" aria-hidden="true" class="mobile-section-icon">
{ICONS.filter.map((d, i) => (
<path key={i} fill="currentColor" d={d} />
))}
</svg>
<span>Filter</span>
</div>
<div class="mobile-filter-card">
{categories.map(cat => {
const isActive = hasActiveOptions(cat);
const activeOptsCount = cat.options.filter(o => o.checked).length;
return (
<button
key={cat.id}
type="button"
class={`mobile-filter-card-item ${isActive ? "active" : ""}`}
onClick={() => handleAddCategory(cat.id)}
>
<div class="filter-item-left">
<span class="filter-item-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={cat.icon} />
</svg>
</span>
<span class="filter-item-label">{cat.label}</span>
{activeOptsCount > 0 && (
<span class="mobile-active-badge">({activeOptsCount})</span>
)}
</div>
<span class="mobile-item-chevron" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" />
</svg>
</span>
</button>
);
})}
</div>
<div class="mobile-clear-card">
<button
type="button"
class="mobile-clear-btn"
disabled={totalActiveCount === 0}
onClick={handleClearAll}
>
Clear all
</button>
</div>
</div>
)}
</div>
</div>
</div>,
document.body
)}
{/* 3. Secondary Active Filter Rule Chips Bar (Horizontal Scroll on Mobile) */}
{visibleCategoryIds.length > 0 && (
<div class="filter-rules-row" role="region" aria-label="Active filters">
{visibleCategoryIds.map((catId, index) => {
const cat = categories.find(c => c.id === catId);
if (!cat) return null;
const isOpen = openChipId === catId;
const filteredOptions = cat.options.filter(opt =>
opt.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div key={catId} class="filter-rule-item-group">
{index > 0 && <span class="filter-connector-and">And</span>}
<div class={`filter-rule-chip ${isOpen ? "open" : ""}`} data-cat-id={catId}>
<button
type="button"
class="filter-rule-chip-main"
onClick={(e) => handleOpenChip(catId, e as any)}
aria-expanded={isOpen}
>
<span class="filter-chip-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={cat.icon} />
</svg>
</span>
<span class="filter-chip-label">{cat.label}:</span>
<span class="filter-chip-arrow">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={isOpen ? ICONS.chevronUp : ICONS.chevronDown} />
</svg>
</span>
</button>
<button
type="button"
class="filter-rule-chip-close"
onClick={() => handleRemoveCategory(catId)}
aria-label={`Remove ${cat.label} filter`}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={ICONS.close} />
</svg>
</button>
</div>
{/* Dropdown Popover for Active Chip (Desktop) */}
{isOpen && (
<div class={`filter-chip-popover desktop-only-filter-popover ${chipAlignRight ? "align-right" : "align-left"}`} ref={popoverRef}>
{cat.options.length > 0 ? (
<>
<div class="filter-chip-search">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={ICONS.search} />
</svg>
<input
type="search"
placeholder={`Search ${cat.label.toLowerCase()}`}
value={searchTerm}
onInput={e => setSearchTerm((e.target as HTMLInputElement).value)}
autoFocus
/>
</div>
<ul class="filter-chip-options-list">
{filteredOptions.length > 0 ? (
filteredOptions.map(opt => (
<li key={opt.id}>
<label class="filter-chip-option-label">
<input
type="checkbox"
checked={opt.checked}
onChange={() => handleToggleOption(catId, opt.id, opt.checked)}
/>
<span class="option-name">{opt.name}</span>
{typeof opt.count === "number" && (
<span class="option-count">({opt.count})</span>
)}
</label>
</li>
))
) : (
<li class="filter-chip-empty">No matching options.</li>
)}
</ul>
</>
) : (
<div class="filter-chip-empty-message">
{cat.emptyMessage || "No additional filter options available."}
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
}
+54 -6
View File
@@ -1,3 +1,4 @@
import { useEffect, useRef } from "preact/hooks";
import type { RecipeStructure } from "../../lib/database";
import type { DragState, Option, PercentMode } from "./types";
@@ -38,6 +39,12 @@ type Props = {
const normal = (value: string) =>
value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
/** Commit a number input's value, returning the parsed number or undefined. */
const commitNumericInput = (input: HTMLInputElement): number | undefined => {
const num = Number(input.value);
return Number.isFinite(num) ? num : undefined;
};
export default function RecipeItemRow({
item,
componentIndex,
@@ -67,6 +74,31 @@ export default function RecipeItemRow({
onCreatePendingIngredient,
onSetPercent,
}: Props) {
// Uncontrolled refs for numeric inputs — avoids Preact fighting
// intermediate values like "2." during decimal entry.
const qtyInputRef = useRef<HTMLInputElement>(null);
const qtyTracker = useRef(item.quantity);
useEffect(() => {
// Sync DOM value when parent changes externally (e.g. percent-driven)
if (item.quantity !== qtyTracker.current && qtyInputRef.current) {
qtyTracker.current = item.quantity;
if (document.activeElement !== qtyInputRef.current) {
qtyInputRef.current.value = String(item.quantity);
}
}
}, [item.quantity]);
const pctInputRef = useRef<HTMLInputElement>(null);
const percentDisplay = percentValue != null ? Math.round(percentValue * 1000) / 1000 : undefined;
const pctTracker = useRef(percentDisplay);
useEffect(() => {
if (percentDisplay !== pctTracker.current && pctInputRef.current) {
pctTracker.current = percentDisplay;
if (document.activeElement !== pctInputRef.current) {
pctInputRef.current.value = percentDisplay != null ? String(percentDisplay) : "";
}
}
}, [percentDisplay]);
const isPending = Boolean(
item.ingredient_id &&
pendingIngredients.some((entry) => entry.id === item.ingredient_id)
@@ -121,14 +153,21 @@ export default function RecipeItemRow({
>
<td class="quantity-column">
<input
ref={qtyInputRef}
type="number"
min="0"
step="any"
aria-label="Quantity"
value={item.quantity}
onInput={(event) =>
onUpdateQuantity(Number(event.currentTarget.value))
}
defaultValue={item.quantity}
onBlur={(event) => {
const num = commitNumericInput(event.currentTarget);
if (num != null) {
qtyTracker.current = num;
onUpdateQuantity(num);
} else {
event.currentTarget.value = String(item.quantity);
}
}}
/>
</td>
<td class="unit-column">
@@ -246,6 +285,7 @@ export default function RecipeItemRow({
<span title="A weight equivalency is required"></span>
) : (
<input
ref={pctInputRef}
aria-label={`${label} percentage`}
type="number"
min="0"
@@ -255,8 +295,16 @@ export default function RecipeItemRow({
: undefined
}
step="any"
value={Math.round(percentValue * 1000) / 1000}
onInput={(event) => onSetPercent(Number(event.currentTarget.value))}
defaultValue={percentDisplay}
onBlur={(event) => {
const num = commitNumericInput(event.currentTarget);
if (num != null) {
pctTracker.current = num;
onSetPercent(num);
} else {
event.currentTarget.value = percentDisplay != null ? String(percentDisplay) : "";
}
}}
/>
)}
</td>
+72
View File
@@ -0,0 +1,72 @@
/**
* Formulation - Centralized Icon Library
* Exact SVG icons and path definitions matching Meez design system
*/
export type TabIconKey = "method" | "costing" | "equivalencies" | "nutrition" | "formula" | "overview";
export type EntityIconKey = "recipe" | "ingredient" | "book" | "purchase" | "inventory";
export type UiIconKey = "chevronRight" | "chevronLeft" | "edit" | "more" | "plus" | "search" | "info" | "warning";
/**
* Tab Icons (viewBox & SVG path) used in workspace & entity read subheaders
*/
export const TAB_ICONS: Record<TabIconKey, string> = {
method: "M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5m0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5m0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5M7 19h14v-2H7zm0-6h14v-2H7zm0-8v2h14V5z",
costing: "M11.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4",
equivalencies: "M19.4 3.3h-6.6v-.5c0-.4-.3-.7-.8-.7-.4 0-.8.3-.8.7v.5H4.6L0 14s.2 3.8 4.7 3.8S9.4 14 9.4 14L6.1 6.2h5.1v15.7h1.5V6.2h5.1L14.6 14s.2 3.8 4.7 3.8S24 14 24 14L19.4 3.3zM7.7 14H1.5l3.1-7.4L7.7 14zm8.5 0l3.1-7.4 3.1 7.4h-6.2z",
nutrition: "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35zM7.5 5C5.57 5 4 6.57 4 8.5c0 2.24 1.95 4.67 4.9 7.37L12 18.72l3.1-2.85C18.05 13.17 20 10.74 20 8.5c0-1.93-1.57-3.5-3.5-3.5-1.54 0-2.94.99-3.57 2.36h-1.87C10.44 5.99 9.04 5 7.5 5zm-3 6.5h3.9l1.6-3.2 2.2 5.3 2-4.6.8.8H19.5v1.4h-3.3l-1.2-1.2-1.8 4.2-2.3-5.5-1.7 3.4H4.5v-1.4z",
formula: "M7.57975 2.24434L7.65872 2.13161C8.13511 1.47697 8.63143 1.08241 9.16762 0.957718C9.57278 0.863497 9.96486 1.18135 10.0434 1.66766C10.1219 2.15397 9.85705 2.62459 9.45189 2.71881C9.18697 2.78042 8.8382 3.1513 8.44252 3.84957C8.78173 3.85543 9.10757 3.88576 9.42032 3.94078C11.9002 4.37705 13.577 7.45745 12.4331 11.6424C11.496 15.0703 9.68081 16.5977 7.229 15.682C6.85528 15.5611 6.58673 15.5046 6.4542 15.5046C6.40147 15.5046 6.36601 15.5163 6.32134 15.5491L6.15949 15.644C3.74207 16.7473 1.8122 15.2446 0.553846 11.7108C-0.274666 9.38411 -0.146354 7.48777 0.75651 6.03344C1.3667 5.05056 2.12543 4.48603 3.08872 4.01846C3.29048 3.92053 3.51746 3.85185 3.7715 3.81087C3.24172 3.20799 2.94401 2.30051 2.83382 1.12442L2.74286 0.153477L3.63312 0.0415436C5.52154 -0.195889 6.8746 0.590513 7.57975 2.24434ZM3.88234 5.71589C3.2053 6.04451 2.69949 6.42087 2.31601 7.03856C1.73369 7.97655 1.64642 9.26637 2.2885 11.0695C3.21226 13.6636 4.17073 14.4453 5.3377 13.9576C5.67319 13.7379 6.05207 13.6244 6.4542 13.6244C6.81471 13.6244 7.24894 13.7157 7.82562 13.9033C9.09594 14.3772 9.97129 13.6407 10.6554 11.1379C11.5187 7.9797 10.4582 6.03137 9.10636 5.79357C8.42613 5.6739 7.60929 5.71807 6.65655 5.93628L6.446 5.9845L6.2363 5.93257C4.98214 5.62199 4.16221 5.58004 3.88234 5.71589ZM4.69764 1.88235C4.80315 2.16337 4.92885 2.34891 5.06454 2.4469C5.26392 2.5909 5.53003 2.73516 5.86115 2.87726C5.58815 2.34716 5.20687 2.02464 4.69764 1.88235Z",
overview: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"
};
export const TAB_VIEWBOXES: Record<TabIconKey, string> = {
method: "0 0 24 24",
costing: "0 0 24 24",
equivalencies: "0 0 24 24",
nutrition: "0 0 24 24",
formula: "0 0 14 16",
overview: "0 0 24 24"
};
/**
* Entity Type Circular Badge Icons
*/
export const TYPE_ICONS: Record<EntityIconKey, string> = {
recipe: "M13.125 0C13.6428 0 14.0625 0.419733 14.0625 0.9375V14.0625C14.0625 14.5803 13.6428 15 13.125 15H0.9375C0.419733 15 0 14.5803 0 14.0625V0.9375C0 0.419733 0.419733 0 0.9375 0H13.125ZM12.1875 1.875H1.875V13.125H12.1875V1.875ZM11.25 9.375V11.25H5.625V9.375H11.25ZM4.6875 9.375V11.25H2.8125V9.375H4.6875ZM11.25 6.5625V8.4375H5.625V6.5625H11.25ZM4.6875 6.5625V8.4375H2.8125V6.5625H4.6875ZM11.25 3.75V5.625H5.625V3.75H11.25ZM4.6875 3.75V5.625H2.8125V3.75H4.6875Z",
ingredient: "M7.57975 2.24434L7.65872 2.13161C8.13511 1.47697 8.63143 1.08241 9.16762 0.957718C9.57278 0.863497 9.96486 1.18135 10.0434 1.66766C10.1219 2.15397 9.85705 2.62459 9.45189 2.71881C9.18697 2.78042 8.8382 3.1513 8.44252 3.84957C8.78173 3.85543 9.10757 3.88576 9.42032 3.94078C11.9002 4.37705 13.577 7.45745 12.4331 11.6424C11.496 15.0703 9.68081 16.5977 7.229 15.682C6.85528 15.5611 6.58673 15.5046 6.4542 15.5046C6.40147 15.5046 6.36601 15.5163 6.32134 15.5491L6.15949 15.644C3.74207 16.7473 1.8122 15.2446 0.553846 11.7108C-0.274666 9.38411 -0.146354 7.48777 0.75651 6.03344C1.3667 5.05056 2.12543 4.48603 3.08872 4.01846C3.29048 3.92053 3.51746 3.85185 3.7715 3.81087C3.24172 3.20799 2.94401 2.30051 2.83382 1.12442L2.74286 0.153477L3.63312 0.0415436C5.52154 -0.195889 6.8746 0.590513 7.57975 2.24434ZM3.88234 5.71589C3.2053 6.04451 2.69949 6.42087 2.31601 7.03856C1.73369 7.97655 1.64642 9.26637 2.2885 11.0695C3.21226 13.6636 4.17073 14.4453 5.3377 13.9576C5.67319 13.7379 6.05207 13.6244 6.4542 13.6244C6.81471 13.6244 7.24894 13.7157 7.82562 13.9033C9.09594 14.3772 9.97129 13.6407 10.6554 11.1379C11.5187 7.9797 10.4582 6.03137 9.10636 5.79357C8.42613 5.6739 7.60929 5.71807 6.65655 5.93628L6.446 5.9845L6.2363 5.93257C4.98214 5.62199 4.16221 5.58004 3.88234 5.71589ZM4.69764 1.88235C4.80315 2.16337 4.92885 2.34891 5.06454 2.4469C5.26392 2.5909 5.53003 2.73516 5.86115 2.87726C5.58815 2.34716 5.20687 2.02464 4.69764 1.88235Z",
book: "M14.0347 0V16H3.12658C1.50098 16 0.527344 15.0264 0.527344 13.4008V2.59923C0.527344 0.97364 1.50098 0 3.12658 0H14.0347ZM12.1594 12.6763L3.95747 12.6765C2.75227 12.6765 2.40226 12.9098 2.40226 13.4008C2.40226 13.9909 2.53647 14.1251 3.12658 14.1251H12.1598L12.1594 12.6763ZM12.1598 1.87492H3.12658C2.53647 1.87492 2.40226 2.00913 2.40226 2.59923L2.40185 10.9999C2.85223 10.8678 3.37428 10.8015 3.95747 10.8015L12.1594 10.8014L12.1598 1.87492ZM10.0768 3.80157V5.67649H4.45204V3.80157H10.0768Z",
purchase: "M19.5 3.5 18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2 4.5 3.5 3 2v20l1.5-1.5L6 22l1.5-1.5L9 22l1.5-1.5L12 22l1.5-1.5L15 22l1.5-1.5L18 22l1.5-1.5L21 22V2l-1.5 1.5zM19 19.09H5V4.91h14v14.18zM6 15h12v2H6zm0-4h12v2H6zm0-4h12v2H6z",
inventory: "M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM7 10h2v7H7zm4-3h2v10h-2zm4 6h2v4h-2z"
};
export const TYPE_ICON_TRANSFORMS: Record<EntityIconKey, string | undefined> = {
recipe: "scale(1.1, 1.1) translate(4px, 3.5px)",
ingredient: "scale(1.3, 1.3) translate(2.5px, 1px)",
book: "scale(1.1, 1.1) translate(3.5px, 2.5px)",
purchase: undefined,
inventory: undefined
};
/**
* General UI & Action Icons
*/
export const UI_ICONS: Record<UiIconKey, string> = {
chevronRight: "M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z",
chevronLeft: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z",
edit: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
more: "M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z",
plus: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
search: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
info: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z",
warning: "M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8"
};
/**
* Generate tab icon markup for Astro / HTML templates
*/
export function getTabIconHtml(key: TabIconKey): string {
const viewBox = TAB_VIEWBOXES[key] || "0 0 24 24";
const path = TAB_ICONS[key];
if (!path) return "";
return `<span class="recipe-tab-icon"><svg viewBox="${viewBox}" aria-hidden="true"><path fill="currentColor" d="${path}"/></svg></span>`;
}
+5
View File
@@ -155,6 +155,9 @@ export type DirectoryRecipeRow = {
yield_unit_id: string;
item_count: number;
placeholder_count: number;
tags_json?: string;
categories_json?: string;
station?: string | null;
};
export type DirectoryIngredientRow = {
@@ -164,6 +167,8 @@ export type DirectoryIngredientRow = {
recipe_count: number;
price_count: number;
nutrition_count: number;
tags_json?: string;
categories_json?: string;
};
export type DirectoryBookRow = {
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { openDatabase } from "../lib/database";
import { createMcpTools } from "./tools";
describe("MCP Tools & Domain Integration", () => {
describe("MCP Tools & Domain Integration", { timeout: 20000 }, () => {
it("searches recipes with keywords and filters", () => {
const db = openDatabase({ readOnly: true });
expect(db).toBeDefined();
+7 -3
View File
@@ -191,12 +191,12 @@ export function createMcpTools(getDb: () => DatabaseSync) {
}) {
const db = getDb();
let recipeId = args.id;
const existing = recipeId ? (db.prepare("SELECT save_version FROM recipes WHERE id = ?").get(recipeId) as any) : null;
let recipeId: string;
const existing = args.id ? (db.prepare("SELECT save_version FROM recipes WHERE id = ?").get(args.id) as any) : null;
const isNew = !existing;
if (isNew) {
if (!recipeId) {
if (!args.id) {
const base = args.title
.toLowerCase()
.normalize("NFKD")
@@ -208,6 +208,8 @@ export function createMcpTools(getDb: () => DatabaseSync) {
while (db.prepare("SELECT 1 FROM recipes WHERE id = ?").get(recipeId)) {
recipeId = `${base}_${suffix++}`;
}
} else {
recipeId = args.id;
}
db.prepare(
@@ -231,6 +233,8 @@ export function createMcpTools(getDb: () => DatabaseSync) {
db.prepare("INSERT INTO recipe_components (recipe_id, id, position, name, notes_json) VALUES (?, 'main', 1, 'Main', '[]')").run(recipeId);
db.prepare("INSERT INTO recipe_steps (recipe_id, id, position, instruction, critical_control_point) VALUES (?, 'step_1', 1, 'Placeholder.', 0)").run(recipeId);
} else {
recipeId = args.id!;
}
const structure = {
+371 -208
View File
@@ -1,208 +1,371 @@
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 300;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Light.woff2') format('woff2');
}
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 400;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Book.woff2') format('woff2');
}
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 500;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 700;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Bold.woff2') format('woff2');
}
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 900;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Black.woff2') format('woff2');
}
:root {
--ink: #050841;
--ink-dark: #050841;
--ink-medium: #202962;
--ink-secondary: #3C4679;
--muted: #8283a0;
--muted-light: #a5a9c1;
--paper: #ffffff;
--card: #ffffff;
--card-alt: #fafbfd;
--card-paper: #f1f5fe;
--line: #edf0f5;
--line-subtle: #f4f5f8;
--line-focus: #3d5df6;
--blue: #3d5df6;
--blue-hover: #1236e1;
--blue-active: #001992;
--blue-light: #647df8;
--blue-tint: #f1f5fe;
--blue-badge: #dbe4ff;
--green: #40b49a;
--green-dark: #20ab85;
--green-tint: #abddd1;
--orange: #f3a642;
--red: #f63d48;
--red-dark: #bc0020;
--red-tint: #fff5f6;
--purple: #6234f2;
--pink-badge: #ffd2f5;
--serif: "CircularCustCapNum", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--sans: "CircularCustCapNum", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
* { box-sizing: border-box; }
html {
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
scroll-behavior: smooth;
scrollbar-gutter: stable;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
line-height: 1.5;
}
body, button, input, select, textarea {
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body { margin: 0; min-height: 100vh; }
a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: .2em; }
.shell { width: min(1160px, calc(100% - 40px)); margin-inline: auto; }
/* Sticky Detail Utility Header */
.detail-utility {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 48px;
background: #ffffff;
border-bottom: 1px solid #ececec;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.detail-utility-left {
display: flex;
align-items: center;
gap: 12px;
}
.detail-utility-home-link {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: #050841;
font-size: 13px;
font-weight: 500;
}
.detail-utility-avatar {
width: 24px;
height: 24px;
background: #050841;
color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
}
.detail-utility-divider {
color: #a5a9c1;
font-size: 13px;
}
.detail-utility-crumb {
color: #050841;
font-size: 13px;
font-weight: 500;
}
.detail-utility-right {
display: flex;
align-items: center;
gap: 12px;
}
.detail-search-bar {
display: flex;
align-items: center;
gap: 8px;
background: #ffffff;
border: 1px solid #ececec;
border-radius: 4px;
padding: 4px 10px;
width: 360px;
}
.detail-search-bar svg {
color: #8283a0;
}
.detail-search-bar input {
border: none;
background: transparent;
outline: none;
font-size: 13px;
color: #050841;
width: 100%;
}
.detail-search-bar input::placeholder {
color: #8283a0;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.site-header {
min-height: 64px;
padding-inline: max(16px, calc((100vw - 1180px) / 2));
background: rgba(255, 255, 255, .96);
box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
display: flex;
align-items: center;
justify-content: space-between;
}
.brand {
display: flex;
gap: 10px;
font-size: 1rem;
font-weight: 700;
text-decoration: none;
}
.brand img { width: 28px; height: 28px; }
nav { display: flex; gap: 20px; }
nav a { font-size: .875rem; text-decoration: none; color: var(--muted); }
nav a:hover { color: var(--ink); }
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 300;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Light.woff2') format('woff2');
}
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 400;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Book.woff2') format('woff2');
}
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 500;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 700;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Bold.woff2') format('woff2');
}
@font-face {
font-family: 'CircularCustCapNum';
font-weight: 900;
font-style: normal;
font-display: swap;
src: url('/fonts/circular/CircularCustCapNum-Black.woff2') format('woff2');
}
:root {
--ink: #050841;
--ink-dark: #050841;
--ink-medium: #202962;
--ink-secondary: #3C4679;
--muted: #8283a0;
--muted-light: #a5a9c1;
--paper: #ffffff;
--card: #ffffff;
--card-alt: #fafbfd;
--card-paper: #f1f5fe;
--line: #edf0f5;
--line-subtle: #f4f5f8;
--line-focus: #3d5df6;
--blue: #3d5df6;
--blue-hover: #1236e1;
--blue-active: #001992;
--blue-light: #647df8;
--blue-tint: #f1f5fe;
--blue-badge: #dbe4ff;
--green: #40b49a;
--green-dark: #20ab85;
--green-tint: #abddd1;
--orange: #f3a642;
--red: #f63d48;
--red-dark: #bc0020;
--red-tint: #fff5f6;
--purple: #6234f2;
--pink-badge: #ffd2f5;
--serif: "CircularCustCapNum", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--sans: "CircularCustCapNum", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
* { box-sizing:border-box; }
html {
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
scroll-behavior: smooth;
scrollbar-gutter: stable;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body, button, input, select, textarea {
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body { margin:0; min-height:100vh; }
a { color:inherit; text-decoration-thickness:1px; text-underline-offset:.2em; }
.shell { width:min(1160px,calc(100% - 40px)); margin-inline:auto; }
.site-header { min-height:76px; padding:0 max(20px,calc((100vw - 1160px)/2)); display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--line); background:rgba(244,240,231,.94); position:sticky; top:0; z-index:10; backdrop-filter:blur(12px); }
.brand { display:flex; gap:12px; align-items:center; font-family:var(--serif); font-size:1.15rem; font-weight:600; text-decoration:none; }
.brand img { width:31px; height:31px; }
nav { display:flex; gap:24px; }
nav a { text-decoration:none; color:var(--muted); font-size:.9rem; font-weight:600; }
nav a:hover { color:var(--ink); }
main { min-height:calc(100vh - 150px); }
h1,h2,h3 { font-family:var(--serif); line-height:1.08; margin-top:0; }
h1 { font-size:clamp(3rem,7vw,6.4rem); letter-spacing:-.045em; }
h2 { font-size:clamp(2rem,4vw,3.4rem); letter-spacing:-.03em; }
h3 { font-size:1.35rem; }
p { line-height:1.65; }
.eyebrow { text-transform:uppercase; letter-spacing:.16em; font-size:.72rem; font-weight:600; color:var(--orange); margin:0 0 14px; }
.button { display:inline-flex; padding:13px 20px; border:1px solid var(--ink); text-decoration:none; font-weight:600; font-size:.9rem; }
.button.primary { color:var(--paper); background:var(--green); border-color:var(--green); }
.section-block { padding-block:80px; }
.section-heading { display:flex; justify-content:space-between; align-items:end; margin-bottom:32px; }
.section-heading h2 { margin-bottom:0; }
.recipe-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:18px; }
.recipe-card { min-height:245px; padding:25px; border:1px solid var(--line); background:var(--card); display:flex; flex-direction:column; }
.recipe-card h2 { font-size:1.65rem; margin:35px 0 10px; }
.recipe-card h2 a { text-decoration:none; }
.recipe-card p { color:var(--muted); font-size:.92rem; margin-top:0; }
.card-topline { display:flex; justify-content:space-between; text-transform:uppercase; font-size:.68rem; letter-spacing:.1em; color:var(--muted); }
.tag-row { display:flex; flex-wrap:wrap; gap:8px 14px; margin-top:auto; }
.tag-row a { color:var(--green); font-size:.78rem; text-decoration:none; }
.page-heading { padding-block:85px 50px; border-bottom:1px solid var(--line); }.page-heading h1 { margin-bottom:15px; }.page-heading p:last-child { color:var(--muted); font-size:1.05rem; }
.taxonomy-grid { padding-block:50px 90px; display:grid; grid-template-columns:repeat(3,1fr); gap:16px; }.taxonomy-grid a { display:flex; flex-direction:column; gap:8px; padding:25px; border:1px solid var(--line); background:var(--card); text-decoration:none; }.taxonomy-grid span { color:var(--muted); font-size:.85rem; }
.tag-cloud { padding-block:50px 90px; display:flex; gap:12px; flex-wrap:wrap; }.tag-cloud a { padding:10px 14px; border:1px solid var(--line); background:var(--card); text-decoration:none; }.tag-cloud small { color:var(--muted); margin-left:4px; }
.recipe-page { padding-block:70px 100px; }.recipe-header { display:grid; grid-template-columns:1fr 280px; gap:80px; align-items:end; padding-bottom:55px; border-bottom:1px solid var(--line); }.recipe-header h1 { font-size:clamp(3rem,6vw,5.6rem); margin:18px 0; }.recipe-kicker { display:flex; gap:20px; text-transform:uppercase; color:var(--muted); letter-spacing:.12em; font-size:.7rem; }.recipe-facts { margin:0; display:grid; grid-template-columns:1fr 1fr; border-top:1px solid var(--ink); }.recipe-facts div { padding:17px 5px; border-bottom:1px solid var(--line); }.recipe-facts dt { color:var(--muted); font-size:.7rem; text-transform:uppercase; letter-spacing:.1em; }.recipe-facts dd { margin:5px 0 0; font-weight:600; }
.workspace-command-bar { display:flex; align-items:center; gap:.75rem; margin-bottom:2rem; }.workspace-command-bar .back-link { margin-right:auto; }.mode-indicator { padding:.45rem .7rem; color:var(--muted); background:var(--card); border:1px solid var(--line); font-size:.72rem; text-transform:uppercase; letter-spacing:.08em; }.mode-indicator.editing { color:#8b321f; background:#f3dfd4; }.primary-command { padding:.65rem .9rem; color:white; background:var(--green); text-decoration:none; font-weight:700; }.recipe-task-tabs { position:sticky; top:0; z-index:2; display:flex; gap:1.25rem; overflow-x:auto; margin-top:1.5rem; padding:.85rem 0; background:var(--paper); border-bottom:1px solid var(--line); }.recipe-task-tabs a { white-space:nowrap; color:var(--green); text-decoration:none; font-size:.78rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; }
.notice { margin-top:28px; padding:16px 20px; border-left:4px solid var(--orange); background:#f3dfd4; }
.calculator { margin-top:65px; }.calculator-heading { display:flex; justify-content:space-between; align-items:end; margin-bottom:25px; }.calculator-heading h2 { margin:0; }.basis-input { display:flex; flex-direction:column; gap:7px; font-size:.72rem; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); }.input-with-unit { display:flex; align-items:center; border-bottom:1px solid var(--ink); color:var(--ink); }.input-with-unit input { width:120px; padding:8px 5px; border:0; background:transparent; font:600 1.1rem var(--sans); color:var(--ink); outline:none; }
.quantity-control { display:inline-flex; align-items:center; border-bottom:1px solid var(--ink); }.quantity-control input { width:9rem; padding:.55rem .3rem; border:0; background:transparent; color:var(--ink); font:600 1.05rem var(--sans); }.quantity-control select { padding:.55rem .3rem; border:0; background:transparent; color:var(--green); font-weight:700; cursor:pointer; }.line-quantity { display:grid; grid-template-columns:7rem 6rem; justify-content:end; border-bottom:0; }.line-quantity input { width:100%; text-align:right; }.line-quantity select { width:100%; text-align:left; }
.formula-component { margin-top:30px; }.table-wrap { overflow-x:auto; } table { width:100%; border-collapse:collapse; } th,td { padding:14px 10px; border-bottom:1px solid var(--line); text-align:left; } th:not(:first-child),td:not(:first-child) { text-align:right; } th { color:var(--muted); font-size:.68rem; text-transform:uppercase; letter-spacing:.1em; }.recipe-ingredients { table-layout:fixed; }.recipe-ingredients th:first-child,.recipe-ingredients td:first-child { text-align:left; }.recipe-ingredients th:nth-last-child(2):not(:first-child),.recipe-ingredients td:nth-last-child(2):not(:first-child) { width:8rem; text-align:right; }.recipe-ingredients th:last-child,.recipe-ingredients td:last-child { width:14rem; text-align:right; }.basis-row td:first-child { color:var(--green); font-weight:600; }td small { display:block; color:var(--muted); margin-top:3px; }.muted { color:var(--muted); font-size:.75em; }.formula-total { display:flex; justify-content:flex-end; gap:60px; padding:20px 10px; background:var(--green); color:white; }
.derived-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,22rem),1fr)); gap:1rem; margin-top:2rem; }.derived-card { padding:1.15rem; background:var(--card); border:1px solid var(--line); }.derived-title { display:flex; justify-content:space-between; gap:1rem; align-items:end; padding-bottom:.6rem; border-bottom:3px solid var(--green); }.derived-title h3,.derived-title p { margin:0; }.derived-title > strong { font-size:.75rem; }.derived-card dl { margin:0; }.derived-card dl div { display:flex; justify-content:space-between; gap:1rem; padding:.4rem 0; border-bottom:1px solid var(--line); }.derived-card dt,.derived-card dd { margin:0; }.derived-card dd { font-weight:700; }.derived-batch { display:flex; justify-content:space-between; gap:1rem; }.derived-warning { color:#8b321f; font-size:.8rem; font-weight:700; }
.nutrition-wrap { margin:0; }.nutrition-label { padding:.45rem .55rem .55rem; color:#111; background:#fff; border:1px solid #111; font-family:Arial,Helvetica,sans-serif; line-height:1.05; }.nutrition-title { display:block; border-bottom:.65rem solid #111; padding-bottom:.3rem; }.nutrition-title h2 { margin:0; font-family:Arial Black,Arial,Helvetica,sans-serif; font-size:2.65rem; line-height:.95; letter-spacing:-.12rem; }.nutrition-title p { margin:.18rem 0; font-size:.9rem; }.serving-size { display:flex; justify-content:space-between; gap:1rem; padding-top:.25rem; border-top:1px solid #111; font-size:1rem; }.amount-heading,.dv-heading { margin:0; font-size:.72rem; font-weight:700; }.calorie-row { display:flex; justify-content:space-between; align-items:end; border-bottom:.35rem solid #111; font-size:2rem; line-height:1; padding:.15rem 0; }.calorie-row strong:first-child { font-size:1.25rem; }.dv-heading { padding:.2rem 0; text-align:right; border-bottom:1px solid #111; }.nutrition-facts { margin:0; }.nutrition-facts div { display:flex; justify-content:space-between; gap:1rem; padding:.22rem .05rem; border-bottom:1px solid #777; font-size:.9rem; }.nutrition-facts dt,.nutrition-facts dd { margin:0; }.nutrition-facts dd { font-weight:700; }.nutrition-facts .protein { border-bottom:.35rem solid #111; }.dv-footnote { margin:.35rem 0 0; padding-left:.65rem; text-indent:-.55rem; font-size:.68rem; line-height:1.2; }.nutrition-source { margin:.75rem 0 0; font-size:.78rem; color:var(--muted); }
.preparation,.notes { width:min(780px,100%); margin-top:80px; }.preparation ol { list-style:none; counter-reset:steps; padding:0; }.preparation li { counter-increment:steps; position:relative; padding:22px 0 22px 62px; border-top:1px solid var(--line); line-height:1.65; }.preparation li::before { content:counter(steps,decimal-leading-zero); position:absolute; left:5px; top:24px; color:var(--orange); font-size:.76rem; font-weight:600; }.preparation li.placeholder { color:var(--orange); }.notes ul { padding-left:20px; }.notes li { margin:10px 0; line-height:1.6; }
.equipment-list { width:min(780px,100%); margin-top:60px; }.equipment-list h2 { margin-top:0; }.equipment-list ul { display:flex; flex-wrap:wrap; gap:.6rem; padding:0; list-style:none; }.equipment-list li { padding:.7rem .9rem; background:var(--card); border:1px solid var(--line); }.equipment-list small { display:block; max-width:18rem; margin-top:.2rem; color:var(--muted); }.step-equipment { display:block; margin-top:.35rem; color:var(--muted); font-size:.72rem; text-transform:uppercase; letter-spacing:.06em; }
.additional-details { width:min(780px,100%); margin-top:80px; }.additional-details h2 { margin-top:0; }.additional-details dl { margin:0; border-top:1px solid var(--ink); }.additional-details dl > div { display:grid; grid-template-columns:10rem 1fr; gap:1rem; padding:1rem .3rem; border-bottom:1px solid var(--line); }.additional-details dt { color:var(--muted); font-size:.72rem; text-transform:uppercase; letter-spacing:.08em; }.additional-details dd { margin:0; font-weight:600; }.additional-details dd small { display:block; margin-top:.25rem; color:var(--muted); font-weight:400; }.additional-details ul { margin:0; padding:0; list-style:none; }.additional-details li + li { margin-top:.4rem; }
.prose { max-width:850px; padding-block:80px 110px; }.prose h1 { font-size:clamp(3rem,6vw,5rem); }.prose h2 { font-size:2rem; margin-top:50px; }.prose p { color:var(--muted); }
.review-toolbar { position:sticky; top:0; z-index:3; display:flex; gap:1rem; align-items:center; flex-wrap:wrap; padding:1rem; margin-bottom:2rem; background:var(--paper); border:1px solid var(--line); box-shadow:0 .5rem 1rem rgba(30,35,25,.08); }.review-toolbar div { margin-right:auto; }.review-toolbar small { display:block; color:var(--muted); }.review-toolbar input[type="search"],.review-toolbar button,.basis-input select { padding:.65rem .8rem; border:1px solid var(--line); background:var(--card); color:var(--ink); }.review-toolbar button { cursor:pointer; background:var(--green); color:white; font-weight:700; }.candidate-card { margin:0 0 1.5rem; padding:1.2rem; border:1px solid var(--line); background:var(--card); }.candidate-card legend { padding:0 .5rem; font:600 1.4rem var(--serif); }.candidate-card code { color:var(--muted); font:400 .72rem var(--sans); }.candidate-choice { display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:1rem; padding:.8rem; border-bottom:1px solid var(--line); cursor:pointer; }.candidate-choice:has(input:checked) { background:#dce6d9; outline:2px solid var(--green); }.candidate-choice small { display:block; margin-top:.2rem; color:var(--muted); }.candidate-choice a { font-size:.78rem; }.candidate-choice.none { border-bottom:0; }
.product-meta { color:var(--muted); font-size:.82rem; text-transform:capitalize; }
.other-choice { display:flex; gap:1rem; align-items:center; justify-content:space-between; padding:.8rem; border-bottom:1px solid var(--line); }.other-choice select { min-width:min(25rem,55vw); padding:.6rem; border:1px solid var(--line); background:var(--paper); }
.quality-summary { display:grid; grid-template-columns:repeat(auto-fit,minmax(9rem,1fr)); gap:1rem; padding-bottom:1rem; }.quality-summary article { display:flex; flex-direction:column; padding:1.1rem; background:var(--card); border:1px solid var(--line); }.quality-summary strong { font:600 2.25rem var(--serif); color:var(--green); }.quality-summary span { color:var(--muted); font-size:.78rem; }.quality-table td:first-child { text-align:left; }.quality-table small { display:block; }.quality-table td:not(:first-child) { font-size:.82rem; }
.app-launcher { display:grid; grid-template-columns:repeat(auto-fit,minmax(15rem,1fr)); gap:1rem; padding-bottom:6rem; }.app-launcher a { position:relative; min-height:14rem; padding:1.4rem; color:var(--ink); background:var(--card); border:1px solid var(--line); text-decoration:none; }.app-launcher h2 { margin:.45rem 0; }.app-launcher strong { position:absolute; top:1.2rem; right:1.2rem; font:600 1.5rem var(--serif); color:var(--green); }.app-launcher p { color:var(--muted); }.entity-list { padding-bottom:6rem; }.entity-list-head,.entity-list > a { display:grid; grid-template-columns:minmax(16rem,2fr) repeat(3,minmax(6rem,1fr)); align-items:center; gap:1rem; padding:.8rem 1rem; border-bottom:1px solid var(--line); }.entity-list-head { color:var(--muted); font-size:.68rem; text-transform:uppercase; letter-spacing:.08em; }.entity-list > a { color:var(--ink); text-decoration:none; }.entity-list > a:hover { background:var(--card); }.entity-list small { display:block; color:var(--muted); }.empty-state { padding:3rem; text-align:center; background:var(--card); border:1px solid var(--line); }
.ingredient-list-tools { display:flex; justify-content:flex-end; padding-block:1rem; }.filter-menu { position:relative; }.filter-menu summary { padding:.65rem .85rem; color:white; background:var(--green); cursor:pointer; font-weight:700; list-style:none; }.filter-menu summary::-webkit-details-marker { display:none; }.filter-menu form { position:absolute; z-index:4; right:0; width:min(20rem,calc(100vw - 2rem)); padding:1rem; background:var(--card); border:1px solid var(--line); box-shadow:0 .75rem 2rem rgba(30,35,25,.14); }.filter-menu label { display:flex; align-items:center; gap:.55rem; padding:.35rem 0; }.filter-menu fieldset { margin:.75rem 0; padding:.75rem; border:1px solid var(--line); }.filter-menu legend { color:var(--muted); font-size:.72rem; text-transform:uppercase; letter-spacing:.06em; }.filter-menu form > div { display:flex; align-items:center; gap:1rem; }.filter-menu button { padding:.6rem .8rem; color:white; background:var(--green); border:0; cursor:pointer; font-weight:700; }.ingredient-entity-list .entity-list-head,.ingredient-entity-list > a { grid-template-columns:minmax(16rem,1fr) 7rem; }.attention-reasons { display:flex; flex-wrap:wrap; gap:.35rem; margin-top:.4rem; }.attention-reasons em { padding:.18rem .4rem; color:#8b321f; background:#f3dfd4; font-size:.68rem; font-style:normal; font-weight:700; text-transform:uppercase; letter-spacing:.04em; }
.directory-workspace { padding-block:28px 72px; }.workspace-pills { display:flex; align-items:center; flex-wrap:wrap; gap:10px; overflow:visible; padding:0 0 28px; }.workspace-pills > a { display:inline-flex; align-items:center; gap:8px; flex:none; min-height:42px; padding:7px 13px; color:rgba(0,0,0,0.87); background:white; border:1px solid #e5e7ef; border-radius:999px; text-decoration:none; font-weight:400; box-shadow:0 1px 2px rgba(15,23,42,.03); }.workspace-pills > a.active { background:#DBE4FF; border-color:#DBE4FF; }.workspace-pills > a small { color:#a0a7ba; font-size:.8rem; font-weight:500; }.workspace-pill-icon { display:grid; place-items:center; width:22px; height:22px; color:white; background:#48558d; border-radius:50%; font-size:.7rem; }.workspace-pill-icon.ingredient,.workspace-pill-icon.purchase { background:#489d96; }.workspace-pill-icon.book { background:#6847ed; }.workspace-pills .filter-menu { flex:none; margin-left:auto; }.workspace-pills .filter-menu summary { color:#172033; background:transparent; white-space:nowrap; }.ingredient-directory { overflow:hidden; background:white; border:1px solid #eef0f5; }.ingredient-selection-bar { display:grid; grid-template-columns:28px 1fr auto; align-items:center; min-height:58px; gap:18px; padding:0 22px; border-bottom:1px solid #eef0f5; }.ingredient-selection-bar input,.ingredient-directory-row > input { width:17px; height:17px; margin:0; accent-color:#4868f7; }.ingredient-selection-bar strong { color:#5366fa; font-size:.9rem; }.ingredient-selection-bar button { color:#64748b; background:transparent; border:0; cursor:pointer; }.ingredient-directory-row { display:grid; grid-template-columns:28px 34px minmax(14rem,1fr) 10rem 28px; align-items:center; min-height:60px; gap:18px; padding:0 22px; border-bottom:1px solid #eef0f5; }.ingredient-directory-row:last-child { border-bottom:0; }.ingredient-directory-row:hover { background:#f8faff; }.ingredient-directory-row.selected { background:#edf2ff; }.ingredient-type-icon { display:grid; place-items:center; width:26px; height:26px; color:white; background:#489d96; border-radius:50%; font-size:.65rem; }.ingredient-directory-name { color:#111a49; text-decoration:none; }.ingredient-directory-name strong { font-size:.9rem; }.ingredient-directory-name small { display:flex; gap:5px; margin-top:3px; }.ingredient-directory-name em { padding:2px 5px; color:#9a3412; background:#fff0e8; border-radius:3px; font-size:.62rem; font-style:normal; font-weight:700; text-transform:uppercase; }.ingredient-usage { color:#8b93a7; font-size:.82rem; text-align:right; }.ingredient-row-menu { color:#a0a7ba; font-size:1.45rem; line-height:1; text-align:center; text-decoration:none; }
.workspace-directory-list { overflow:hidden; background:white; border:1px solid #eef0f5; }.workspace-directory-heading,.workspace-directory-row { display:grid; grid-template-columns:34px minmax(16rem,1fr) 9rem 10rem 28px; align-items:center; gap:18px; padding:0 22px; }.workspace-directory-heading { min-height:42px; color:#8b93a7; border-bottom:1px solid #eef0f5; font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; }.workspace-directory-heading strong { grid-column:2; }.workspace-directory-row { min-height:64px; color:#111a49; border-bottom:1px solid #eef0f5; text-decoration:none; }.workspace-directory-row:last-child { border-bottom:0; }.workspace-directory-row:hover { background:#f8faff; }.workspace-directory-row > span:nth-child(2) { display:flex; flex-direction:column; min-width:0; }.workspace-directory-row small { overflow:hidden; color:#8b93a7; text-overflow:ellipsis; white-space:nowrap; }.workspace-directory-row b { color:#a0a7ba; font-size:1.35rem; text-align:center; }.workspace-directory-heading:has(+ .books),.workspace-directory-row.books { grid-template-columns:34px minmax(16rem,1fr) 9rem 28px; }.workspace-directory-heading:has(+ .purchases),.workspace-directory-row.purchases { grid-template-columns:34px minmax(14rem,1fr) minmax(10rem,1fr) 9rem 7rem 28px; }
.workspace-search-tools { display:flex; align-items:flex-start; gap:10px; margin-bottom:18px; }.workspace-global-search { display:flex; align-items:center; gap:10px; flex:1; max-width:680px; min-height:44px; padding:0 14px; background:white; border:1px solid #dfe3ec; border-radius:7px; }.workspace-global-search:focus-within { border-color:#7185ec; box-shadow:0 0 0 3px #e9efff; }.workspace-global-search > span { color:#818aa0; font-size:1.35rem; }.workspace-global-search input[type="search"] { flex:1; min-width:0; padding:10px 0; color:#111a49; background:transparent; border:0; outline:0; font:inherit; }.workspace-global-search a { color:#818aa0; font-size:1.35rem; text-decoration:none; }.search-type-filter { position:relative; }.search-type-filter summary { min-height:44px; padding:11px 14px; color:#172033; background:white; border:1px solid #dfe3ec; border-radius:7px; cursor:pointer; font-weight:700; list-style:none; white-space:nowrap; }.search-type-filter summary::-webkit-details-marker { display:none; }.search-type-filter form { position:absolute; z-index:5; right:0; width:220px; padding:12px; background:white; border:1px solid #dfe3ec; border-radius:7px; box-shadow:0 .75rem 2rem rgba(30,35,25,.14); }.search-type-filter label { display:flex; align-items:center; gap:8px; padding:6px 2px; }.search-type-filter form > div { display:flex; align-items:center; gap:12px; margin-top:10px; }.search-type-filter button { padding:7px 10px; color:white; background:#48558d; border:0; border-radius:5px; cursor:pointer; font-weight:700; }.workspace-search-results > p { color:#687086; font-size:.86rem; }.workspace-search-results > div { overflow:hidden; background:white; border:1px solid #eef0f5; }.workspace-search-results > div > a { display:grid; grid-template-columns:34px minmax(12rem,1fr) 9rem 24px; align-items:center; gap:16px; min-height:62px; padding:0 22px; color:#111a49; border-bottom:1px solid #eef0f5; text-decoration:none; }.workspace-search-results > div > a:last-child { border-bottom:0; }.workspace-search-results > div > a:hover { background:#f8faff; }.workspace-search-results > div > a > span:nth-child(2) { display:flex; flex-direction:column; min-width:0; }.workspace-search-results small { overflow:hidden; color:#8b93a7; text-overflow:ellipsis; white-space:nowrap; }.workspace-search-results em { color:#8b93a7; font-size:.75rem; font-style:normal; text-align:right; text-transform:uppercase; }.workspace-search-results b { color:#a0a7ba; font-size:1.3rem; }
.workspace-tabs { display:flex; flex-wrap:wrap; gap:.5rem; margin-top:1.5rem; }.workspace-tabs a { padding:.55rem .75rem; color:var(--ink); background:var(--card); border:1px solid var(--line); text-decoration:none; }.ingredient-workspace { display:grid; gap:1.25rem; padding-bottom:6rem; }.workspace-panel { scroll-margin-top:1rem; padding:1.25rem; background:var(--card); border:1px solid var(--line); }.workspace-panel h2 { margin-top:0; }.workspace-panel > label { display:grid; grid-template-columns:minmax(9rem,1fr) 3fr; gap:1rem; align-items:center; margin:.75rem 0; color:var(--muted); }.workspace-panel input,.workspace-panel textarea,.workspace-panel select { padding:.65rem; color:var(--ink); background:var(--paper); border:1px solid var(--line); font:1rem var(--sans); }.inline-form { display:flex; flex-wrap:wrap; gap:.55rem; align-items:center; margin-top:1rem; padding-top:1rem; border-top:1px solid var(--line); }.inline-form button { padding:.7rem; color:white; background:var(--green); border:1px solid var(--green); cursor:pointer; }.purchase-row { display:grid; grid-template-columns:minmax(12rem,1fr) auto auto; gap:1rem; align-items:center; padding:.7rem 0; border-bottom:1px solid var(--line); }.purchase-row small { display:block; color:var(--muted); }
.panel-intro { color:var(--muted); }.ingredient-nutrition { margin-top:1rem; padding:1rem; background:var(--paper); border:1px solid var(--line); }.nutrition-mapping-head { display:flex; justify-content:space-between; gap:1rem; align-items:start; padding-bottom:.75rem; border-bottom:3px solid var(--green); }.nutrition-mapping-head small { display:block; margin-top:.2rem; color:var(--muted); text-transform:capitalize; }.nutrition-mapping-head a { white-space:nowrap; }.ingredient-nutrition dl { display:grid; grid-template-columns:repeat(auto-fit,minmax(11rem,1fr)); margin:0; }.ingredient-nutrition dl div { display:flex; justify-content:space-between; gap:1rem; padding:.55rem; border-bottom:1px solid var(--line); }.ingredient-nutrition dt,.ingredient-nutrition dd { margin:0; }.ingredient-nutrition dd { font-weight:700; }.ingredient-nutrition footer { display:flex; flex-wrap:wrap; gap:.5rem 1.2rem; padding-top:.75rem; color:var(--muted); font-size:.72rem; }
.purchase-editor { border-top:1px solid var(--line); }.purchase-editor:last-child { border-bottom:1px solid var(--line); }.purchase-editor > summary { display:flex; justify-content:space-between; gap:1rem; align-items:center; padding:1rem .25rem; cursor:pointer; }.purchase-editor > summary > span:last-child { text-align:right; }.purchase-editor summary small { display:block; margin-top:.2rem; color:var(--muted); }.add-purchase > summary { color:var(--green); font-weight:700; }.purchase-item-form { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:.85rem; padding:1rem; margin-bottom:1rem; background:var(--paper); border:1px solid var(--line); }.purchase-item-form label { display:flex; flex-direction:column; gap:.3rem; min-width:0; color:var(--muted); font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; }.purchase-item-form .purchase-name { grid-column:span 2; }.purchase-item-form input,.purchase-item-form select { width:100%; }.input-suffix { display:flex; align-items:center; }.input-suffix input { min-width:0; }.input-suffix > span { padding:.65rem; color:var(--ink); background:var(--card); border:1px solid var(--line); border-left:0; }.purchase-rate { grid-column:span 3; display:flex; align-items:center; gap:1rem; margin:0; padding:.65rem; background:var(--card); }.purchase-rate span { color:var(--muted); }.purchase-form-actions { display:flex; justify-content:flex-end; align-items:end; }.purchase-form-actions button { padding:.7rem; color:white; background:var(--green); border:1px solid var(--green); cursor:pointer; font-weight:700; }.price-history { grid-column:1/-1; }.price-history summary { cursor:pointer; color:var(--green); font-size:.8rem; font-weight:700; }.price-history table { margin-top:.5rem; }.price-history td { padding:.4rem; font-size:.8rem; }.price-history td:last-child { text-align:right; }
.usda-id-form { display:flex; flex-wrap:wrap; gap:.65rem; align-items:end; }.usda-id-form label { display:flex; flex-direction:column; gap:.35rem; }.usda-id-form input { min-width:15rem; }.usda-id-form button { padding:.7rem; color:white; background:var(--green); border:1px solid var(--green); cursor:pointer; }.usda-id-form a { padding:.65rem 0; }
.editor-layout { display:grid; grid-template-columns:minmax(0,2fr) minmax(16rem,1fr); gap:2rem; padding-bottom:6rem; }.editor-form fieldset { display:grid; gap:1rem; margin:0 0 1.5rem; padding:1.25rem; border:1px solid var(--line); background:var(--card); }.editor-form legend { padding:0 .5rem; font:600 1.35rem var(--serif); }.editor-form label { display:flex; flex-direction:column; gap:.4rem; color:var(--muted); font-size:.76rem; text-transform:uppercase; letter-spacing:.06em; }.editor-form input,.editor-form textarea,.editor-form select { width:100%; padding:.7rem; color:var(--ink); background:var(--paper); border:1px solid var(--line); font:1rem var(--sans); text-transform:none; letter-spacing:normal; }.editor-row { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }.editor-actions { display:flex; flex-wrap:wrap; gap:.75rem; }.editor-actions button { padding:.8rem 1rem; border:1px solid var(--green); background:var(--green); color:white; font-weight:700; cursor:pointer; }.editor-actions .secondary { background:transparent; color:var(--green); }.success-notice { margin-bottom:1rem; padding:1rem; color:#204f31; background:#dcebdc; border-left:4px solid #39734c; }
.structure-editor button { padding:.55rem .7rem; border:1px solid var(--line); background:var(--card); cursor:pointer; }.structure-editor .section-heading > button,.add-line button { color:white; background:var(--green); }.component-editor { margin:0 0 1.5rem; padding:1rem; border:1px solid var(--line); }.component-editor legend { padding:0 .5rem; font:600 1.25rem var(--serif); }.editor-inline,.add-line { display:flex; gap:.5rem; margin-bottom:1rem; }.editor-inline input,.add-line select,.editor-items input[type="number"],.editor-items select,.method-editor textarea { width:100%; padding:.55rem; color:var(--ink); background:var(--paper); border:1px solid var(--line); }.editor-inline input { font-weight:700; }.editor-items { min-width:60rem; table-layout:fixed; }.editor-items th,.editor-items td { vertical-align:middle; text-align:left; }.editor-items th:nth-child(2),.editor-items td:nth-child(2) { width:7rem; text-align:right; }.editor-items td:nth-child(2) input { text-align:right; }.editor-items th:nth-child(3),.editor-items td:nth-child(3) { width:12rem; }.editor-items th:nth-child(4),.editor-items td:nth-child(4) { width:9rem; }.editor-items th:nth-child(5),.editor-items td:nth-child(5) { width:6rem; text-align:center; }.editor-items th:last-child,.editor-items td:last-child { width:8.5rem; text-align:right; }.editor-items input[type="number"],.editor-items select { min-width:0; }.editor-items td:last-child .row-actions button { width:2.1rem; height:2.1rem; padding:0; }.row-actions { display:flex; gap:.25rem; justify-content:flex-end; }.structure-editor button.danger { color:#8b321f; }.inline-check { display:inline-flex; align-items:center; justify-content:center; gap:.35rem; white-space:nowrap; font-size:.76rem; }.inline-check input { width:auto; min-width:0; margin:0; }.secondary-action { margin-bottom:1.5rem; }.method-editor ol { padding:0; list-style:none; counter-reset:method; }.method-editor li { display:grid; grid-template-columns:1fr auto; gap:.5rem; margin-bottom:.75rem; counter-increment:method; }.method-editor li::before { content:counter(method); display:none; }
.editor-entity-link { color:var(--green); text-decoration-thickness:1px; text-underline-offset:3px; }
@media (max-width:900px) { .recipe-grid,.taxonomy-grid { grid-template-columns:repeat(2,1fr); }.recipe-header { grid-template-columns:1fr; gap:25px; }.recipe-facts { max-width:500px; }.purchase-item-form { grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (max-width:620px) { .site-header { align-items:flex-start; padding-block:17px; }.site-header nav { gap:8px 14px; flex-wrap:wrap; justify-content:flex-end; }.brand span { display:none; }.recipe-grid,.taxonomy-grid { grid-template-columns:1fr; }.calculator-heading { align-items:flex-start; gap:25px; flex-direction:column; }.formula-total { justify-content:space-between; gap:15px; }.recipe-page { padding-top:45px; }.recipe-header h1 { font-size:3.2rem; }.entity-list-head { display:none; }.entity-list > a { grid-template-columns:1fr 1fr; }.editor-layout { grid-template-columns:1fr; }.workspace-panel > label { grid-template-columns:1fr; }.purchase-row { grid-template-columns:1fr; }.purchase-item-form { grid-template-columns:1fr; }.purchase-item-form .purchase-name,.purchase-rate { grid-column:auto; }.purchase-editor > summary { align-items:flex-start; }.purchase-editor > summary > span:last-child { text-align:left; } }
/* Conventional application theme */
html { line-height:1.5; }
body { background:var(--paper); }
.shell { width:min(1180px,calc(100% - 32px)); }
.site-header { min-height:64px; padding-inline:max(16px,calc((100vw - 1180px)/2)); background:rgba(255,255,255,.96); box-shadow:0 1px 2px rgba(15,23,42,.04); }
.brand { gap:10px; font-size:1rem; font-weight:700; }
.brand img { width:28px; height:28px; }
nav { gap:20px; }
nav a { font-size:.875rem; }
h1,h2,h3 { font-family:var(--sans); line-height:1.2; letter-spacing:-.02em; }
h1 { font-size:clamp(2rem,4vw,3rem); }
h2 { font-size:clamp(1.5rem,2.5vw,2rem); }
h3 { font-size:1.125rem; }
.eyebrow { margin-bottom:8px; color:var(--muted); font-size:.7rem; letter-spacing:.1em; }
.button,.primary-command { border-radius:6px; }
.button { padding:10px 16px; font-size:.875rem; }
.button.primary,.primary-command { background:var(--green); border-color:var(--green); }
.section-block { padding-block:48px; }
.section-heading { margin-bottom:20px; align-items:center; }
.recipe-grid { gap:16px; }
.recipe-card,.taxonomy-grid a,.candidate-card,.workspace-panel,.derived-card,.app-launcher a { border-radius:8px; box-shadow:0 1px 2px rgba(15,23,42,.04); }
.recipe-card { min-height:190px; padding:20px; }
.recipe-card h2 { margin:24px 0 8px; font-size:1.25rem; }
.page-heading { padding-block:48px 28px; }
.page-heading h1 { margin-bottom:8px; }
.page-heading p:last-child { font-size:.95rem; }
.recipe-page { padding-block:48px 72px; }
.recipe-header { grid-template-columns:1fr 260px; gap:48px; padding-bottom:36px; }
.recipe-header h1 { margin:12px 0; font-size:clamp(2rem,4vw,3rem); }
.workspace-command-bar { margin-bottom:1.25rem; }
.mode-indicator,.workspace-tabs a,.primary-command { border-radius:6px; }
.recipe-task-tabs { top:64px; background:rgba(248,250,252,.96); }
.calculator { margin-top:44px; }
th,td { padding:11px 10px; }
th { font-size:.7rem; letter-spacing:.06em; }
.formula-total { padding:14px 10px; background:#eef2ff; color:var(--ink); border-bottom:1px solid var(--line); }
.equipment-list li { border-radius:6px; }
.preparation,.notes,.additional-details { margin-top:56px; }
.review-toolbar { top:64px; border-radius:8px; background:rgba(248,250,252,.96); }
.entity-list > a:hover { background:#f1f5f9; }
.filter-menu summary,.filter-menu button,.inline-form button,.purchase-form-actions button,.editor-actions button { border-radius:6px; }
.workspace-panel { padding:1.25rem; }
.workspace-panel input,.workspace-panel textarea,.workspace-panel select,.editor-form input,.editor-form textarea,.editor-form select,.editor-inline input,.add-line select,.editor-items input[type="number"],.editor-items select,.method-editor textarea { border-radius:6px; background:white; }
.component-editor { border-radius:8px; background:var(--card); }
.structure-editor button { border-radius:6px; }
.success-notice,.notice { border-radius:6px; }
.purchase-item-form,.ingredient-nutrition { border-radius:6px; background:#f8fafc; }
.app-launcher a { min-height:11rem; }
@media (max-width:620px) {
.shell { width:min(100% - 24px,1180px); }
.recipe-page { padding-top:32px; }
.recipe-header h1 { font-size:2.25rem; }
.page-heading { padding-block:36px 24px; }
}
.workspace-new-menu { position:relative; margin-left:auto; }
.workspace-new-menu > summary { display:flex; align-items:center; gap:5px; min-height:44px; padding:9px 16px; color:white; background:#4868f7; border-radius:22px; cursor:pointer; font-weight:700; list-style:none; white-space:nowrap; }
.workspace-new-menu > summary::-webkit-details-marker { display:none; }
.workspace-new-menu > nav { position:absolute; z-index:6; top:52px; right:0; width:260px; padding:10px 0; background:white; border:1px solid #dfe3ec; border-radius:7px; box-shadow:0 .75rem 2rem rgba(30,35,25,.16); }
.workspace-new-menu > nav a { display:grid; grid-template-columns:28px 1fr; align-items:center; gap:14px; min-height:54px; padding:8px 20px; color:#111a49; text-decoration:none; }
.workspace-new-menu > nav a:hover { background:#f8faff; }
.create-entity { max-width:720px; padding-bottom:72px; }
.entity-detail-shell,.recipe-detail-shell { width:min(1500px,calc(100% - 48px)); margin:0 auto; padding:24px 0 72px; color:#101a4a; }
.entity-detail-header { display:flex; align-items:flex-start; justify-content:space-between; gap:24px; min-height:108px; border-bottom:1px solid #e8eaf0; }.entity-detail-header h1 { margin:8px 0 2px; font:700 clamp(1.7rem,2.4vw,2.35rem) var(--sans); letter-spacing:-.035em; }.entity-detail-header p { margin:0; color:#858da2; }.entity-breadcrumb a { color:#26346d; font-size:.82rem; text-decoration:none; }.identity-edit { position:relative; }.identity-edit > summary { padding:8px 15px; color:#4868f7; border:1px solid #4868f7; border-radius:20px; cursor:pointer; font-weight:700; list-style:none; }.identity-edit > summary::-webkit-details-marker { display:none; }.identity-edit > form { position:absolute; z-index:8; right:0; width:min(390px,calc(100vw - 32px)); padding:18px; background:white; border:1px solid #dfe3ec; border-radius:7px; box-shadow:0 .75rem 2rem rgba(30,35,25,.16); }.identity-edit label { display:grid; gap:4px; margin-bottom:10px; color:#737c91; font-size:.75rem; }.identity-edit input,.identity-edit select,.identity-edit textarea { width:100%; padding:9px; border:1px solid #dfe3ec; }.identity-edit button { padding:9px 12px; color:white; background:#4868f7; border:0; border-radius:5px; font-weight:700; }.entity-notice { margin:14px 0; }.entity-detail-grid { display:grid; grid-template-columns:minmax(0,1.02fr) minmax(0,.98fr); min-height:720px; }.entity-primary { padding:48px 42px 40px 0; border-right:1px solid #e8eaf0; }.entity-secondary { min-width:0; padding-left:32px; }.entity-section { margin-bottom:54px; }.entity-section h2,.entity-tab-panel h2 { margin:0 0 8px; font:700 1.35rem var(--sans); }.prep-action-table { margin-top:34px; }.prep-action-table th,.prep-action-table td { padding:13px 8px; text-align:left; border-bottom:1px solid #edf0f5; }.prep-action-table th { color:#9098aa; font-size:.72rem; font-weight:500; }.inline-editor { margin-top:20px; }.inline-editor > summary { color:#4868f7; cursor:pointer; font-weight:700; list-style:none; }.additional-card { min-height:260px; padding:28px; border:1px solid #e8eaf0; }.additional-card > details { padding:16px 0; }.additional-card summary { cursor:pointer; font-weight:700; }.additional-card summary small { margin-left:5px; color:#9aa2b5; }.additional-card ul { padding-left:20px; }.additional-card li { display:flex; justify-content:space-between; gap:10px; padding:6px 0; }.entity-tab-panel { display:none; padding:42px 0; background:transparent; border:0; }.entity-tab-panel.active { display:block; }.equivalency-list p { display:grid; grid-template-columns:1fr auto 1fr; gap:12px; padding:14px 4px; border-bottom:1px solid #e8eaf0; }.equivalency-list p small { grid-column:1/-1; color:#8b93a7; }.entity-secondary .purchase-item-form { grid-template-columns:repeat(2,minmax(0,1fr)); }.entity-secondary .purchase-name,.entity-secondary .purchase-rate { grid-column:1/-1; }
.recipe-detail-shell { width:min(1600px,calc(100% - 48px)); }.recipe-detail-shell .entity-detail-header { min-height:92px; }.recipe-overview-strip { display:flex; align-items:flex-start; justify-content:space-between; gap:24px; padding:18px 0; }.recipe-overview-form { flex:1; }.recipe-overview-form > details > summary { color:#4868f7; cursor:pointer; font-weight:700; }.recipe-overview-form fieldset { max-width:720px; margin-top:15px; }.recipe-yield-summary { display:flex; align-items:baseline; gap:12px; min-width:260px; padding:12px 16px; border:1px solid #e8eaf0; }.recipe-yield-summary span,.recipe-yield-summary small { color:#8b93a7; }.recipe-structure-workspace { border-top:1px solid #e8eaf0; }.recipe-structure-workspace .structure-editor { display:grid; grid-template-columns:minmax(0,1fr) minmax(420px,.92fr); column-gap:32px; }.recipe-structure-workspace .section-heading { grid-column:1/-1; }.recipe-structure-workspace .component-editor,.recipe-structure-workspace > .structure-editor > .secondary-action { grid-column:1; }.recipe-structure-workspace .method-editor { grid-column:2; grid-row:2 / span 40; min-width:0; padding-left:32px; border-left:1px solid #e8eaf0; }.recipe-structure-workspace .component-editor { border-color:#e8eaf0; background:white; }.recipe-structure-workspace .editor-items { min-width:46rem; }.recipe-structure-workspace .method-editor li { padding:15px; border:1px solid #e8eaf0; }.recipe-history { margin-top:30px; padding:18px; border:1px solid #e8eaf0; }.recipe-history summary { cursor:pointer; font-weight:700; }.recipe-history li span,.recipe-history li small { display:block; color:#8b93a7; }
.immersive-main { min-height:100vh; background:#fff; }.immersive-main .entity-detail-shell,.immersive-main .recipe-detail-shell { width:100%; max-width:none; margin:0; padding:0 0 72px; }.detail-utility { display:flex; align-items:center; justify-content:flex-end; gap:18px; min-height:58px; padding:7px 28px; border-bottom:1px solid #e8eaf0; }.detail-utility form { width:min(560px,42vw); }.detail-utility input { width:100%; min-height:42px; padding:8px 14px; border:1px solid #e5e8ef; border-radius:4px; font:inherit; }.detail-utility > a { color:#26346d; text-decoration:none; font-weight:700; }.detail-utility .detail-new { padding:9px 16px; color:white; background:#4868f7; border-radius:21px; }.immersive-main .entity-detail-header { width:52%; min-height:128px; padding:26px 38px; border-right:1px solid #e8eaf0; }.immersive-main .entity-notice { width:calc(52% - 76px); margin:12px 38px; }.immersive-main .entity-detail-grid { grid-template-columns:52% 48%; }.immersive-main .entity-primary { padding:48px 38px; }.immersive-main .entity-secondary { padding:0 30px; }.immersive-main .recipe-detail-shell .entity-detail-header { width:52%; padding:22px 38px; }.immersive-main .recipe-workspace-tabs { width:48%; margin:-92px 0 30px 52%; min-height:92px; align-items:end; padding:0 22px; }.immersive-main .recipe-overview-strip { width:52%; min-height:150px; padding:20px 38px; border-right:1px solid #e8eaf0; }.immersive-main .recipe-structure-workspace { padding:0 38px; }.immersive-main .recipe-structure-workspace .structure-editor { grid-template-columns:minmax(0,52fr) minmax(420px,48fr); column-gap:0; }.immersive-main .recipe-structure-workspace .section-heading { padding-right:32px; }.immersive-main .recipe-structure-workspace .component-editor,.immersive-main .recipe-structure-workspace > .structure-editor > .secondary-action { margin-right:32px; }.immersive-main .recipe-structure-workspace .method-editor { padding-left:32px; }.immersive-main .recipe-history { margin-inline:38px; }
.immersive-main .entity-detail-shell,.immersive-main .recipe-detail-shell { position:relative; }.immersive-main .detail-utility { position:absolute; z-index:10; top:0; right:0; width:48%; height:58px; background:#fff; }.detail-new-menu { position:relative; }.detail-new-menu > summary { padding:9px 16px; color:white; background:#4868f7; border-radius:21px; cursor:pointer; font-weight:700; list-style:none; white-space:nowrap; }.detail-new-menu > summary::-webkit-details-marker { display:none; }.detail-new-menu > div { position:absolute; top:46px; right:0; width:220px; padding:8px 0; background:#fff; border:1px solid #e5e8ef; border-radius:6px; box-shadow:0 12px 30px rgba(22,30,65,.15); }.detail-new-menu > div a { display:flex; gap:12px; padding:13px 18px; color:#111a49; text-decoration:none; }.detail-new-menu > div a:hover { background:#f7f8fc; }.immersive-main .entity-detail-header { min-height:164px; padding-top:24px; }.immersive-main .entity-detail-grid { border-top:1px solid #e8eaf0; }.immersive-main .entity-secondary { margin-top:-106px; background:#fff; }.immersive-main .entity-tabs { min-height:58px; }.immersive-main .recipe-detail-shell .entity-detail-header { min-height:116px; padding-top:20px; }.immersive-main .recipe-workspace-tabs { min-height:58px; margin:-58px 0 28px 52%; }.immersive-main .recipe-workspace-tabs a { padding-block:17px; }.immersive-main .recipe-overview-strip { min-height:126px; }.immersive-main .recipe-structure-workspace { border-top:0; }.immersive-main .recipe-structure-workspace .structure-editor { border-top:1px solid #e8eaf0; }.immersive-main .recipe-structure-workspace .section-heading { min-height:76px; margin:0; padding:18px 32px 12px 0; }.immersive-main .recipe-structure-workspace .section-heading h2 { margin:2px 0; font:700 1.35rem var(--sans); }
.edit-command { padding:8px 15px; color:#4868f7; border:1px solid #4868f7; border-radius:20px; text-decoration:none; font-weight:700; }.recipe-view-workspace { display:grid; grid-template-columns:52% 48%; border-top:1px solid #e8eaf0; }.recipe-view-formula { min-width:0; padding:26px 32px 48px 38px; border-right:1px solid #e8eaf0; }.recipe-view-formula .calculator { padding:0; }.recipe-view-formula .calculator-heading { display:flex; align-items:baseline; gap:24px; margin-bottom:22px; }.recipe-view-formula .calculator-heading h2 { margin:0; font:700 1.4rem var(--sans); }.recipe-view-formula .calculator-heading .eyebrow { display:none; }.recipe-view-formula .basis-input { min-width:0; }.recipe-view-formula .formula-component { margin:0 0 14px; border:0; background:transparent; }.recipe-view-formula .formula-component h3 { margin:0; padding:8px 0; background:transparent; border-bottom:1px solid #f3f3f3; font:500 1.125rem var(--sans); }.recipe-view-formula .recipe-ingredients th { color:#8b93a7; background:white; font-size:.72rem; font-weight:500; }.recipe-view-formula .recipe-ingredients td { border:0; }.recipe-view-formula .formula-total { margin-top:12px; }.recipe-view-details { min-width:0; padding:28px 30px 60px; }.recipe-view-method h2,.recipe-derived-panel > h2 { font:700 1.4rem var(--sans); }.recipe-view-method h2 small { color:#9aa2b5; font-size:.85rem; }.recipe-view-method ol { padding:0; list-style:none; }.recipe-derived-panel { margin-top:46px; padding-top:26px; border-top:1px solid #e8eaf0; }.recipe-derived-panel dl { margin:0; }.recipe-derived-panel dl > div { display:flex; justify-content:space-between; padding:12px 4px; border-bottom:1px solid #edf0f5; }.recipe-derived-panel dd { margin:0; font-weight:700; }.recipe-derived-panel .nutrition-wrap { display:block; }.recipe-derived-panel .nutrition-label { max-width:520px; }
.ingredient-identity-form { display:grid; grid-template-columns:1fr 180px; gap:12px; margin:0 0 42px; padding:20px; background:#fafbfe; border:1px solid #e8eaf0; }.ingredient-identity-form h2 { grid-column:1/-1; margin:0 0 6px; font:700 1.2rem var(--sans); }.ingredient-identity-form label { display:grid; gap:5px; color:#788197; font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; }.ingredient-identity-form label:nth-of-type(3),.ingredient-identity-form label:nth-of-type(4) { grid-column:1/-1; }.ingredient-identity-form input,.ingredient-identity-form select,.ingredient-identity-form textarea { width:100%; padding:9px; color:#111a49; background:#fff; border:1px solid #dfe3ec; font:1rem var(--sans); text-transform:none; letter-spacing:normal; }.ingredient-identity-form button { justify-self:start; padding:9px 13px; color:white; background:#4868f7; border:0; border-radius:5px; font-weight:700; }.primary-command { padding:9px 16px; color:white; background:#43b99f; border-radius:21px; text-decoration:none; font-weight:700; }
.ingredient-identity-data { display:none; }
.ingredient-prep-section .panel-intro { max-width:650px; line-height:1.55; }
.prep-action-table td:first-child small { display:block; margin-top:3px; color:#8d93a7; font-weight:400; }
.prep-action-editor .inline-form { display:grid; align-items:end; grid-template-columns:minmax(150px,1.2fr) minmax(110px,.55fr) minmax(160px,1fr) auto; }
.prep-action-editor label { display:grid; gap:5px; color:#8b92a6; font-size:11px; }
.prep-action-editor label input,.prep-action-editor label select { width:100%; }
.percent-input { display:flex; align-items:center; min-width:0; border:1px solid #dfe3ec; background:#fff; }
.percent-input input { min-width:0; border:0 !important; }
.percent-input i { padding-right:10px; color:#8b92a6; font-style:normal; }
.field-help { margin:10px 0 0; color:#9298aa; font-size:12px; line-height:1.5; }
.alias-editor { display:grid; gap:7px; margin-top:14px; color:#8b92a6; font-size:11px; }
.alias-editor textarea { width:100%; padding:10px 12px; resize:vertical; border:1px solid #dfe3ec; border-radius:3px; color:#111a49; font:14px var(--sans); }
@media (max-width:720px) { .prep-action-table { display:block; overflow-x:auto; }.prep-action-table th,.prep-action-table td { min-width:90px; white-space:nowrap; }.prep-action-editor .inline-form { grid-template-columns:1fr; } }
.ingredient-title-editor { overflow:hidden; resize:none; field-sizing:content; }
.ingredient-prep-section .inline-editor > summary { display:inline-flex; align-items:center; min-height:40px; margin-top:5px; padding:8px 16px; color:#202962; background:#fff; border:1px solid #202962; border-radius:22px; font-size:14px; font-weight:600; }
.ingredient-additional-edit { padding:42px 32px 62px !important; border:1px solid #f3f3f3 !important; border-radius:4px; }
.ingredient-detail-fields { display:grid; gap:24px; margin-top:28px; }
.ingredient-detail-fields > label { display:grid; gap:8px; color:#3c4679; font-size:14px; }
.ingredient-detail-fields > label > small { margin-top:-5px; color:#a5a9c1; font-size:11px; }
.ingredient-detail-fields input,.ingredient-detail-fields textarea { width:100%; min-height:44px; padding:10px 12px; color:#050841; background:#fff; border:1px solid #ececec; border-bottom-color:#b5b5b5; border-radius:0; font:15px var(--sans); resize:vertical; }
.ingredient-detail-fields textarea:focus,.ingredient-detail-fields input:focus { background:#f1f5fe; border-bottom-color:#3d5df6; outline:0; }
.ingredient-tag-view,.ingredient-description-view { padding:16px 0; border-bottom:1px solid #eeeef3; }
.ingredient-tag-view > strong,.ingredient-description-view > strong { display:block; margin-bottom:10px; color:#3c4679; font-size:13px; }
.ingredient-tag-view > span { display:flex; flex-wrap:wrap; gap:7px; }
.ingredient-tag-view i { padding:5px 11px; color:#202962; background:#f1f5fe; border-radius:16px; font-size:12px; font-style:normal; }
.ingredient-description-view p { margin:0; color:#505773; line-height:1.55; }
.ingredient-cost-edit { display:grid; gap:28px; }
.ingredient-purchase-form { padding-bottom:28px; border-bottom:1px solid #f3f3f3; }
.ingredient-purchase-form > header { display:flex; align-items:center; justify-content:space-between; gap:15px; margin-bottom:18px; }
.ingredient-purchase-form h3 { margin:0; color:#3c4679; font-size:14px; font-weight:600; }
.ingredient-purchase-form > header form { margin:0; }
.ingredient-purchase-form > header button { padding:5px 7px; color:#f63d48; background:transparent; border:0; cursor:pointer; font-size:11px; }
.ingredient-purchase-form .purchase-item-form { margin:0; padding:0; background:#fff; border:0; border-radius:0; }
.ingredient-purchase-form .purchase-item-form label { color:#3c4679; font-size:11px; font-weight:500; letter-spacing:0; text-transform:none; }
.ingredient-purchase-form .purchase-item-form input,.ingredient-purchase-form .purchase-item-form select { min-height:44px; padding:9px 10px; background:#ffffff; border-color:#ececec; border-radius:0; }
.ingredient-purchase-form .purchase-form-actions button { padding:9px 16px; background:#3d5df6; border-color:#3d5df6; border-radius:20px; }
.ingredient-cost-row { display:flex; align-items:center; justify-content:space-between; gap:20px; min-height:68px; padding:12px 0; border-bottom:1px solid #edf0f5; }
.ingredient-cost-row > span:last-child { text-align:right; }
.ingredient-cost-row strong,.ingredient-cost-row small { display:block; }
.ingredient-cost-row small { margin-top:3px; color:#a5a9c1; font-size:11px; }
.detail-actions-menu label { display:grid; gap:6px; padding:9px 14px; color:#a5a9c1; font-size:11px; }
.detail-actions-menu select { width:250px; padding:9px; }
.application-body .ingredient-actions-menu > div { width:290px; }
@media (min-width:981px) { .application-body .entity-detail-shell .entity-primary { background:#ffffff; } }
.application-body .entity-detail-shell .entity-detail-header { background:#ffffff; }
.application-body .entity-detail-shell .entity-primary { padding-top:35px; }
.application-body .entity-detail-shell .entity-section { margin-bottom:35px; }
.application-body .entity-detail-shell .entity-section h2,.application-body .entity-detail-shell .entity-tab-panel h2 { color:#050841; font-size:22px; font-weight:700; line-height:1.25; }
.application-body .entity-detail-shell .panel-intro { max-width:640px; margin:5px 0 20px; color:#95969c; font-size:14px; line-height:1.5; }
.application-body .entity-detail-shell .entity-tabs { position:relative; min-height:58px; background:#fff; border-color:#edf0f5; }
.application-body .entity-detail-shell .entity-tabs button { min-height:58px; padding:8px 12px; color:#050841; font-size:15px; font-weight:400; }
.application-body .entity-detail-shell .entity-tabs button.active { color:#050841; background:#f1f5fe; border-bottom:2px solid #3d5df6; }
.application-body .entity-detail-shell .entity-tab-panel { padding:35px 2px 60px; }
.application-body .entity-detail-shell .prep-action-table { width:100%; margin-top:26px; table-layout:fixed; }
.application-body .entity-detail-shell .prep-action-table th { height:40px; padding:8px 10px; color:#757677; background:#ffffff; border-color:#edf0f5; font-size:12px; }
.application-body .entity-detail-shell .prep-action-table td { height:58px; padding:10px; color:#050841; background:#fff; border:1px solid #edf0f5; font-size:14px; }
.application-body .entity-detail-shell .inline-editor > summary,.application-body .entity-detail-shell .add-purchase > summary { display:inline-flex; align-items:center; min-height:40px; padding:8px 16px; color:#202962; background:#fff; border:1px solid #202962; border-radius:22px; font-size:14px; font-weight:600; list-style:none; }
.application-body .entity-detail-shell .inline-editor > summary::-webkit-details-marker,.application-body .entity-detail-shell .add-purchase > summary::-webkit-details-marker { display:none; }
.application-body .entity-detail-shell .inline-form { gap:10px; margin-top:18px; padding-top:18px; border-color:#f3f3f3; }
.application-body .entity-detail-shell .inline-form label { color:#3c4679; font-size:11px; font-weight:500; }
.application-body .entity-detail-shell .inline-form input,.application-body .entity-detail-shell .inline-form select { min-height:44px; padding:9px 10px; color:#050841; background:#fff; border:1px solid #ececec; border-radius:0; font-size:14px; }
.application-body .entity-detail-shell .inline-form input:focus,.application-body .entity-detail-shell .inline-form select:focus { background:#f1f5fe; border-color:#3d5df6; outline:0; }
.application-body .entity-detail-shell .inline-form button,.application-body .entity-detail-shell .usda-id-form button { min-height:38px; padding:8px 16px; color:#fff; background:#3d5df6; border:0; border-radius:20px; font-size:13px; font-weight:600; }
.application-body .equivalency-editor .inline-form { display:grid; grid-template-columns:90px minmax(120px,1fr) auto 90px minmax(120px,1fr) minmax(130px,1fr) auto; align-items:end; }
.application-body .equivalency-editor .inline-form label { display:grid; gap:5px; }
.application-body .equivalency-editor .inline-form > i { align-self:center; color:#a5a9c1; font-style:normal; }
.application-body .equivalency-list { margin-top:8px; }
.application-body .equivalency-list p { min-height:58px; margin:0; padding:12px 8px; border-color:#f3f3f3; color:#050841; }
.application-body .equivalency-list p small { color:#a5a9c1; font-size:11px; }
.application-body .equivalency-row { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; border-bottom:1px solid #f3f3f3; }
.application-body .equivalency-row > p { border-bottom:0; }
.application-body .equivalency-actions { display:flex; gap:4px; }
.application-body .equivalency-actions form { margin:0; }
.application-body .equivalency-actions button,.application-body .cancel-equivalency { padding:6px 8px; color:#3d5df6; background:transparent; border:0; cursor:pointer; font-size:11px; }
.application-body .equivalency-actions form button { color:#f63d48; }
.application-body .cancel-equivalency { align-self:end; min-height:38px; }
.application-body .usda-id-form { gap:10px; margin:24px 0; padding:18px 0; border-block:1px solid #f3f3f3; }
.application-body .usda-id-form label { gap:6px; color:#3c4679; font-size:11px; font-weight:500; }
.application-body .usda-id-form input { min-height:44px; padding:9px 10px; background:#fff; border:1px solid #ececec; border-radius:0; font-size:14px; }
.application-body .usda-id-form a,.application-body .nutrition-mapping-head a { color:#3d5df6; font-size:12px; font-weight:600; text-decoration:none; }
.application-body .ingredient-nutrition { margin-top:20px; padding:0; background:#fff; border:0; border-radius:0; }
.application-body .nutrition-mapping-head { padding:0 0 14px; border-bottom:1px solid #3d5df6; }
.application-body .ingredient-nutrition dl div { min-height:44px; padding:10px 6px; border-color:#f3f3f3; font-size:13px; }
.application-body .ingredient-nutrition footer { color:#a5a9c1; }
.application-body .prep-edit-wrap { overflow-x:auto; margin-top:26px; }
.application-body .prep-edit-table { min-width:700px; margin-top:0 !important; }
.application-body .prep-edit-table th:first-child { width:31%; }
.application-body .prep-edit-table th:nth-child(2) { width:15%; }
.application-body .prep-edit-table th:last-child { width:44px; }
.application-body .prep-edit-table select,.application-body .prep-edit-table input { width:100%; min-height:36px; padding:7px 8px; color:#050841; background:#fff; border:0; border-bottom:1px solid #ececec; border-radius:0; font-size:13px; }
.application-body .prep-edit-table .prep-row-notes { margin-top:3px; color:#757677; font-size:11px; font-style:italic; }
.application-body .prep-edit-table .percent-input { border:0; }
.application-body .prep-edit-table .percent-input i { padding-right:5px; }
.application-body .prep-row-remove { display:grid; place-items:center; width:30px; height:30px; padding:0; color:#a5a9c1; background:transparent; border:0; border-radius:50%; cursor:pointer; font-size:20px; }
.application-body .prep-row-remove:hover { color:#f63d48; background:#fff1f2; }
.application-body .prep-equivalency-link { width:100%; min-height:32px; padding:4px; color:#3d5df6; background:transparent; border:0; border-radius:3px; cursor:pointer; font-size:12px; text-align:left; }
.application-body .prep-equivalency-link:hover { background:#f1f5fe; text-decoration:underline; }
.application-body .outlined-add-action { min-height:40px; margin-top:20px; padding:8px 16px; color:#202962; background:#fff; border:1px solid #202962; border-radius:22px; cursor:pointer; font-size:14px; font-weight:600; }
.application-body #ingredient-done.dirty::after { content:""; display:inline-block; width:6px; height:6px; margin-left:7px; background:#fff; border-radius:50%; vertical-align:middle; }
.recipe-tab-panel { display:none; }.recipe-tab-panel.active { display:block; }.recipe-view-details > .recipe-derived-panel { margin-top:0; padding-top:0; border-top:0; }.recipe-view-details .derived-grid { display:block; }.recipe-view-details .derived-card { border:0; padding:0; }.recipe-view-details .derived-title h2 { font:700 1.4rem var(--sans); }
.recipe-view-equivalencies h2,.recipe-equivalence-editor h2 { font:700 1.4rem var(--sans); }.recipe-view-equivalencies > div,.recipe-equivalence-editor > div { display:grid; grid-template-columns:1fr auto 1fr; gap:12px; padding:14px 4px; border-bottom:1px solid #e8eaf0; }.recipe-view-equivalencies small,.recipe-equivalence-editor small { grid-column:1/-1; color:#8b93a7; }.recipe-equivalence-editor { margin:30px 38px; padding:24px; border:1px solid #e8eaf0; }.recipe-equivalence-editor > p { color:#8b93a7; }.recipe-equivalence-editor form { display:grid; grid-template-columns:90px 1fr auto 90px 1fr minmax(150px,1fr) auto; align-items:center; gap:8px; margin-top:20px; }.recipe-equivalence-editor input,.recipe-equivalence-editor select { min-width:0; padding:9px; border:1px solid #dfe3ec; }.recipe-equivalence-editor button { padding:10px 12px; color:white; background:#4868f7; border:0; border-radius:5px; font-weight:700; }
.editable-entity-title {
display:block;
width:min(680px,50vw);
margin:0;
padding:2px 0 4px;
color:#050841;
background:transparent;
border:0;
border-bottom:1px solid transparent;
border-radius:0;
font-family:var(--meez-font-sans);
font-size:28px;
font-weight:900;
line-height:42px;
letter-spacing:0;
}
.editable-entity-title:hover,
.editable-entity-title:focus {
border-bottom-color:#3d5df6;
outline:0;
background:transparent;
}
#recipe-done {
display:inline-flex;
align-items:center;
justify-content:center;
min-height:32px;
padding:6px 18px;
color:#fff;
background:#3d5df6;
border:0;
border-radius:100px;
font-family:var(--meez-font-sans);
font-size:15px;
font-weight:500;
cursor:pointer;
box-shadow:none;
}
#recipe-done:hover {
background:#2f4de0;
}
.calculate-toggle { display:inline-flex; align-items:center; gap:9px; color:#26346d; font-size:.82rem; font-weight:700; white-space:nowrap; }.calculate-toggle input { position:absolute; opacity:0; pointer-events:none; }.calculate-toggle i { position:relative; width:35px; height:20px; background:#cbd1dc; border-radius:12px; }.calculate-toggle i::after { content:""; position:absolute; top:3px; left:3px; width:14px; height:14px; background:#fff; border-radius:50%; transition:.15s; }.calculate-toggle input:checked + i { background:#43b99f; }.calculate-toggle input:checked + i::after { transform:translateX(15px); }.calculator-heading .calculate-toggle { grid-column:1/-1; justify-self:end; }.structure-heading-actions { display:flex; align-items:center; gap:18px; }.recipe-edit-workspace { display:grid; grid-template-columns:52% 48%; border-top:1px solid #e8eaf0; }.recipe-edit-structure { min-width:0; padding:0 32px 48px 38px; border-right:1px solid #e8eaf0; }.recipe-edit-structure .structure-editor { display:block; }.recipe-edit-structure .section-heading { min-height:76px; margin:0; padding:18px 0 12px; }.recipe-edit-structure .component-editor { margin-right:0; background:#fff; border-color:#e8eaf0; }.recipe-edit-static-method { min-width:0; padding:28px 30px 60px; }.recipe-edit-structure .editor-items { min-width:54rem; }.recipe-edit-structure .editor-items th:nth-child(2),.recipe-edit-structure .editor-items td:nth-child(2) { width:6rem; }.recipe-edit-structure .editor-items th:nth-child(3),.recipe-edit-structure .editor-items td:nth-child(3) { width:7rem; text-align:right; }.recipe-edit-structure .editor-items th:nth-child(4),.recipe-edit-structure .editor-items td:nth-child(4) { width:11rem; }.recipe-edit-structure .editor-items th:nth-child(5),.recipe-edit-structure .editor-items td:nth-child(5) { width:9rem; }.recipe-edit-structure .editor-items th:nth-child(6),.recipe-edit-structure .editor-items td:nth-child(6) { width:6rem; }.recipe-edit-structure .editor-items th:last-child,.recipe-edit-structure .editor-items td:last-child { width:8rem; }.recipe-edit-static-method h2 { margin-bottom:18px; }
.percent-mode { display:inline-flex; border:1px solid #dfe3ec; border-radius:18px; overflow:hidden; }.percent-mode button { min-width:96px; padding:7px 11px; color:#26346d; background:#fff; border:0; cursor:pointer; font-weight:700; }.percent-mode button.active { background:#e9efff; }.calculator-percent-controls { grid-column:1/-1; display:flex; align-items:center; justify-content:space-between; gap:14px; }.calculator-percent-controls .calculate-toggle { justify-self:auto; }.basis-choice { display:inline-flex; align-items:center; gap:4px; margin-left:10px; color:#4868f7; font-size:.68rem; font-weight:700; }.basis-choice input { width:auto; }.structure-heading-actions { flex-wrap:wrap; justify-content:flex-end; }
.editor-items .base-column { width:4.5rem; text-align:center; }.editor-items .base-column input { width:17px; height:17px; margin:0; accent-color:#4868f7; }.bakers-base-summary { display:grid; grid-template-columns:55px 1fr; gap:14px; margin:0 0 28px; padding:18px; background:#f7f7f8; border-radius:5px; }.bakers-base-summary > strong { color:#a0a7ba; font-weight:500; }.bakers-base-summary p { display:grid; grid-template-columns:80px 1fr; gap:10px; margin:0 0 10px; }.bakers-base-summary p:last-child { margin-bottom:0; }.bakers-base-summary a { color:#111a49; text-decoration:none; font-weight:700; }.bakers-base-summary span { color:#26346d; }
.bakers-base-summary p > span { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; border-bottom:1px solid #a5a9c1; }
.bakers-base-summary input,.bakers-base-summary select { min-width:0; padding:3px 2px; color:#26346d; background:transparent; border:0; font:inherit; }
.bakers-base-summary input { width:100%; text-align:right; }
.bakers-base-summary select { max-width:48px; color:#3d5df6; }
.entity-directory-table { background:#fff; border:1px solid #eef0f5; }.entity-directory-toolbar { display:grid; grid-template-columns:36px 64px minmax(12rem,1fr) auto; align-items:center; gap:8px; min-height:58px; padding:0 20px; border-bottom:1px solid #eef0f5; }.entity-directory-toolbar input,.entity-directory-row > input { width:20px; height:20px; margin:0; accent-color:#3d5df6; }.entity-directory-toolbar strong { color:#5366fa; font-size:.88rem; }.entity-directory-toolbar button { padding:7px 10px; color:#64748b; background:transparent; border:0; cursor:pointer; }.entity-directory-toolbar .bulk-delete { color:#a73838; }.directory-error { margin:0; padding:11px 20px; color:#8d2e2e; background:#fff1f1; border-bottom:1px solid #f5cece; }.entity-directory-head,.entity-directory-row { display:grid; align-items:center; gap:8px; padding:0 20px; }.entity-directory-head { grid-template-columns:36px 64px minmax(14rem,1fr) repeat(var(--directory-columns),minmax(7rem,10rem)) 28px; min-height:38px; color:#8b93a7; border-bottom:1px solid #eef0f5; font-size:.7rem; text-transform:uppercase; }.entity-directory-head > span:first-child { grid-column:auto; }.entity-directory-row { grid-template-columns:36px 64px minmax(14rem,1fr) repeat(var(--directory-columns),minmax(7rem,10rem)) 28px; min-height:62px; border-bottom:1px solid #eef0f5; }.entity-directory-row:last-child { border-bottom:0; }.entity-directory-row.selected { background:#edf2ff; }.entity-directory-head.columns-1,.entity-directory-row.columns-1 { --directory-columns:1; }.entity-directory-head.columns-2,.entity-directory-row.columns-2 { --directory-columns:2; }.entity-directory-head.columns-3,.entity-directory-row.columns-3 { --directory-columns:3; }.entity-directory-name { display:flex; flex-direction:column; min-width:0; }.entity-directory-name a { overflow:hidden; color:#111a49; text-decoration:none; text-overflow:ellipsis; white-space:nowrap; }.entity-directory-name small { color:#8b93a7; }.directory-attention { display:flex; gap:5px; }.directory-attention em { padding:2px 5px; color:#9a3412; background:#fff0e8; border-radius:3px; font-size:.62rem; font-style:normal; font-weight:700; text-transform:uppercase; }.entity-directory-value { overflow:hidden; color:#687086; font-size:.82rem; text-overflow:ellipsis; white-space:nowrap; }.entity-row-actions { position:relative; }.entity-row-actions > summary { color:#a0a7ba; cursor:pointer; font-size:1.4rem; list-style:none; text-align:center; }.entity-row-actions > summary::-webkit-details-marker { display:none; }.entity-row-actions > div { position:absolute; z-index:5; top:28px; right:0; width:130px; padding:5px; background:#fff; border:1px solid #dfe3ec; border-radius:5px; box-shadow:0 8px 22px rgba(20,30,60,.14); }.entity-row-actions a,.entity-row-actions button { display:block; width:100%; padding:8px 10px; color:#26346d; background:transparent; border:0; text-align:left; text-decoration:none; cursor:pointer; }.entity-row-actions button { color:#a73838; }.entity-row-actions a:hover,.entity-row-actions button:hover { background:#f7f8fc; }
.delete-confirmation { width:min(700px,calc(100% - 32px)); padding:30px 38px; color:#050841; background:#fff; border:0; border-radius:10px; box-shadow:0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12); }.delete-confirmation::backdrop { background:rgba(0,0,0,0.5); }.delete-confirmation h2 { margin:0 30px 12px 0; color:#202962; font:700 28px/39px var(--sans); }.delete-confirmation p { margin:0 0 30px; color:#050841; font-size:15px; font-weight:400; line-height:1.5; }.delete-confirmation > div { display:flex; justify-content:flex-end; align-items:center; gap:52px; }.delete-confirmation button { padding:0; background:transparent; border:0; cursor:pointer; color:#050841; font-size:15px; font-weight:500; }.delete-confirmation .confirm-delete { min-width:250px; padding:16px 24px; color:#fff; background:#f63d48; border-radius:100px; font-size:16px; font-weight:500; line-height:16px; }.delete-confirmation .dialog-close { position:absolute; top:18px; right:18px; padding:4px; color:#a5a9c1; border:0; font-size:24px; }
.entity-directory-head,.entity-directory-row,.entity-directory-head.columns-1,.entity-directory-head.columns-2,.entity-directory-head.columns-3,.entity-directory-row.columns-1,.entity-directory-row.columns-2,.entity-directory-row.columns-3 { grid-template-columns:36px 64px minmax(14rem,1fr) 28px; }.entity-directory-head > span:first-child { grid-column:auto; }.entity-directory-head > span:nth-child(2) { text-align:left; }.entity-directory-name { justify-content:center; }.entity-directory-toolbar { min-height:52px; }
File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
/* Purchasing Review Tool */
.application-body .purchasing-review-page { min-height: 100vh; background: #ffffff; padding-top: 48px; }
.application-body .purchasing-review-workspace { width: min(1120px, calc(100% - 48px)); margin: 0 auto; padding: 28px 0 64px; }
.application-body .purchasing-review-header { margin-bottom: 24px; }
.application-body .purchasing-breadcrumbs { margin-bottom: 8px; }
.application-body .purchasing-breadcrumbs a { display: inline-flex; align-items: center; gap: 4px; color: #3d5df6; font-size: 14px; font-weight: 500; text-decoration: none; }
.application-body .purchasing-review-header h1 { margin: 0 0 6px; color: #050841; font-size: 28px; font-weight: 700; letter-spacing: -0.015em; }
.application-body .purchasing-subtitle { color: #8b93a7; font-size: 14px; margin: 0; }
.application-body .purchasing-review-toolbar { position: sticky; top: 52px; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 20px; margin-bottom: 24px; background: #fff; border: 1px solid #eef0f5; border-radius: 8px; box-shadow: 0 2px 8px rgba(5,8,65,0.06); }
.application-body .purchasing-review-stats { display: flex; flex-direction: column; gap: 2px; }
.application-body .stats-counter { display: flex; align-items: center; gap: 6px; }
.application-body .stats-count { color: #3d5df6; font-size: 16px; font-weight: 700; }
.application-body .stats-label { color: #050841; font-size: 14px; font-weight: 500; }
.application-body .stats-hint { color: #8b93a7; font-size: 12px; }
.application-body .purchasing-review-actions { display: flex; align-items: center; gap: 12px; }
.application-body .purchasing-search-box { display: flex; align-items: center; gap: 6px; min-height: 38px; padding: 0 12px; background: #ffffff; border: 1px solid #dfe3ec; border-radius: 4px; }
.application-body .purchasing-search-box .search-icon { color: #8283a0; flex: none; }
.application-body .purchasing-search-box input { border: 0; background: transparent; outline: none; font-family: var(--meez-font-sans); font-size: 14px; color: #050841; min-width: 180px; }
.application-body .search-clear-btn { background: transparent; border: 0; color: #a5a9c1; font-size: 18px; cursor: pointer; padding: 0 2px; }
.application-body .purchasing-filter-toggle { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; }
.application-body .purchasing-filter-toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.application-body .toggle-track { display: inline-block; width: 34px; height: 18px; background: #cbd1dc; border-radius: 12px; position: relative; transition: background 0.15s ease; }
.application-body .toggle-thumb { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; background: #fff; border-radius: 50%; transition: transform 0.15s ease; }
.application-body .purchasing-filter-toggle input:checked + .toggle-track { background: #3d5df6; }
.application-body .purchasing-filter-toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(16px); }
.application-body .toggle-label { color: #050841; font-size: 13px; font-weight: 500; }
.application-body .purchasing-export-btn { display: inline-flex; align-items: center; gap: 6px; min-height: 38px; padding: 0 16px; background: #3d5df6; color: #fff; border: 0; border-radius: 100px; font-family: var(--meez-font-sans); font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 2px 6px rgba(61, 93, 246, 0.28); transition: background 0.15s ease; }
.application-body .purchasing-export-btn:hover { background: #2b4be0; }
.application-body .purchasing-products-grid { display: flex; flex-direction: column; gap: 20px; }
.application-body .purchasing-card { padding: 24px; background: #fff; border: 1px solid #eef0f5; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); transition: border-color 0.15s ease; }
.application-body .purchasing-card.resolved { border-left: 3px solid #40b49a; }
.application-body .purchasing-card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid #edf0f5; }
.application-body .purchasing-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.application-body .supplier-badge { display: inline-flex; padding: 2px 8px; background: #eef2ff; border-radius: 4px; color: #3d5df6; font-size: 12px; font-weight: 600; text-transform: capitalize; }
.application-body .supplier-badge.walmart { background: #e6f1fc; color: #0071dc; }
.application-body .supplier-badge.sams_club { background: #e1f5fe; color: #0067a0; }
.application-body .sku-badge, .application-body .package-badge { display: inline-flex; padding: 2px 8px; background: #f7f7f8; border: 1px solid #ececec; border-radius: 4px; color: #687086; font-size: 12px; font-weight: 500; }
.application-body .price-badge { display: inline-flex; padding: 2px 8px; background: #e8f5e9; border-radius: 4px; color: #2e7d32; font-size: 12px; font-weight: 700; }
.application-body .purchasing-product-name { margin: 0; color: #050841; font-size: 18px; font-weight: 600; }
.application-body .purchasing-external-link { display: inline-flex; align-items: center; gap: 4px; color: #3d5df6; font-size: 13px; font-weight: 500; text-decoration: none; white-space: nowrap; }
.application-body .purchasing-external-link:hover { text-decoration: underline; }
.application-body .candidates-heading { display: block; margin-bottom: 12px; color: #687086; font-size: 13px; font-weight: 600; }
.application-body .candidate-options-list { display: flex; flex-direction: column; gap: 8px; }
.application-body .candidate-option-card { display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 10px 16px; background: #fff; border: 1px solid #dfe3ec; border-radius: 6px; cursor: pointer; transition: all 0.12s ease; }
.application-body .candidate-option-card:hover { background: #f7f9fd; border-color: #bcc6e5; }
.application-body .candidate-option-card.selected { background: #f1f5fe; border-color: #3d5df6; box-shadow: 0 0 0 1px #3d5df6; }
.application-body .candidate-radio { display: none; }
.application-body .candidate-custom-radio { display: grid; place-items: center; width: 18px; height: 18px; border: 2px solid #a5a9c1; border-radius: 50%; flex: none; transition: all 0.12s ease; }
.application-body .candidate-custom-radio i { width: 8px; height: 8px; background: transparent; border-radius: 50%; transition: background 0.12s ease; }
.application-body .candidate-option-card.selected .candidate-custom-radio { border-color: #3d5df6; }
.application-body .candidate-option-card.selected .candidate-custom-radio i { background: #3d5df6; }
.application-body .candidate-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.application-body .candidate-name { color: #050841; font-size: 15px; font-weight: 500; }
.application-body .candidate-meta { color: #8b93a7; font-size: 12px; }
.application-body .candidate-score-badge { display: inline-flex; padding: 2px 8px; background: #f1f5fe; border-radius: 12px; color: #3d5df6; font-size: 12px; font-weight: 600; }
.application-body .candidate-score-badge.high { background: #e8f5e9; color: #2e7d32; }
.application-body .candidate-option-card.none { border-style: dashed; }
.application-body .candidate-other-option { margin: 4px 0; padding: 6px 0; }
.application-body .other-select-label { display: flex; align-items: center; gap: 12px; }
.application-body .other-select-text { color: #687086; font-size: 13px; font-weight: 500; white-space: nowrap; }
.application-body .purchasing-select { flex: 1; min-height: 38px; padding: 6px 12px; background: #fff; border: 1px solid #dfe3ec; border-radius: 4px; color: #050841; font-family: var(--meez-font-sans); font-size: 14px; outline: none; cursor: pointer; }
.application-body .purchasing-select:focus { border-color: #3d5df6; }
.application-body .purchasing-notice-card { display: flex; align-items: flex-start; gap: 14px; padding: 24px; background: #fff; border: 1px solid #eef0f5; border-radius: 6px; color: #050841; }
.application-body .purchasing-notice-card svg { color: #3d5df6; flex: none; margin-top: 2px; }
.application-body .purchasing-notice-card strong { display: block; margin-bottom: 4px; font-size: 16px; }
.application-body .purchasing-notice-card p { margin: 0; color: #8b93a7; font-size: 14px; }
.application-body .ingredient-merge { margin:24px 0; padding:16px; border:1px solid #f0d6d6; }
.application-body .ingredient-merge summary { color:#a73838; cursor:pointer; font-weight:600; }
.application-body .ingredient-merge form { display:flex; gap:8px; }
.application-body .ingredient-merge select { flex:1; padding:8px; }
.application-body .ingredient-merge button { color:#fff; background:#f63d48; border:0; padding:8px 12px; }
.application-body .recipe-additional-view { display:grid; grid-template-columns:minmax(0,360px) minmax(0,1fr); gap:32px; margin:36px 38px; padding-top:30px; border-top:1px solid #eeeef3; }.application-body .recipe-additional-view figure { margin:0; }.application-body .recipe-additional-view img { display:block; width:100%; max-height:300px; object-fit:cover; }.application-body .recipe-additional-view h2 { font-size:20px; }.application-body .recipe-additional-view p { display:grid; grid-template-columns:120px 1fr; padding:10px 0; border-bottom:1px solid #eeeef3; }.application-body .recipe-additional-view ul { color:#202962; }
.application-body .recipe-media-form { grid-template-columns:110px 150px 1fr 1fr auto; align-items:end; margin-top:26px; padding-top:20px; border-top:1px solid #eeeef3; }.application-body .recipe-media-form h3 { grid-column:1/-1; margin:0; font-size:16px; }.application-body .step-media { margin:14px 0 0; }.application-body .step-media img,.application-body .step-media video { display:block; width:min(100%,420px); max-height:300px; object-fit:cover; }.application-body .step-media figcaption { margin-top:5px; color:#8b93a7; font-size:12px; }
/* Responsive application geometry. Keep wide workspaces useful without allowing
a two-pane layout to squeeze either pane below its usable width. */
.application-body { overflow-x:clip; }
.application-body .entity-detail-shell,.application-body .recipe-detail-shell,
.application-body .entity-detail-grid,.application-body .entity-primary,.application-body .entity-secondary,
.application-body .recipe-view-workspace,.application-body .recipe-view-formula,.application-body .recipe-view-details,
.application-body .recipe-edit-workspace,.application-body .recipe-edit-structure,.application-body .recipe-edit-static-method,
.application-body .structure-editor,.application-body .component-editor,.application-body .table-wrap { min-width:0; max-width:100%; }
.application-body .table-wrap { overflow-x:auto; overscroll-behavior-inline:contain; scrollbar-gutter:stable; }
+73
View File
@@ -0,0 +1,73 @@
/* Recipe Books */
.application-body .recipe-book-page { min-height: 100vh; background: #ffffff; padding-top: 48px; }
.application-body .recipe-book-workspace { width: min(1120px, calc(100% - 48px)); margin: 0 auto; padding: 28px 0 64px; }
.application-body .recipe-book-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 28px; }
.application-body .recipe-book-breadcrumbs { margin-bottom: 8px; }
.application-body .recipe-book-breadcrumbs a { display: inline-flex; align-items: center; gap: 4px; color: #3d5df6; font-size: 14px; font-weight: 500; text-decoration: none; transition: color 0.15s ease; }
.application-body .recipe-book-breadcrumbs a:hover { color: #202962; }
.application-body .recipe-book-header h1 { margin: 0 0 6px; color: #050841; font-size: 28px; font-weight: 700; letter-spacing: -0.015em; }
.application-body .recipe-book-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; }
.application-body .recipe-book-count-badge { display: inline-flex; align-items: center; padding: 3px 10px; background: #fff; border: 1px solid #dfe3ec; border-radius: 999px; color: #687086; font-size: 13px; font-weight: 500; }
.application-body .recipe-book-count-badge .count-number { color: #3d5df6; font-weight: 700; margin-right: 4px; }
.application-body .recipe-book-desc, .application-body .recipe-book-subtitle { color: #8b93a7; font-size: 14px; font-weight: 400; margin: 0; }
.application-body .recipe-book-header-actions { display: flex; align-items: center; gap: 10px; }
.application-body .book-action-btn { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 16px; color: #3d5df6; background: #fff; border: 1px solid #dfe3ec; border-radius: 100px; font-size: 14px; font-weight: 500; text-decoration: none; box-shadow: 0 1px 2px rgba(5,8,65,0.04); transition: all 0.15s ease; }
.application-body .book-action-btn:hover { background: #f7f9fd; border-color: #bcc6e5; }
.application-body .book-action-btn.active { color: #687086; }
.application-body .book-notice { margin-bottom: 20px; padding: 12px 16px; background: #fff1f1; border: 1px solid #f5cece; border-radius: 6px; color: #8d2e2e; font-size: 14px; }
.application-body .book-directory-table { background: #fff; border: 1px solid #eef0f5; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.application-body .book-directory-toolbar { display: flex; align-items: center; justify-content: space-between; min-height: 48px; padding: 0 20px; background: #ffffff; border-bottom: 1px solid #edf0f5; }
.application-body .book-toolbar-title { color: #050841; font-size: 14px; font-weight: 600; }
.application-body .book-toolbar-count { color: #8b93a7; font-size: 13px; font-weight: 400; }
.application-body .book-recipe-list { display: flex; flex-direction: column; }
.application-body .book-recipe-row { display: grid; grid-template-columns: 36px minmax(0, 1fr) 36px; align-items: center; gap: 12px; min-height: 58px; padding: 0 20px; border-bottom: 1px solid #edf0f5; transition: background-color 0.12s ease; }
.application-body .book-recipe-row:last-child { border-bottom: 0; }
.application-body .book-recipe-row:hover { background: #f8faff; }
.application-body .book-recipe-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; background: #3c4679; border-radius: 50%; color: #fff; }
.application-body .book-recipe-title { color: #050841; font-size: 15px; font-weight: 500; text-decoration: none; transition: color 0.12s ease; }
.application-body .book-recipe-title:hover { color: #3d5df6; }
.application-body .book-recipe-arrow { display: grid; place-items: center; width: 28px; height: 28px; color: #a5a9c1; border-radius: 50%; text-decoration: none; transition: all 0.12s ease; }
.application-body .book-recipe-row:hover .book-recipe-arrow { color: #3d5df6; background: #f1f5fe; }
.application-body .book-empty-state, .application-body .archive-empty-state, .application-body .purchasing-empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 64px 24px; background: #fff; border: 1px solid #eef0f5; border-radius: 6px; text-align: center; }
.application-body .book-empty-state .empty-icon { display: grid; place-items: center; width: 54px; height: 54px; margin-bottom: 16px; background: #f1f5fe; border-radius: 50%; color: #3d5df6; font-size: 24px; }
.application-body .book-empty-state h3, .application-body .archive-empty-state h3, .application-body .purchasing-empty-state h3 { margin: 0 0 6px; color: #050841; font-size: 18px; font-weight: 600; }
.application-body .book-empty-state p, .application-body .archive-empty-state p, .application-body .purchasing-empty-state p { margin: 0 0 20px; color: #8b93a7; font-size: 14px; max-width: 420px; }
.application-body .book-add-recipes-btn, .application-body .archive-back-btn, .application-body .purchasing-reset-btn { display: inline-flex; align-items: center; gap: 6px; min-height: 38px; padding: 0 20px; background: #3d5df6; color: #fff; border: 0; border-radius: 100px; font-size: 14px; font-weight: 500; text-decoration: none; cursor: pointer; box-shadow: 0 2px 6px rgba(61, 93, 246, 0.28); transition: background 0.15s ease; }
.application-body .book-add-recipes-btn:hover, .application-body .archive-back-btn:hover, .application-body .purchasing-reset-btn:hover { background: #2b4be0; }
/* Recipe Book Editing */
.application-body .book-edit-form { display: flex; flex-direction: column; gap: 20px; }
.application-body .book-edit-card { padding: 24px; background: #fff; border: 1px solid #eef0f5; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.application-body .book-edit-card h2 { margin: 0 0 16px; color: #050841; font-size: 18px; font-weight: 600; }
.application-body .book-edit-card-header { margin-bottom: 16px; }
.application-body .section-subtitle { margin: 4px 0 0; color: #8b93a7; font-size: 14px; }
.application-body .book-field-group { display: flex; flex-direction: column; gap: 16px; }
.application-body .book-field-group label { display: flex; flex-direction: column; gap: 6px; }
.application-body .field-label { color: #050841; font-size: 13px; font-weight: 600; }
.application-body .book-input, .application-body .book-textarea { width: 100%; padding: 10px 14px; background: #fff; border: 1px solid #dfe3ec; border-radius: 4px; color: #050841; font-family: var(--meez-font-sans); font-size: 15px; font-weight: 400; outline: none; box-sizing: border-box; transition: border-color 0.15s ease, box-shadow 0.15s ease; }
.application-body .book-input:focus, .application-body .book-textarea:focus { border-color: #3d5df6; box-shadow: 0 0 0 2px rgba(61, 93, 246, 0.15); }
.application-body .book-recipe-checklist { display: flex; flex-direction: column; gap: 4px; max-height: 480px; overflow-y: auto; padding-right: 4px; }
.application-body .book-checklist-item { display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 8px 14px; background: #fff; border: 1px solid #edf0f5; border-radius: 6px; cursor: pointer; transition: background 0.12s ease, border-color 0.12s ease; }
.application-body .book-checklist-item:hover { background: #f7f9fd; border-color: #e4e7ed; }
.application-body .book-checklist-item:has(.book-checkbox:checked) { background: #f1f5fe; border-color: #cfd5fa; }
.application-body .book-checkbox { width: 18px; height: 18px; margin: 0; accent-color: #3d5df6; cursor: pointer; }
.application-body .book-checklist-label { display: flex; align-items: center; gap: 8px; flex: 1; }
.application-body .book-checklist-label strong { color: #050841; font-size: 14px; font-weight: 500; }
.application-body .book-edit-actions { display: flex; align-items: center; gap: 16px; padding: 12px 0; }
.application-body .book-save-btn { min-height: 40px; padding: 0 24px; background: #3d5df6; color: #fff; border: 0; border-radius: 100px; font-family: var(--meez-font-sans); font-size: 15px; font-weight: 500; cursor: pointer; box-shadow: 0 2px 6px rgba(61, 93, 246, 0.28); transition: background 0.15s ease; }
.application-body .book-save-btn:hover { background: #2b4be0; }
.application-body .book-cancel-link { color: #687086; font-size: 14px; font-weight: 500; text-decoration: none; padding: 8px 12px; transition: color 0.12s ease; }
.application-body .book-cancel-link:hover { color: #050841; }
.application-body .recipe-additional-editor { width:min(800px,calc(100% - 76px)); margin:36px 38px; padding-top:28px; border-top:1px solid #eeeef3; }.application-body .recipe-additional-editor > form { display:grid; gap:14px; }.application-body .recipe-additional-editor label { display:grid; gap:5px; }.application-body .recipe-additional-editor input,.application-body .recipe-additional-editor textarea,.application-body .recipe-additional-editor select { padding:9px; }.application-body .recipe-additional-editor fieldset { display:grid; grid-template-columns:120px 120px 1fr; gap:10px; border:1px solid #eeeef3; }.application-body .recipe-additional-editor button { justify-self:start; padding:9px 14px; color:#fff; background:#3d5df6; border:0; }
.application-body .nutrition-ingredient-list { margin-top:16px; border-top:1px solid #eeeef3; }
.application-body .nutrition-list-heading { display:flex; justify-content:space-between; gap:20px; padding:14px 4px 10px; color:#202962; }
.application-body .nutrition-list-heading small { color:#a5a9c1; }
.application-body .nutrition-ingredient-list > a { display:grid; grid-template-columns:24px minmax(0,1fr) auto 14px; align-items:center; gap:10px; min-height:58px; padding:7px 4px; border-bottom:1px solid #eeeef3; text-decoration:none; }
.application-body .nutrition-ingredient-list > a > span:nth-child(2) { min-width:0; }
.application-body .nutrition-ingredient-list strong,.application-body .nutrition-ingredient-list small { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.application-body .nutrition-ingredient-list strong { color:#050841; font-weight:500; }
.application-body .nutrition-ingredient-list small { margin-top:2px; color:#a5a9c1; font-size:12px; }
.application-body .nutrition-ingredient-list em { color:#8b93a7; font-size:12px; font-style:normal; }
.application-body .nutrition-ingredient-list b { color:#a5a9c1; font-size:18px; }
.application-body .recipe-derived-panel .nutrition-label,.application-body .recipe-edit-nutrition .nutrition-label { max-width:430px; }
+864
View File
@@ -0,0 +1,864 @@
/* Actions group on the far right: Delete (RemoveCircle) followed by Drag (6-dot) */
.application-body .unified-recipe-editor .row-actions-group {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 3px;
}
.application-body .unified-recipe-editor .remove-row-btn,
.application-body .unified-recipe-editor .drag-handle {
display: grid;
place-items: center;
width: 28px;
height: 28px;
color: #a5a9c1;
background: transparent;
border: 0;
border-radius: 50%;
padding: 0;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease, color 0.15s ease, background-color 0.15s ease, transform 0.12s ease;
}
.application-body .unified-recipe-editor .drag-handle {
cursor: grab;
}
.application-body .unified-recipe-editor .drag-handle:active {
cursor: grabbing;
}
.application-body .unified-recipe-editor .remove-row-btn svg,
.application-body .unified-recipe-editor .drag-handle svg {
display: block;
}
.application-body .unified-recipe-editor .editor-items tbody tr:hover .remove-row-btn,
.application-body .unified-recipe-editor .editor-items tbody tr:focus-within .remove-row-btn,
.application-body .unified-recipe-editor .editor-items tbody tr:hover .drag-handle,
.application-body .unified-recipe-editor .editor-items tbody tr:focus-within .drag-handle {
opacity: 1;
}
.application-body .unified-recipe-editor .remove-row-btn:hover {
color: #f63d48;
background-color: #fff1f2;
transform: scale(1.08);
}
.application-body .unified-recipe-editor .drag-handle:hover {
color: #050841;
background-color: #eef1f8;
transform: scale(1.08);
}
/* Row inputs: 15px font, borderless with Meez blue underline on hover & focus */
.application-body .unified-recipe-editor .editor-items .quantity-column input,
.application-body .unified-recipe-editor .editor-items .unit-column select,
.application-body .unified-recipe-editor .editor-items .entity-column input,
.application-body .unified-recipe-editor .editor-items .notes-column input,
.application-body .unified-recipe-editor .editor-items .percent-column input {
width: 100%;
min-height: 38px;
height: 38px;
padding: 4px 6px;
color: #050841;
background: transparent;
border: 0;
border-bottom: 1px solid transparent;
border-radius: 0;
font-family: var(--meez-font-sans);
font-size: 15px;
font-weight: 400;
text-align: left;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
transition: border-bottom-color 0.12s ease;
}
.application-body .unified-recipe-editor .editor-items .unit-column select {
appearance: none;
-webkit-appearance: none;
background-image: none;
cursor: pointer;
}
.application-body .unified-recipe-editor .editor-items .notes-column input {
font-style: italic;
font-weight: 300;
color: #050841;
}
.application-body .unified-recipe-editor .editor-items .notes-column input::placeholder {
font-style: italic;
color: #a5a9c1;
opacity: 0.85;
}
/* Hover: crisp Meez blue underline */
.application-body .unified-recipe-editor .editor-items tbody tr input:hover,
.application-body .unified-recipe-editor .editor-items tbody tr select:hover {
background: transparent;
border: 0;
border-bottom: 1px solid #3d5df6;
}
/* Focus: thicker 2px blue underline */
.application-body .unified-recipe-editor .editor-items tbody tr input:focus,
.application-body .unified-recipe-editor .editor-items tbody tr select:focus {
background: transparent !important;
border: 0 !important;
border-bottom: 2px solid #3d5df6 !important;
outline: 0 !important;
box-shadow: none !important;
}
/* Sub-recipe styling in entity input */
.application-body .unified-recipe-editor .row-entity-combobox.is-subrecipe input {
color: #3d5df6;
font-weight: 500;
}
/* Autocomplete Combobox Dropdown */
.application-body .unified-recipe-editor .row-entity-combobox {
position: relative;
width: 100%;
}
.application-body .unified-recipe-editor .row-entity-results {
position: absolute;
z-index: 100;
top: calc(100% + 4px);
left: 0;
min-width: 300px;
width: max-content;
max-width: 440px;
max-height: 280px;
overflow-y: auto;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(5, 8, 41, 0.14);
padding: 6px;
}
.application-body .unified-recipe-editor .row-entity-results button {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
padding: 8px 12px;
background: transparent;
border: 0;
border-radius: 6px;
color: #050841;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
text-align: left;
cursor: pointer;
transition: background-color 0.1s ease, color 0.1s ease;
}
.application-body .unified-recipe-editor .row-entity-results button:hover,
.application-body .unified-recipe-editor .row-entity-results button:focus {
background: #f1f5fe;
color: #3d5df6;
outline: 0;
}
.application-body .unified-recipe-editor .row-entity-results .badge-recipe {
background: #f1f5fe;
color: #3d5df6;
font-size: 11px;
font-weight: 600;
padding: 2px 7px;
border-radius: 4px;
}
.application-body .unified-recipe-editor .row-entity-results .badge-ingredient {
background: #eaf8f4;
color: #23856d;
font-size: 11px;
font-weight: 600;
padding: 2px 7px;
border-radius: 4px;
}
.application-body .unified-recipe-editor .row-create-actions {
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid #f3f3f3;
}
.application-body .unified-recipe-editor .row-create-actions button {
color: #3d5df6;
font-weight: 600;
}
.application-body .unified-recipe-editor .remove-row-btn {
width:22px;
height:22px;
padding:0;
display:inline-grid;
place-items:center;
color:#a5a9c1;
background:transparent;
border:0;
border-radius:4px;
font-size:12px;
cursor:pointer;
opacity: 0;
transition: opacity 0.12s ease, color 0.12s ease, background-color 0.12s ease;
}
.application-body .unified-recipe-editor .editor-items tbody tr:hover .remove-row-btn,
.application-body .unified-recipe-editor .editor-items tbody tr:focus-within .remove-row-btn {
opacity: 1;
}
.application-body .unified-recipe-editor .remove-row-btn:hover {
color:#f63d48;
background:#fff1f2;
}
.application-body .unified-recipe-editor .add-line-row {
margin:6px 0 0;
}
.application-body .unified-recipe-editor .quick-add-search {
position:relative;
}
.application-body .unified-recipe-editor .quick-add-search input {
width:100%;
min-height:36px;
padding:6px 12px;
color:#050841;
background:#fff;
border:1px dashed #cfd5fa;
border-radius:6px;
font-family:var(--meez-font-sans);
font-size:13px;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.application-body .unified-recipe-editor .quick-add-search input::placeholder {
color: #8283a0;
}
.application-body .unified-recipe-editor .quick-add-search input:focus {
border-style:solid;
border-color:#3d5df6;
outline:0;
box-shadow:0 0 0 2px rgba(61,93,246,0.12);
}
.application-body .unified-recipe-editor .recipe-item-results {
position: absolute;
z-index: 100;
top: calc(100% + 4px);
left: 0;
min-width: 300px;
width: 100%;
max-height: 280px;
overflow-y: auto;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(5, 8, 41, 0.14);
padding: 6px;
}
.application-body .unified-recipe-editor .recipe-item-results button {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 8px 12px;
background: transparent;
border: 0;
border-radius: 6px;
color: #050841;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
text-align: left;
cursor: pointer;
}
.application-body .unified-recipe-editor .recipe-item-results button:hover {
background: #f1f5fe;
color: #3d5df6;
}
.application-body .unified-recipe-editor .recipe-item-results small {
color: #8283a0;
font-size: 11px;
}
.application-body .unified-recipe-editor .recipe-item-results .create-ingredient-result {
color: #3d5df6;
border-top: 1px solid #f3f3f3;
margin-top: 4px;
padding-top: 8px;
}
/* Method editor: modern compact step cards matching Meez */
.application-body .method-editor {
min-width: 0;
padding: 0;
border-left: 0;
}
.application-body .method-header-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.application-body .method-header-row h2 {
margin: 0;
font-family: var(--meez-font-sans);
font-size: 20px;
font-weight: 500;
color: #050841;
}
.application-body .method-header-row h2 small {
font-size: 14px;
font-weight: 400;
color: #8b93a7;
margin-left: 6px;
}
.application-body .bulk-prep-btn {
min-height: 28px;
padding: 4px 12px;
color: #3d5df6;
background: #fff;
border: 1px solid #3d5df6;
border-radius: 100px;
font-family: var(--meez-font-sans);
font-size: 12px;
font-weight: 500;
cursor: pointer;
}
.application-body .bulk-prep-btn:hover {
background: #f1f5fe;
}
.application-body .method-steps-list {
display: flex;
flex-direction: column;
gap: 6px;
margin: 0;
padding: 0;
list-style: none;
}
.application-body .method-step-card {
display: grid;
grid-template-columns: 24px minmax(0, 1fr) 52px;
gap: 6px;
align-items: start;
min-height: 36px;
padding: 4px 8px;
background: #fff;
border: 1px solid #edf0f5;
border-radius: 4px;
transition: border-color 0.12s ease, background-color 0.12s ease;
}
.application-body .method-step-card:hover {
border-color: #dfe3ec;
background-color: #fafbfe;
}
.application-body .method-step-card:focus-within {
border-color: #dfe3ec;
background-color: #fafbfe;
}
.application-body .step-badge {
display: grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: #f1f5fe;
color: #3d5df6;
font-size: 11px;
font-weight: 700;
margin-top: 2px;
}
.application-body .step-body {
min-width: 0;
}
.application-body .method-step-card .step-num {
font-size: 14px;
font-weight: 700;
color: #050841;
padding-top: 4px;
}
.application-body .method-step-card .step-textarea {
width: 100%;
min-height: 28px;
padding: 3px 0 3px;
border: 0;
border-bottom: 1px solid transparent;
border-radius: 0;
outline: 0;
background: transparent;
font-family: var(--meez-font-sans);
font-size: 14px;
line-height: 20px;
color: #050841;
resize: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
transition: border-bottom-color 0.12s ease;
}
.application-body .method-step-card .step-textarea:hover {
border-bottom: 1px solid #3d5df6;
}
.application-body .method-step-card .step-textarea:focus {
border-bottom: 2px solid #3d5df6;
}
.application-body .method-step-card.prep-heading .step-textarea {
font-weight: 700;
}
.application-body .method-step-card.prep-note .step-textarea {
font-style: italic;
font-weight: 300;
color: #4d5481;
}
.application-body .method-step-card .step-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 2px;
}
.application-body .method-step-card .remove-step-btn,
.application-body .method-step-card .drag-handle {
display: grid;
place-items: center;
width: 24px;
height: 24px;
color: #a5a9c1;
background: transparent;
border: 0;
border-radius: 50%;
padding: 0;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease, color 0.15s ease, background-color 0.15s ease, transform 0.12s ease;
}
.application-body .method-step-card .drag-handle {
cursor: grab;
}
.application-body .method-step-card .drag-handle:active {
cursor: grabbing;
}
.application-body .method-step-card .remove-step-btn svg,
.application-body .method-step-card .drag-handle svg {
display: block;
}
.application-body .method-step-card:hover .remove-step-btn,
.application-body .method-step-card:focus-within .remove-step-btn,
.application-body .method-step-card:hover .drag-handle,
.application-body .method-step-card:focus-within .drag-handle {
opacity: 1;
}
.application-body .method-step-card .remove-step-btn:hover {
color: #f63d48;
background-color: #fff1f2;
transform: scale(1.08);
}
.application-body .method-step-card .drag-handle:hover {
color: #050841;
background-color: #eef1f8;
transform: scale(1.08);
}
/* Prep Method View Mode - Compact Meez Parity */
.application-body .recipe-view-method h2 {
margin: 0 0 16px 0 !important;
font-family: var(--meez-font-sans) !important;
font-size: 20px !important;
font-weight: 500 !important;
color: #050841 !important;
display: flex !important;
align-items: center !important;
gap: 6px !important;
}
.application-body .recipe-view-method h2 small {
font-size: 14px !important;
font-weight: 400 !important;
color: #8b93a7 !important;
}
.application-body .recipe-view-method .method-steps-list {
display: flex !important;
flex-direction: column !important;
gap: 10px !important;
margin: 0 !important;
padding: 0 !important;
list-style: none !important;
}
.application-body .recipe-view-method .method-step-item {
display: flex !important;
align-items: baseline !important;
gap: 10px !important;
margin: 0 !important;
padding: 0 !important;
color: #050841 !important;
font-family: var(--meez-font-sans) !important;
font-size: 14px !important;
line-height: 1.45 !important;
}
.application-body .recipe-view-method .method-step-item strong {
min-width: 20px !important;
flex-shrink: 0 !important;
font-family: var(--meez-font-sans) !important;
font-size: 14px !important;
font-weight: 700 !important;
color: #050841 !important;
line-height: 1.45 !important;
}
.application-body .recipe-view-method .method-step-item span {
flex: 1 !important;
font-family: var(--meez-font-sans) !important;
font-size: 14px !important;
font-weight: 400 !important;
color: #050841 !important;
line-height: 1.45 !important;
}
.application-body .recipe-view-method .method-step-item.prep-heading {
margin-top: 14px !important;
margin-bottom: 2px !important;
}
.application-body .recipe-view-method .method-heading-text {
margin: 0 !important;
font-family: var(--meez-font-sans) !important;
font-size: 14px !important;
font-weight: 700 !important;
color: #050841 !important;
line-height: 1.45 !important;
}
.application-body .recipe-view-method .method-step-item.prep-note {
margin: 2px 0 6px 0 !important;
}
.application-body .recipe-view-method .method-note-text {
margin: 0 !important;
font-family: var(--meez-font-sans) !important;
font-size: 13px !important;
font-style: italic !important;
color: #737c91 !important;
line-height: 1.4 !important;
}
.application-body .method-footer-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 18px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f3f3f3;
}
/* Smart Quick Add Bar */
.application-body .unified-recipe-editor .quick-add-bar {
display: grid;
grid-template-columns: 64% 36%;
gap: 10px;
margin: 10px 0 16px;
}
.application-body .unified-recipe-editor .quick-add-search,
.application-body .unified-recipe-editor .quick-add-notes {
position: relative;
}
.application-body .unified-recipe-editor .quick-add-bar input {
width: 100%;
height: 42px;
padding: 6px 12px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 15px;
font-weight: 400;
color: #050841;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.application-body .unified-recipe-editor .quick-add-bar input::placeholder {
color: #a5a9c1;
}
.application-body .unified-recipe-editor .quick-add-bar input:focus {
border-color: #3d5df6;
outline: 0;
box-shadow: 0 0 0 2.5px rgba(61, 93, 246, 0.18);
}
/* Bottom Bar & Actions */
.application-body .unified-recipe-editor .structure-bottom-bar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
min-height: 48px;
margin: 16px 0 28px;
padding: 4px 0;
}
.application-body .editor-keyboard-hint {
display: flex;
align-items: center;
gap: 14px;
color: #a5a9c1;
font-size: 13px;
font-weight: 300;
}
.application-body .editor-keyboard-hint kbd {
display: inline-block;
padding: 2px 6px;
background: #f1f3f8;
border-radius: 4px;
color: #4d5481;
font-family: inherit;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.application-body .unified-recipe-editor .structure-add-actions {
display: flex;
align-items: center;
gap: 18px;
}
.application-body .action-link-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
color: #3d5df6;
background: transparent;
border: 0;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: color 0.12s ease;
}
.application-body .action-link-btn svg {
display: block;
}
.application-body .action-link-btn:hover {
color: #202962;
text-decoration: underline;
}
.application-body .bulk-add-trigger,
.application-body .bulk-prep-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 36px;
height: 36px;
padding: 0 18px;
color: #050841;
background: #ffffff;
border: 1px solid #050841;
border-radius: 999px;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: all 0.12s ease;
}
.application-body .bulk-add-trigger:hover,
.application-body .bulk-prep-btn:hover {
color: #3d5df6;
border-color: #3d5df6;
background: #f1f5fe;
text-decoration: none;
}
.application-body .unified-recipe-editor .add-line {
display:grid;
grid-template-columns:minmax(0,2fr) minmax(130px,1fr);
gap:0;
}
.application-body .unified-recipe-editor .add-line-notes {
width:100%;
min-height:58px;
padding:12px 14px;
color:#050841;
background:#fff;
border:1px solid #f3f3f3;
border-left:0;
border-radius:0;
font:italic 14px var(--sans);
}
.application-body .unified-recipe-editor .add-line-notes::placeholder { color:#a5a9c1; }
.bulk-ingredient-backdrop {
position:fixed;
z-index:1000;
inset:0;
display:grid;
place-items:center;
padding:24px;
background:rgba(5,8,31,.52);
}
.bulk-ingredient-dialog {
position:relative;
display:flex;
width:min(780px,100%);
max-height:calc(100vh - 48px);
padding:52px 68px 48px;
overflow:auto;
flex-direction:column;
color:#050841;
background:#fff;
border-radius:10px;
box-shadow:0 24px 70px rgba(5,8,31,.28);
}
.bulk-ingredient-dialog h2 { margin:0 0 24px; font-size:30px; font-weight:700; }
.bulk-ingredient-dialog > p:not(.bulk-dialog-error) { margin:0 0 8px; color:#8b8d99; font-size:15px; font-weight:500; }
.bulk-ingredient-dialog textarea {
width:100%;
min-height:330px;
padding:18px 14px;
resize:vertical;
color:#050841;
background:#fff;
border:1px solid #eeeef3;
border-bottom:1px solid #647df8;
border-radius:0;
font:15px/1.55 var(--sans);
}
.bulk-ingredient-dialog textarea::placeholder { color:#a5a9c1; opacity:1; }
.bulk-dialog-close {
position:absolute;
top:28px;
right:36px;
width:38px;
height:38px;
padding:0;
color:#050841;
background:transparent;
border:0;
cursor:pointer;
font-size:34px;
font-weight:300;
line-height:1;
}
.bulk-dialog-error { margin:7px 0 0; color:#ef3d4d; font-size:13px; font-weight:600; }
.bulk-entry-help { display:flex; margin:18px 0 34px; flex-direction:column; gap:12px; font-size:14px; }
.bulk-entry-help small { color:#a5a9c1; font-size:inherit; }
.bulk-ingredient-dialog footer { display:flex; align-items:center; justify-content:flex-end; gap:44px; }
.bulk-ingredient-dialog footer button { min-height:48px; padding:10px 24px; cursor:pointer; border:0; font-weight:700; }
.bulk-ingredient-dialog .bulk-cancel { color:#3d5df6; background:transparent; }
.bulk-ingredient-dialog .bulk-submit { min-width:250px; color:#fff; background:#647df8; border-radius:28px; }
.bulk-ingredient-dialog .bulk-submit:disabled { background:#aeb9f8; cursor:not-allowed; }
.bulk-prep-dialog { width:min(860px,100%); }
.bulk-prep-dialog textarea { min-height:390px; }
.application-body .unified-recipe-editor .method-editor li.prep-heading { min-height:70px; padding-block:18px; background:#ffffff; }
.application-body .unified-recipe-editor .method-editor li.prep-heading textarea { min-height:34px; font-weight:700; }
.application-body .unified-recipe-editor .method-editor li.prep-note { min-height:76px; background:#ffffff; }
.application-body .unified-recipe-editor .method-editor li.prep-note textarea { min-height:44px; color:#727a91; font-style:italic; }/* Recipe edit title and breadcrumb refinements. */
.application-body .editable-entity-title {
margin: 0;
padding: 0 0 4px;
background: transparent;
border: 0;
border-bottom: 1px solid transparent;
border-radius: 0;
color: #050841;
font-family: var(--meez-font-sans);
font-size: 28px;
font-weight: 900;
line-height: 38px;
width: 100%;
}
.application-body .editable-entity-title:focus {
outline: 0;
border-bottom-color: #3d5df6;
}
/* Persistent detail breadcrumbs. */
.application-body .immersive-main .detail-utility { position:absolute; z-index:30; top:0; left:0; right:0; width:100%; height:48px; min-height:48px; display:grid; grid-template-columns:50% 50%; gap:0; padding:0; background:#ffffff; border-bottom:1px solid #edf0f5; }
.application-body .detail-context,.application-body .detail-tools { display:flex; align-items:center; min-width:0; height:48px; }
.application-body .detail-context { gap:14px; padding:0 36px; }
.application-body .detail-tools { gap:18px; padding:0 32px; border-left:1px solid #edf0f5; }
.application-body .detail-tools form { flex:1; width:auto; }
.application-body .detail-back { display:grid; place-items:center; width:30px; height:30px; color:#202962; text-decoration:none; font-size:30px; font-weight:400; line-height:1; }
.application-body .detail-back:hover { color:#3d5df6; }
.application-body .detail-breadcrumbs { display:flex; align-items:center; min-width:0; gap:0; margin:0; padding:0; list-style:none; }
.application-body .detail-breadcrumbs li { display:flex; align-items:center; min-width:0; }
.application-body .detail-breadcrumbs li+li::before { content:"/"; margin:0 8px; color:#a5a9c1; }
.application-body .detail-breadcrumbs a { overflow:hidden; color:#202962; text-decoration:none; text-overflow:ellipsis; white-space:nowrap; font-size:12px; font-weight:400; }
.application-body .detail-breadcrumbs a:hover { color:#3d5df6; text-decoration:underline; }
.application-body .entity-breadcrumb { display:none; }
.application-body .recipe-detail-shell > .entity-detail-header > div:first-child > p { display:none; }
.application-body .tag-chip-editor { min-height:42px; padding:6px 10px; background:#fff; border:1px solid #dfe3ec; border-radius:4px; }
.application-body .tag-chip-list { display:flex; flex-wrap:wrap; gap:6px; }
.application-body .tag-chip-list span { display:inline-flex; align-items:center; gap:5px; min-height:26px; padding:2px 7px 2px 10px; color:#202962; background:#f1f5fe; border-radius:16px; font-size:12px; }
.application-body .tag-chip-list b { font-weight:500; }
.application-body .tag-chip-list button { display:grid; place-items:center; width:18px; height:18px; padding:0; color:#7d86a0; background:transparent; border:0; border-radius:50%; cursor:pointer; font-size:14px; }
.application-body .tag-chip-list button:hover { color:#050841; background:#e1e7fa; }
.application-body input.tag-chip-entry { width:100%; min-height:28px !important; padding:2px !important; background:transparent !important; border:0 !important; outline:0; }
.application-body .recipe-additional-editor {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #f3f3f3;
}
.application-body .recipe-additional-editor h2 {
margin: 0 0 20px;
color: #050841;
font-size: 18px;
font-weight: 700;
}
.application-body .recipe-additional-editor > form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px 24px;
}
.application-body .recipe-additional-editor fieldset {
grid-template-columns: 1fr 1fr;
padding: 0;
border: 0;
}
.application-body .recipe-additional-editor fieldset input[name="storage_condition"] {
grid-column: 1/-1;
}
.application-body .recipe-additional-editor input,
.application-body .recipe-additional-editor select {
min-height: 40px;
padding: 6px 10px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 14px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.application-body .recipe-additional-editor input:hover,
.application-body .recipe-additional-editor select:hover {
border-color: #3d5df6;
}
.application-body .recipe-additional-editor input:focus,
.application-body .recipe-additional-editor select:focus {
border-color: #3d5df6;
outline: 0;
box-shadow: 0 0 0 2.5px rgba(61, 93, 246, 0.18);
}
.application-body .recipe-additional-editor label {
display: flex;
flex-direction: column;
gap: 6px;
}
.application-body .recipe-additional-editor label span {
font-size: 13px;
font-weight: 500;
color: #4d5481;
}
.application-body .recipe-edit-tab-panel {
width: 100%;
padding: 16px 0;
}
.application-body .recipe-edit-tab-panel h2 {
margin: 0 0 16px;
color: #050841;
font-size: 20px;
font-weight: 700;
}
.application-body .recipe-edit-tab-panel .recipe-cost-ledger { width:100%; }
.application-body .recipe-edit-tab-panel .cost-ledger-lines { border-top:1px solid #eeeef3; }
.application-body .recipe-history { display:none; }
File diff suppressed because it is too large Load Diff
+124
View File
@@ -0,0 +1,124 @@
/* ==========================================================================
Subheader Tabs Navigation (Meez Parity)
========================================================================== */
.application-body .recipe-workspace-tabs,
.application-body .recipe-read-tabs,
.application-body .entity-tabs {
display: flex;
align-items: center;
height: 48px;
min-height: 48px;
max-height: 48px;
margin: 0;
padding: 0;
background: #ffffff;
border-bottom: 1px solid #edf0f5;
justify-content: flex-start;
box-sizing: border-box;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.application-body .recipe-workspace-tabs::-webkit-scrollbar,
.application-body .recipe-read-tabs::-webkit-scrollbar,
.application-body .entity-tabs::-webkit-scrollbar {
display: none;
}
.application-body .recipe-workspace-tabs button,
.application-body .recipe-read-tabs button,
.application-body .entity-tabs button {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
height: 48px;
min-height: 48px;
padding: 0 14px 0 2px;
color: #050841;
background: transparent;
border: 0;
border-bottom: 2px solid transparent;
border-right: 1px solid #f3f3f3;
border-radius: 0;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 400;
line-height: 22px;
cursor: pointer;
white-space: nowrap;
box-sizing: border-box;
transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.application-body .recipe-workspace-tabs button:last-child,
.application-body .recipe-read-tabs button:last-child,
.application-body .entity-tabs button:last-child {
border-right: 0;
}
.application-body .recipe-workspace-tabs button:hover:not(.active),
.application-body .recipe-read-tabs button:hover:not(.active),
.application-body .entity-tabs button:hover:not(.active) {
background: #fbfbfb !important;
color: #050841 !important;
border-bottom: 2px solid #050841 !important;
}
.application-body .recipe-workspace-tabs button.active,
.application-body .recipe-read-tabs button.active,
.application-body .entity-tabs button.active {
color: #050841 !important;
background: #f1f5fe !important;
border-bottom: 2px solid #3d5df6 !important;
}
/* Tab Icon: Fixed dimensions, no layout shift across all states */
.application-body .recipe-tab-icon {
display: grid;
place-items: center;
width: 32px;
height: 48px;
margin-right: 2px;
color: #050841 !important;
flex: 0 0 auto;
box-sizing: border-box;
}
.application-body .recipe-tab-icon svg {
display: block;
width: 20px;
height: 20px;
color: inherit !important;
flex: 0 0 auto;
}
/* Responsive adjustment for 13" laptop screens (1280px / 1366px) */
@media (max-width: 1366px) and (min-width: 901px) {
.application-body .recipe-workspace-tabs button,
.application-body .recipe-read-tabs button,
.application-body .entity-tabs button {
padding: 0 10px 0 2px;
font-size: 13.5px;
}
.application-body .recipe-tab-icon {
width: 28px;
margin-right: 2px;
}
}
@media (max-width: 1100px) and (min-width: 901px) {
.application-body .recipe-workspace-tabs button,
.application-body .recipe-read-tabs button,
.application-body .entity-tabs button {
padding: 0 8px 0 0;
font-size: 13px;
}
.application-body .recipe-tab-icon {
width: 26px;
margin-right: 0;
}
}
+640
View File
@@ -0,0 +1,640 @@
/* ==========================================================================
Ingredient Detail (View & Edit) — Meez matching styles
========================================================================== */
.application-body .recipe-read-shell.ingredient-read-shell {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-areas: "utility utility" "left right";
width: 100%;
max-width: none;
min-height: auto;
margin: 0;
padding: 0;
background: #ffffff;
}
.application-body .ingredient-read-left {
grid-area: left;
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: 100%;
padding: 24px 36px 35px;
background: #fff;
border-right: 1px solid #edf0f5;
box-sizing: border-box;
}
.application-body .ingredient-read-left .entity-detail-header {
padding: 0 0 20px;
min-height: auto;
border-bottom: 0;
background: transparent;
}
.application-body .ingredient-read-left .entity-breadcrumb {
margin: 0 0 12px;
}
.application-body .ingredient-read-left .entity-breadcrumb a {
color: #202962;
font-size: 12px;
font-weight: 400;
text-decoration: none;
}
.application-body .ingredient-read-left .entity-detail-header h1 {
font-family: var(--meez-font-sans);
font-size: 28px;
font-weight: 700;
line-height: 38px;
color: #050841;
margin: 0;
}
.application-body .ingredient-title-editor {
display: block;
width: 100%;
min-height: 40px;
padding: 0 0 4px;
background: transparent;
border: 0;
border-bottom: 1px solid transparent;
outline: 0;
font-family: var(--meez-font-sans);
font-size: 28px;
font-weight: 700;
line-height: 38px;
color: #050841;
resize: none;
box-sizing: border-box;
transition: border-color 0.12s ease;
}
.application-body .ingredient-title-editor:hover {
border-bottom: 1px solid #3d5df6;
}
.application-body .ingredient-title-editor:focus {
border-bottom: 2px solid #3d5df6;
}
.application-body .ingredient-read-left .entity-section h2 {
font-family: var(--meez-font-sans);
font-size: 22px;
font-weight: 700;
line-height: 33px;
color: #050841;
margin: 0 0 6px;
}
.application-body .ingredient-read-left .panel-intro {
font-family: var(--meez-font-sans);
font-size: 13px;
line-height: 1.5;
color: #8283a0;
margin: 0 0 16px;
}
.application-body .ingredient-read-left .field-help {
font-family: var(--meez-font-sans);
font-size: 12px;
line-height: 1.5;
color: #8283a0;
margin: 12px 0 0;
}
.application-body .ingredient-read-left .outlined-add-action {
display: inline-flex;
align-items: center;
height: 36px;
padding: 0 18px;
color: #050841;
background: #fff;
border: 1px solid #050841;
border-radius: 18px;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin: 0 0 20px;
transition: border-color 0.12s ease, color 0.12s ease, background-color 0.12s ease;
}
.application-body .ingredient-read-left .outlined-add-action:hover {
color: #3d5df6;
border-color: #3d5df6;
background-color: #f1f5fe;
}
.application-body .ingredient-read-left .prep-action-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
margin-bottom: 20px;
}
.application-body .ingredient-read-left .prep-action-table th {
height: 38px;
padding: 8px 10px;
color: #8283a0;
background: #fff;
border: 0;
border-bottom: 1px solid #edf0f5;
font-family: var(--meez-font-sans);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
text-align: left;
}
.application-body .ingredient-read-left .prep-action-table td {
height: 48px;
padding: 6px 10px;
color: #050841;
background: #fff;
border: 0;
border-bottom: 1px solid #edf0f5;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 400;
vertical-align: middle;
}
.application-body .ingredient-read-left .prep-action-table tr:hover td {
background-color: #fafbfe;
}
.application-body .ingredient-read-left .prep-action-table td select,
.application-body .ingredient-read-left .prep-action-table td input {
width: 100%;
height: 32px;
padding: 0 4px;
background: transparent;
border: 0;
border-bottom: 1px solid transparent;
outline: 0;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 400;
color: #050841;
transition: border-color 0.12s ease;
}
.application-body .ingredient-read-left .prep-action-table td select:hover,
.application-body .ingredient-read-left .prep-action-table td input:hover {
border-bottom: 1px solid #3d5df6;
}
.application-body .ingredient-read-left .prep-action-table td select:focus,
.application-body .ingredient-read-left .prep-action-table td input:focus {
border-bottom: 2px solid #3d5df6;
}
.application-body .ingredient-read-left .prep-equivalency-link {
display: inline-flex;
align-items: center;
padding: 4px 8px;
color: #3d5df6;
background: transparent;
border: 0;
font-family: var(--meez-font-sans);
font-size: 13px;
font-weight: 500;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.12s ease;
}
.application-body .ingredient-read-left .prep-equivalency-link:hover {
background-color: #eef2fe;
}
.application-body .ingredient-read-left .prep-row-remove {
display: none;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
color: #95969c;
background: transparent;
border: 0;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
line-height: 1;
}
.application-body .ingredient-read-left .prep-action-table tr:hover .prep-row-remove {
display: inline-flex;
}
.application-body .ingredient-read-left .prep-row-remove:hover {
color: #d32f2f;
background-color: #fee;
}
.application-body .ingredient-read-left .additional-card {
margin-top: 36px;
padding: 0;
background: transparent;
border: 0;
min-height: auto;
}
.application-body .ingredient-read-left .additional-card h2 {
font-family: var(--meez-font-sans);
font-size: 18px;
font-weight: 500;
color: #050841;
margin: 0 0 16px;
}
.application-body .ingredient-read-left .additional-card details {
border-bottom: 1px solid #edf0f5;
padding: 12px 0;
}
.application-body .ingredient-read-left .additional-card summary {
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
color: #050841;
cursor: pointer;
}
.application-body .ingredient-read-left .additional-card summary small {
color: #8283a0;
font-weight: 400;
margin-left: 6px;
}
.application-body .ingredient-read-left .ingredient-tag-view {
margin: 12px 0;
}
.application-body .ingredient-read-left .ingredient-tag-view i {
display: inline-block;
font-style: normal;
background: #f1f3f8;
color: #050841;
font-size: 13px;
padding: 4px 12px;
border-radius: 16px;
margin-right: 6px;
margin-bottom: 6px;
}
.application-body .ingredient-read-left .ingredient-description-view {
margin: 12px 0;
font-size: 14px;
line-height: 1.5;
color: #050841;
}
.application-body .ingredient-read-left .ingredient-additional-edit {
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 6px;
padding: 24px;
}
.application-body .ingredient-read-left .ingredient-detail-fields {
display: grid;
gap: 18px;
}
.application-body .ingredient-read-left .ingredient-detail-fields label {
display: grid;
gap: 6px;
font-family: var(--meez-font-sans);
font-size: 13px;
font-weight: 500;
color: #050841;
}
.application-body .ingredient-read-left .ingredient-detail-fields input,
.application-body .ingredient-read-left .ingredient-detail-fields textarea {
width: 100%;
padding: 10px 12px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 14px;
color: #050841;
box-sizing: border-box;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.application-body .ingredient-read-left .ingredient-detail-fields input:focus,
.application-body .ingredient-read-left .ingredient-detail-fields textarea:focus {
border-color: #3d5df6;
box-shadow: 0 0 0 2px rgba(61, 93, 246, 0.15);
outline: 0;
}
/* Right pane */
.application-body .ingredient-read-right {
grid-area: right;
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: 100%;
background: #ffffff;
box-sizing: border-box;
}
.application-body .ingredient-read-right .ingredient-view-details {
padding: 24px 36px 35px;
background: transparent;
border-left: 0;
box-sizing: border-box;
}
.application-body .ingredient-read-right .ingredient-view-details h2 {
font-family: var(--meez-font-sans);
font-size: 22px;
font-weight: 700;
line-height: 33px;
color: #050841;
margin: 0 0 6px;
}
.application-body .ingredient-read-right .ingredient-view-details .panel-intro {
font-family: var(--meez-font-sans);
font-size: 13px;
line-height: 1.5;
color: #8283a0;
margin: 0 0 20px;
}
.application-body .ingredient-read-right .add-purchase summary,
.application-body .ingredient-read-right .equivalency-editor summary {
display: inline-flex;
align-items: center;
height: 36px;
padding: 0 18px;
color: #050841;
background: #fff;
border: 1px solid #050841;
border-radius: 18px;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin: 0 0 20px;
list-style: none;
transition: border-color 0.12s ease, color 0.12s ease, background-color 0.12s ease;
}
.application-body .ingredient-read-right .add-purchase summary:hover,
.application-body .ingredient-read-right .equivalency-editor summary:hover {
color: #3d5df6;
border-color: #3d5df6;
background-color: #f1f5fe;
}
.application-body .ingredient-read-right .ingredient-purchase-form {
margin-bottom: 24px;
}
.application-body .ingredient-read-right .purchase-item-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 6px;
padding: 24px;
}
.application-body .ingredient-read-right .purchase-item-form .purchase-name,
.application-body .ingredient-read-right .purchase-item-form .purchase-rate,
.application-body .ingredient-read-right .purchase-item-form .purchase-form-actions,
.application-body .ingredient-read-right .purchase-item-form .price-history {
grid-column: 1 / -1;
}
.application-body .ingredient-read-right .purchase-item-form label {
display: grid;
gap: 6px;
font-family: var(--meez-font-sans);
font-size: 11px;
font-weight: 700;
color: #8283a0;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.application-body .ingredient-read-right .purchase-item-form input,
.application-body .ingredient-read-right .purchase-item-form select {
height: 40px;
padding: 0 12px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 400;
color: #050841;
box-sizing: border-box;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.application-body .ingredient-read-right .purchase-item-form input:focus,
.application-body .ingredient-read-right .purchase-item-form select:focus {
border-color: #3d5df6;
box-shadow: 0 0 0 2px rgba(61, 93, 246, 0.15);
outline: 0;
}
.application-body .ingredient-read-right .purchase-form-actions button {
height: 40px;
padding: 0 24px;
background: #3d5df6;
color: #fff;
border: 0;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s ease;
}
.application-body .ingredient-read-right .purchase-form-actions button:hover {
background-color: #2b4be0;
}
.application-body .ingredient-read-right .ingredient-cost-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 6px;
margin-bottom: 12px;
}
.application-body .ingredient-read-right .ingredient-cost-row strong {
display: block;
font-size: 15px;
color: #050841;
}
.application-body .ingredient-read-right .ingredient-cost-row small {
color: #8283a0;
font-size: 13px;
}
.application-body .ingredient-read-right .equivalency-editor {
margin-top: 16px;
}
.application-body .ingredient-read-right .equivalency-editor .inline-form {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 12px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 6px;
padding: 20px;
margin-top: 12px;
}
.application-body .ingredient-read-right .equivalency-editor .inline-form label {
display: grid;
gap: 6px;
font-family: var(--meez-font-sans);
font-size: 11px;
font-weight: 700;
color: #8283a0;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.application-body .ingredient-read-right .equivalency-editor .inline-form input,
.application-body .ingredient-read-right .equivalency-editor .inline-form select {
height: 40px;
padding: 0 12px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 14px;
color: #050841;
box-sizing: border-box;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.application-body .ingredient-read-right .equivalency-editor .inline-form input:focus,
.application-body .ingredient-read-right .equivalency-editor .inline-form select:focus {
border-color: #3d5df6;
box-shadow: 0 0 0 2px rgba(61, 93, 246, 0.15);
outline: 0;
}
.application-body .ingredient-read-right .equivalency-editor .inline-form i {
display: inline-flex;
align-items: center;
justify-content: center;
height: 40px;
font-style: normal;
font-size: 18px;
font-weight: 700;
color: #8283a0;
padding: 0 4px;
}
.application-body .ingredient-read-right .equivalency-editor .inline-form button[name="intent"] {
height: 40px;
padding: 0 20px;
background: #3d5df6;
color: #fff;
border: 0;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s ease;
}
.application-body .ingredient-read-right .equivalency-editor .inline-form button[name="intent"]:hover {
background-color: #2b4be0;
}
.application-body .ingredient-read-right .equivalency-editor .inline-form .cancel-equivalency {
height: 40px;
padding: 0 16px;
background: transparent;
color: #8283a0;
border: 1px solid #dfe3ec;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 14px;
cursor: pointer;
}
.application-body .ingredient-read-right .usda-id-form {
display: flex;
align-items: flex-end;
gap: 12px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 6px;
padding: 20px;
margin-bottom: 20px;
}
.application-body .ingredient-read-right .usda-id-form label {
display: grid;
gap: 6px;
font-family: var(--meez-font-sans);
font-size: 11px;
font-weight: 700;
color: #8283a0;
text-transform: uppercase;
letter-spacing: 0.04em;
flex: 1;
}
.application-body .ingredient-read-right .usda-id-form input {
height: 40px;
padding: 0 12px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 14px;
color: #050841;
box-sizing: border-box;
}
.application-body .ingredient-read-right .usda-id-form button {
height: 40px;
padding: 0 20px;
background: #3d5df6;
color: #fff;
border: 0;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s ease;
}
.application-body .ingredient-read-right .usda-id-form button:hover {
background-color: #2b4be0;
}
+15 -4630
View File
File diff suppressed because it is too large Load Diff
+353
View File
@@ -0,0 +1,353 @@
@media print {
body.application-body * { visibility:hidden !important; }
body.application-body .nutrition-label,body.application-body .nutrition-label * { visibility:visible !important; }
body.application-body .nutrition-label { position:absolute; top:0; left:0; width:360px; max-width:none !important; border:1px solid #000; }
@page { margin:.35in; }
}
.application-body .recipe-overview-strip {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 24px;
padding: 0 0 16px 0;
background: transparent;
border-bottom: 1px solid #edf0f5;
}
.application-body .yield-title-label {
display: block;
font-size: 12px;
font-weight: 500;
color: #95969c;
margin-bottom: 4px;
}
.application-body .yield-inputs-row {
display: flex;
align-items: center;
gap: 8px;
}
.application-body .yield-inputs-row input[name="yield_quantity"] {
width: 76px;
height: 38px;
padding: 6px 10px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 15px;
font-weight: 500;
color: #050841;
text-align: left;
}
.application-body .yield-inputs-row select[name="yield_unit_id"] {
min-width: 60px;
height: 38px;
padding: 6px 10px;
background: #fff;
border: 1px solid #dfe3ec;
border-radius: 4px;
font-family: var(--meez-font-sans);
font-size: 15px;
font-weight: 400;
color: #050841;
cursor: pointer;
}
.application-body .inline-auto-yield {
display: flex;
align-items: center;
gap: 10px;
margin-top: 4px;
font-size: 14px;
color: #050841;
}
.application-body .inline-auto-yield .toggle-button {
position: relative;
width: 36px;
height: 20px;
padding: 0;
background: #cbd1dc;
border: 0;
border-radius: 20px;
cursor: pointer;
transition: background 0.15s ease;
}
.application-body .inline-auto-yield .toggle-button i {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
transition: transform 0.15s ease;
}
.application-body .inline-auto-yield.active .toggle-button { background: #40b49a; }
.application-body .inline-auto-yield.active .toggle-button i { transform: translateX(16px); }
.application-body .unified-recipe-editor { padding: 0; }
.application-body .unified-recipe-editor .structure-editor {
display: block;
border-top: 0;
}
.application-body .unified-recipe-editor .section-heading {
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 36px;
margin-bottom: 8px;
padding: 0;
}
.application-body .structure-heading-actions {
display: flex;
align-items: center;
gap: 12px;
}
.application-body .calculate-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--meez-font-sans);
font-size: 13px;
font-weight: 500;
color: #050841;
cursor: pointer;
user-select: none;
}
.application-body .calculate-toggle input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.application-body .calculate-toggle i {
display: inline-block;
width: 34px;
height: 18px;
background: #cbd1dc;
border-radius: 12px;
position: relative;
transition: background-color 0.15s ease;
}
.application-body .calculate-toggle i::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
background: #fff;
border-radius: 50%;
transition: transform 0.15s ease;
}
.application-body .calculate-toggle input:checked + i {
background: #40b49a;
}
.application-body .calculate-toggle input:checked + i::after {
transform: translateX(16px);
}
.application-body .percent-mode {
display: inline-flex;
align-items: center;
background: #ebedf2;
padding: 2px;
border-radius: 100px;
gap: 2px;
}
.application-body .percent-mode button {
height: 24px;
padding: 0 10px;
font-size: 12px;
font-weight: 500;
color: #4d5481;
background: transparent;
border: 0;
border-radius: 100px;
cursor: pointer;
transition: all 0.12s ease;
}
.application-body .percent-mode button.active {
background: #fff;
color: #050841;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.application-body .unified-recipe-editor .editor-items tr.component-header-row,
.application-body .unified-recipe-editor .editor-items tr.component-header-row:hover,
.application-body .unified-recipe-editor .editor-items tr.component-header-row:focus-within {
background: transparent !important;
}
.application-body .unified-recipe-editor .editor-items tr.component-header-row td {
padding: 18px 0 6px;
border-bottom: 0;
background: transparent;
}
.application-body .unified-recipe-editor .component-header-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 38px;
padding: 4px 0;
background: transparent;
border-bottom: 1px solid #edf0f5;
border-radius: 0;
transition: border-color 0.15s ease;
}
.application-body .unified-recipe-editor .component-header-content:hover,
.application-body .unified-recipe-editor .component-header-content:focus-within {
background: transparent;
border-bottom-color: #3d5df6;
}
.application-body .unified-recipe-editor .component-name-input {
width: 100%;
padding: 6px 0;
color: #050841;
background: transparent;
border: 0;
outline: 0;
font-family: var(--meez-font-sans);
font-size: 18px;
font-weight: 500;
line-height: 27px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.application-body .unified-recipe-editor .component-header-actions {
display: flex;
align-items: center;
gap: 4px;
}
.application-body .unified-recipe-editor .component-header-actions .remove-component-btn,
.application-body .unified-recipe-editor .component-header-actions .drag-handle {
display: grid;
place-items: center;
width: 26px;
height: 26px;
color: #a5a9c1;
background: transparent;
border: 0;
border-radius: 4px;
padding: 0;
cursor: pointer;
transition: color 0.12s ease;
}
.application-body .unified-recipe-editor .component-header-actions .drag-handle {
cursor: grab;
}
.application-body .unified-recipe-editor .component-header-actions .remove-component-btn:hover {
color: #f63d48;
}
.application-body .unified-recipe-editor .component-header-actions .drag-handle:hover {
color: #050841;
}
.application-body .unified-recipe-editor .table-wrap {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
border-top: 0;
overflow-x: auto !important;
}
.application-body .unified-recipe-editor .editor-items {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
border-collapse: collapse;
table-layout: auto;
}
.application-body .unified-recipe-editor .editor-items th {
height: 38px;
padding: 0 6px 6px;
color: #95969c;
font-family: var(--meez-font-sans);
font-size: 12px;
font-weight: 500;
letter-spacing: normal;
text-transform: none;
border-bottom: 1px solid #f3f3f3;
text-align: left;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.application-body .unified-recipe-editor .editor-items th.quantity-column,
.application-body .unified-recipe-editor .editor-items .quantity-column { text-align: left; width: 56px; min-width: 46px; }
.application-body .unified-recipe-editor .editor-items th.unit-column,
.application-body .unified-recipe-editor .editor-items .unit-column { text-align: left; width: 66px; min-width: 52px; }
.application-body .unified-recipe-editor .editor-items th.entity-column,
.application-body .unified-recipe-editor .editor-items .entity-column { text-align: left; min-width: 130px; }
.application-body .unified-recipe-editor .editor-items th.notes-column,
.application-body .unified-recipe-editor .editor-items .notes-column { text-align: left; width: 22%; min-width: 80px; }
.application-body .unified-recipe-editor .editor-items th.percent-column,
.application-body .unified-recipe-editor .editor-items .percent-column { text-align: left; width: 58px; min-width: 46px; }
.application-body .unified-recipe-editor .editor-items th.base-column,
.application-body .unified-recipe-editor .editor-items .base-column { text-align: center; width: 40px; min-width: 34px; }
.application-body .unified-recipe-editor .editor-items th.actions-column,
.application-body .unified-recipe-editor .editor-items .actions-column { width: 54px; min-width: 48px; text-align: right; }
.application-body .unified-recipe-editor .editor-items tbody tr {
height: 56px;
min-height: 56px;
transition: background-color 0.12s ease;
}
.application-body .unified-recipe-editor .editor-items tbody tr:hover,
.application-body .unified-recipe-editor .editor-items tbody tr:focus-within {
background-color: #f1f4fb;
}
.application-body .unified-recipe-editor .editor-items td {
height: 56px;
padding: 4px 4px;
border: 0;
border-bottom: 1px solid #f3f3f3;
vertical-align: middle;
}
/* Recipe read view — meez skin (measured live 2026-08-14). */
.application-body .immersive-main { background:#ffffff; }
.application-body .recipe-detail-shell.recipe-read-shell {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-areas: "utility utility" "left right";
width: 100%;
max-width: none;
min-height: auto;
margin: 0;
padding: 0;
background: #ffffff;
}
.application-body .recipe-read-left {
width: 100%;
max-width: 100%;
padding: 24px 36px 35px;
background: #fff;
border-right: 1px solid #edf0f5;
box-sizing: border-box;
}
.application-body .recipe-read-left .entity-detail-header {
padding: 0 0 16px;
min-height: auto;
border-bottom: 0;
background: transparent;
}
.application-body .recipe-read-left .recipe-overview-strip {
padding: 0 0 16px;
margin-bottom: 20px;
background: transparent;
border-bottom: 1px solid #edf0f5;
}
.application-body .recipe-read-left .recipe-structure-workspace {
padding: 0;
margin: 0;
border-top: 0;
}
.application-body .recipe-read-left .structure-editor {
padding: 0;
margin: 0;
border: 0;
}
.application-body .recipe-read-right {
width: 100%;
max-width: 100%;
background: #ffffff;
box-sizing: border-box;
}
.application-body .recipe-detail-shell .detail-actions-menu button { width:100% !important; max-width:100% !important; min-height:44px; padding:10px 20px; color:#050841; font-size:15px; font-weight:500; }
+806
View File
@@ -0,0 +1,806 @@
@media (min-width: 982px) {
.application-body .immersive-main .recipe-read-shell .recipe-workspace-tabs button.mobile-only-tab,
.application-body .recipe-read-shell .recipe-workspace-tabs button.mobile-only-tab,
.application-body .recipe-workspace-tabs button.mobile-only-tab,
.application-body .entity-tabs button.mobile-only-tab,
.application-body .mobile-only-tab {
display: none !important;
}
.application-body .recipe-view-formula-pane.recipe-tab-panel {
display: block !important;
}
.application-body .ingredient-main-content.entity-tab-panel {
display: block !important;
}
}
@media (max-width: 981px) {
.application-body .immersive-main .recipe-read-shell .recipe-workspace-tabs button.mobile-only-tab,
.application-body .recipe-read-shell .recipe-workspace-tabs button.mobile-only-tab,
.application-body .recipe-workspace-tabs button.mobile-only-tab,
.application-body .entity-tabs button.mobile-only-tab,
.application-body .mobile-only-tab {
display: flex !important;
}
.application-body .recipe-detail-shell.recipe-read-shell,
.application-body .recipe-read-shell.ingredient-read-shell,
.application-body .recipe-read-shell,
.application-body .ingredient-read-shell {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
.application-body .recipe-read-left,
.application-body .recipe-read-right,
.application-body .ingredient-read-left,
.application-body .ingredient-read-right {
display: contents !important;
}
/* Consistent Top Utility Bar on Mobile */
.application-body .detail-utility {
order: 1 !important;
position: relative !important;
height: 52px !important;
min-height: 52px !important;
padding: 0 16px !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
width: 100% !important;
box-sizing: border-box !important;
border-bottom: 1px solid #e4e5ed !important;
background: #fff !important;
}
.application-body .detail-context {
height: 100% !important;
padding: 0 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
flex: 0 0 auto !important;
min-width: 0 !important;
}
.application-body .detail-back {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 28px !important;
height: 28px !important;
font-size: 22px !important;
color: #050841 !important;
text-decoration: none !important;
line-height: 1 !important;
}
.application-body .detail-breadcrumbs {
display: none !important;
}
.application-body .detail-tools {
display: flex !important;
align-items: center !important;
justify-content: flex-end !important;
gap: 8px !important;
height: 100% !important;
flex: 1 1 auto !important;
min-width: 0 !important;
padding: 0 !important;
border-left: 0 !important;
margin-left: auto !important;
}
.application-body .detail-search-form {
display: flex !important;
align-items: center !important;
flex: 1 1 auto !important;
max-width: 220px !important;
min-width: 80px !important;
height: 36px !important;
padding: 0 8px !important;
background: #fff !important;
border: 1px solid #dfe3ec !important;
border-radius: 4px !important;
margin: 0 !important;
}
.application-body .detail-search-form .search-field {
width: 100% !important;
min-width: 40px !important;
padding: 4px 0 !important;
font-size: 13px !important;
font-family: var(--meez-font-sans) !important;
border: 0 !important;
background: transparent !important;
outline: none !important;
}
.application-body .detail-new-menu {
flex: none !important;
position: relative !important;
}
.application-body .detail-new-menu > summary {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
min-height: 32px !important;
padding: 0 !important;
border-radius: 50% !important;
background: #3d5df6 !important;
color: #fff !important;
box-shadow: 0 1px 3px rgba(61, 93, 246, 0.3) !important;
list-style: none !important;
cursor: pointer !important;
}
.application-body .detail-new-menu > summary span:not(.new-trigger-plus) {
display: none !important;
}
.application-body .detail-new-menu > div {
position: fixed !important;
top: 56px !important;
right: 16px !important;
left: auto !important;
width: min(240px, calc(100vw - 32px)) !important;
z-index: 99999 !important;
box-shadow: 0 12px 36px rgba(5, 8, 65, 0.22) !important;
border: 1px solid #e4e5ed !important;
background: #fff !important;
border-radius: 6px !important;
}
.application-body .entity-header-actions {
display: flex !important;
align-items: center !important;
gap: 8px !important;
margin-left: auto !important;
}
.application-body .entity-header-actions .edit-command {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
padding: 6px 10px !important;
color: #3d5df6 !important;
font-size: 15px !important;
font-weight: 500 !important;
text-decoration: none !important;
}
.application-body .entity-header-actions .detail-actions-menu > summary {
width: 32px !important;
height: 32px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
}
/* Entity Detail Title Alignment (Aligned to 16px margin) */
.application-body .entity-detail-header,
.application-body .immersive-main .entity-detail-header,
.application-body .immersive-main .recipe-detail-shell .entity-detail-header,
.application-body .recipe-read-shell .entity-detail-header,
.application-body .ingredient-read-left .entity-detail-header {
order: 2 !important;
padding: 16px 16px 4px 16px !important;
margin: 0 !important;
width: 100% !important;
min-height: auto !important;
box-sizing: border-box !important;
border-right: 0 !important;
border-bottom: 0 !important;
display: block !important;
}
.application-body .entity-detail-header h1,
.application-body .recipe-read-shell .entity-detail-header h1,
.application-body .ingredient-read-left .entity-detail-header h1 {
margin: 0 !important;
padding: 0 !important;
font-family: var(--meez-font-sans) !important;
font-size: 24px !important;
line-height: 1.25 !important;
font-weight: 700 !important;
color: #050841 !important;
letter-spacing: -0.02em !important;
text-align: left !important;
}
.application-body .entity-detail-header p,
.application-body .recipe-read-shell .entity-detail-header p,
.application-body .ingredient-read-left .entity-detail-header p {
margin: 4px 0 0 0 !important;
padding: 0 !important;
color: #737c91 !important;
font-size: 14px !important;
font-weight: 400 !important;
text-align: left !important;
}
.application-body .recipe-title-editor,
.application-body .editable-entity-title,
.application-body .ingredient-title-editor {
font-family: var(--meez-font-sans) !important;
font-size: 24px !important;
font-weight: 700 !important;
line-height: 1.25 !important;
color: #050841 !important;
border: 1px solid #dfe3ec !important;
border-radius: 4px !important;
padding: 6px 10px !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
.application-body .recipe-overview-strip,
.application-body .entity-notice {
order: 3 !important;
margin-inline: 16px !important;
}
/* Segmented Mobile Tab Bar (Parity with Meez) */
.application-body .recipe-read-tabs,
.application-body .entity-tabs,
.application-body .recipe-workspace-tabs {
order: 4 !important;
display: grid !important;
grid-template-columns: repeat(5, 1fr) !important;
gap: 0 !important;
height: 48px !important;
min-height: 48px !important;
max-height: 48px !important;
margin: 12px 16px 16px !important;
padding: 0 !important;
width: calc(100% - 32px) !important;
background: #fff !important;
border: 1px solid #e4e5ed !important;
border-radius: 6px !important;
overflow: hidden !important;
box-sizing: border-box !important;
}
.application-body .entity-tabs {
grid-template-columns: repeat(4, 1fr) !important;
}
.application-body .recipe-read-tabs button,
.application-body .entity-tabs button,
.application-body .recipe-workspace-tabs button {
display: flex !important;
align-items: center !important;
justify-content: center !important;
height: 100% !important;
min-height: 46px !important;
min-width: 0 !important;
padding: 0 !important;
margin: 0 !important;
background: #fff !important;
color: #050841 !important;
border: 0 !important;
border-right: 1px solid #e4e5ed !important;
border-bottom: 2px solid transparent !important;
border-radius: 0 !important;
font-size: 0 !important;
cursor: pointer !important;
transition: background-color 0.12s ease, border-color 0.12s ease !important;
}
.application-body .recipe-read-tabs button:last-child,
.application-body .entity-tabs button:last-child,
.application-body .recipe-workspace-tabs button:last-child {
border-right: 0 !important;
}
.application-body .recipe-read-tabs .recipe-tab-label,
.application-body .entity-tabs .recipe-tab-label,
.application-body .recipe-workspace-tabs .recipe-tab-label {
display: none !important;
}
.application-body .recipe-read-tabs button .recipe-tab-icon,
.application-body .entity-tabs button .recipe-tab-icon,
.application-body .recipe-workspace-tabs button .recipe-tab-icon {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 22px !important;
height: 22px !important;
margin: 0 !important;
color: #050841 !important;
}
.application-body .recipe-read-tabs button .recipe-tab-icon svg,
.application-body .entity-tabs button .recipe-tab-icon svg,
.application-body .recipe-workspace-tabs button .recipe-tab-icon svg {
width: 20px !important;
height: 20px !important;
display: block !important;
}
.application-body .recipe-read-tabs button.active,
.application-body .entity-tabs button.active,
.application-body .recipe-workspace-tabs button.active {
background: #eaf1fe !important;
border-bottom: 2px solid #3d5df6 !important;
}
.application-body .recipe-read-tabs button.active .recipe-tab-icon,
.application-body .entity-tabs button.active .recipe-tab-icon,
.application-body .recipe-workspace-tabs button.active .recipe-tab-icon {
color: #050841 !important;
}
/* Mobile Content Panes */
.application-body .recipe-view-formula-pane,
.application-body .recipe-view-details-pane,
.application-body .ingredient-main-content,
.application-body .ingredient-view-details,
.application-body .recipe-structure-workspace {
order: 5 !important;
width: 100% !important;
box-sizing: border-box !important;
}
.application-body .recipe-additional-view {
order: 6 !important;
margin-top: 0 !important;
padding: 0 16px 32px !important;
}
.application-body .recipe-view-formula-pane.recipe-tab-panel,
.application-body .ingredient-main-content.entity-tab-panel,
.application-body .recipe-tab-panel,
.application-body .entity-tab-panel {
display: none !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 !important;
}
.application-body .recipe-view-formula,
.application-body .ingredient-main-content {
padding: 0 16px 24px !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.application-body .recipe-view-details,
.application-body .ingredient-view-details {
padding: 0 16px 24px !important;
border-left: 0 !important;
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
box-sizing: border-box !important;
}
.application-body .ingredient-view-details .workspace-panel,
.application-body .ingredient-main-content .entity-section {
padding: 0 !important;
margin: 0 0 24px 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* Edit Mode Mobile & Tablet Enhancements */
.application-body .unified-recipe-editor .table-wrap {
width: 100% !important;
max-width: 100% !important;
overflow-x: auto !important;
-webkit-overflow-scrolling: touch;
border-radius: 6px;
}
.application-body .prep-edit-wrap {
width: 100% !important;
max-width: 100% !important;
overflow-x: auto !important;
-webkit-overflow-scrolling: touch;
}
.application-body .recipe-additional-editor {
width: 100% !important;
max-width: 100% !important;
margin: 16px 0 0 !important;
padding: 16px 0 0 !important;
box-sizing: border-box !important;
}
.application-body .recipe-additional-editor fieldset {
display: grid !important;
grid-template-columns: 1fr 1fr !important;
gap: 8px !important;
}
.application-body .recipe-additional-editor fieldset input:last-child {
grid-column: 1 / -1 !important;
}
.application-body .recipe-derived-panel .nutrition-label,
.application-body .recipe-edit-nutrition .nutrition-label {
max-width: 100% !important;
width: 100% !important;
}
/* Mobile Home Search & Filter Popups */
.application-body .workspace-search-tools {
padding: 14px 16px 10px !important;
position: relative !important;
z-index: 100 !important;
}
.application-body .workspace-search-tools-inner {
width: 100% !important;
display: flex !important;
align-items: center !important;
flex-wrap: nowrap !important;
gap: 8px !important;
}
.application-body .workspace-global-search {
flex: 1 1 auto !important;
min-width: 0 !important;
height: 40px !important;
min-height: 40px !important;
padding: 0 10px !important;
background: #fff !important;
border: 1px solid #dfe3ec !important;
border-radius: 4px !important;
}
.application-body form.workspace-global-search input[type="search"] {
font-size: 14px !important;
font-family: var(--meez-font-sans) !important;
}
.application-body .search-type-filter {
flex: none !important;
position: relative !important;
}
.application-body .search-type-filter summary {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
height: 40px !important;
min-height: 40px !important;
padding: 0 6px !important;
font-size: 14px !important;
font-weight: 500 !important;
color: #050841 !important;
white-space: nowrap !important;
border-radius: 4px !important;
}
.application-body .workspace-new-menu {
flex: none !important;
position: relative !important;
}
.application-body .workspace-new-menu > summary {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 36px !important;
height: 36px !important;
min-height: 36px !important;
padding: 0 !important;
border-radius: 50% !important;
background: #3d5df6 !important;
color: #fff !important;
box-shadow: 0 1px 3px rgba(61, 93, 246, 0.3) !important;
}
.application-body .workspace-new-menu > summary .new-trigger-label {
display: none !important;
}
.application-body .workspace-new-menu > summary .new-trigger-plus {
width: 20px !important;
height: 20px !important;
font-size: 20px !important;
}
.application-body .workspace-new-menu > summary .new-trigger-plus svg {
width: 20px !important;
height: 20px !important;
}
/* Popups: Search Type Filter, Filter Menu, New Menu (Fixed on Top of Everything) */
.application-body .search-type-filter form {
position: fixed !important;
top: 66px !important;
right: 16px !important;
left: auto !important;
width: min(300px, calc(100vw - 32px)) !important;
max-width: calc(100vw - 32px) !important;
z-index: 99999 !important;
box-shadow: 0 12px 36px rgba(5, 8, 65, 0.22) !important;
border: 1px solid #e4e5ed !important;
background: #fff !important;
border-radius: 6px !important;
}
.application-body .workspace-new-menu > nav {
position: fixed !important;
top: 66px !important;
right: 16px !important;
left: auto !important;
width: min(300px, calc(100vw - 32px)) !important;
max-width: calc(100vw - 32px) !important;
z-index: 99999 !important;
box-shadow: 0 12px 36px rgba(5, 8, 65, 0.22) !important;
border: 1px solid #e4e5ed !important;
background: #fff !important;
border-radius: 6px !important;
}
.application-body .filter-menu form {
position: fixed !important;
top: 125px !important;
right: 16px !important;
left: auto !important;
width: min(320px, calc(100vw - 32px)) !important;
max-width: calc(100vw - 32px) !important;
z-index: 99999 !important;
box-shadow: 0 12px 36px rgba(5, 8, 65, 0.22) !important;
border: 1px solid #e4e5ed !important;
background: #fff !important;
border-radius: 6px !important;
}
/* Workspace Pills on Mobile: Smooth Horizontal Scrolling */
.application-body .directory-workspace {
width: calc(100% - 32px) !important;
margin-inline: auto !important;
padding-top: 0 !important;
}
.application-body .workspace-pills {
display: flex !important;
align-items: center !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
scrollbar-width: none !important;
gap: 8px !important;
padding: 0 0 4px !important;
margin: 0 !important;
width: 100% !important;
}
.application-body .workspace-pills::-webkit-scrollbar {
display: none !important;
}
.application-body .workspace-pills > a {
min-height: 36px !important;
padding: 6px 14px 6px 10px !important;
font-size: 14px !important;
flex: 0 0 auto !important;
white-space: nowrap !important;
}
.application-body .workspace-filter-system {
display: contents !important;
}
.application-body .workspace-filter-trigger-wrapper {
display: inline-flex !important;
align-items: center !important;
flex: 0 0 auto !important;
margin-left: 0 !important;
}
.application-body .workspace-filter-btn {
min-height: 36px !important;
padding: 6px 12px !important;
font-size: 14px !important;
white-space: nowrap !important;
flex: 0 0 auto !important;
}
.application-body .filter-rules-row {
display: flex !important;
align-items: center !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
scrollbar-width: none !important;
gap: 8px !important;
padding: 2px 0 8px !important;
width: 100% !important;
flex-basis: 100% !important;
}
.application-body .filter-rules-row::-webkit-scrollbar {
display: none !important;
}
.application-body .filter-rule-item-group {
flex: 0 0 auto !important;
}
/* Directory Table Mobile Enhancements */
.application-body .entity-directory-row,
.application-body .entity-directory-row.columns-1,
.application-body .entity-directory-row.columns-2,
.application-body .entity-directory-row.columns-3 {
display: grid !important;
grid-template-columns: 36px 44px minmax(0, 1fr) 36px !important;
gap: 8px !important;
min-height: 52px !important;
padding: 0 !important;
}
.application-body .entity-directory-row .entity-directory-name strong {
font-size: 15px !important;
font-weight: 500 !important;
color: #050841 !important;
}
/* Bulk Ingredient Dialog Mobile */
.bulk-ingredient-backdrop { padding: 0 !important; }
.bulk-ingredient-dialog { width: 100% !important; max-height: 100vh !important; min-height: 100vh !important; padding: 52px 22px 28px !important; border-radius: 0 !important; }
.bulk-ingredient-dialog h2 { font-size: 24px !important; }
.bulk-ingredient-dialog textarea { min-height: 260px !important; }
.bulk-dialog-close { top: 16px !important; right: 18px !important; }
.bulk-ingredient-dialog footer { gap: 12px !important; }
.bulk-ingredient-dialog .bulk-submit { min-width: 0 !important; flex: 1 !important; }
/* Recipe Books Mobile */
.application-body .recipe-book-workspace {
width: calc(100% - 32px) !important;
margin-inline: auto !important;
padding: 20px 0 40px !important;
}
.application-body .recipe-book-header {
flex-direction: column !important;
align-items: stretch !important;
gap: 16px !important;
margin-bottom: 20px !important;
}
.application-body .recipe-book-header-actions {
align-self: flex-start !important;
}
.application-body .book-recipe-row {
grid-template-columns: 32px minmax(0, 1fr) 28px !important;
padding: 0 14px !important;
min-height: 52px !important;
}
.application-body .book-directory-toolbar {
padding: 0 14px !important;
}
/* Archive Mobile */
.application-body .archive-workspace {
width: calc(100% - 32px) !important;
margin-inline: auto !important;
padding: 20px 0 40px !important;
}
.application-body .archive-table-head {
display: none !important;
}
.application-body .archive-table-row {
grid-template-columns: 36px minmax(0, 1fr) auto !important;
gap: 10px !important;
padding: 10px 14px !important;
min-height: 56px !important;
}
.application-body .archive-date-cell {
display: none !important;
}
/* Purchasing Review Mobile */
.application-body .purchasing-review-workspace {
width: calc(100% - 32px) !important;
margin-inline: auto !important;
padding: 20px 0 40px !important;
}
.application-body .purchasing-review-toolbar {
position: static !important;
flex-direction: column !important;
align-items: stretch !important;
gap: 14px !important;
padding: 14px !important;
}
.application-body .purchasing-review-actions {
flex-direction: column !important;
align-items: stretch !important;
gap: 10px !important;
}
.application-body .purchasing-search-box {
width: 100% !important;
}
.application-body .purchasing-search-box input {
min-width: 0 !important;
flex: 1 !important;
}
.application-body .purchasing-card {
padding: 16px !important;
}
.application-body .purchasing-card-header {
flex-direction: column !important;
align-items: flex-start !important;
gap: 10px !important;
}
.application-body .other-select-label {
flex-direction: column !important;
align-items: stretch !important;
gap: 6px !important;
}
/* Cost Ledger Mobile */
.application-body .cost-ledger-heading {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
padding: 8px 12px !important;
font-size: 12px !important;
}
.application-body .cost-ledger-heading .head-subject {
font-size: 12px !important;
}
.application-body .cost-ledger-heading button {
font-size: 12px !important;
}
.application-body .cost-ledger-line > summary {
grid-template-columns: 20px 26px minmax(0, 1fr) 20px 90px !important;
gap: 8px !important;
padding: 8px 12px !important;
min-height: 48px !important;
}
.application-body .cost-subject-name {
font-size: 14px !important;
}
.application-body .cost-line-value {
font-size: 13px !important;
}
.application-body .cost-line-detail {
grid-template-columns: 1fr !important;
gap: 12px !important;
padding: 12px 16px !important;
}
.application-body .cost-summary {
width: 100% !important;
margin-top: 24px !important;
}
/* Inventory Workspace Mobile */
.application-body .inventory-workspace {
width: calc(100% - 32px) !important;
margin-inline: auto !important;
padding: 16px 0 40px !important;
}
.application-body .inventory-header {
flex-direction: column !important;
align-items: flex-start !important;
gap: 14px !important;
}
.application-body .inventory-header-actions {
width: 100% !important;
}
.application-body .primary-count-btn {
width: 100% !important;
justify-content: center !important;
}
.application-body .count-sheet-header {
flex-direction: column !important;
align-items: stretch !important;
gap: 14px !important;
}
.application-body .count-sheet-header-right {
flex-wrap: wrap !important;
justify-content: space-between !important;
}
.application-body .location-tabs {
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
scrollbar-width: none !important;
padding-bottom: 6px !important;
}
.application-body .location-tabs::-webkit-scrollbar {
display: none !important;
}
.application-body .count-table-container {
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
}
.application-body .add-item-bar {
flex-direction: column !important;
align-items: stretch !important;
}
.application-body .quick-add-dropdown {
max-width: 100% !important;
width: 100% !important;
}
.application-body .quick-add-btn {
width: 100% !important;
justify-content: center !important;
}
/* Archive Floating Bar Mobile */
.application-body .archive-floating-bar {
width: calc(100% - 32px) !important;
max-width: 400px !important;
box-sizing: border-box !important;
flex-direction: column !important;
gap: 10px !important;
padding: 12px 16px !important;
bottom: 16px !important;
}
.application-body .floating-bar-actions {
width: 100% !important;
justify-content: space-between !important;
}
/* Formula Calculator Heading Mobile */
.application-body .recipe-view-formula .calculator-heading {
gap: 14px 20px !important;
margin-bottom: 18px !important;
}
.application-body .recipe-view-formula .calculator-percent-controls {
width: 100% !important;
justify-content: flex-start !important;
margin-top: 4px !important;
}
}