void;
- onClose: () => void;
- onSubmit: () => void;
-};
-
-export default function BulkIngredientImportModal({
- open,
- text,
- error,
- parsing,
- onTextChange,
- onClose,
- onSubmit,
-}: Props) {
- if (!open) return null;
-
- return (
- {
- if (event.currentTarget === event.target) onClose();
- }}
- >
-
-
- Add Ingredients
-
- Type or copy/paste ingredients from a document, spreadsheet, PDF, or
- website.
-
-
-
- );
-}
diff --git a/src/components/recipe-editor/BulkPrepStepsModal.tsx b/src/components/recipe-editor/BulkPrepStepsModal.tsx
index c51d7cd..7e9e907 100644
--- a/src/components/recipe-editor/BulkPrepStepsModal.tsx
+++ b/src/components/recipe-editor/BulkPrepStepsModal.tsx
@@ -19,14 +19,14 @@ export default function BulkPrepStepsModal({
return (
{
if (event.currentTarget === event.target) onClose();
}}
>
= {
- g: "gram", gram: "gram", grams: "gram", kg: "kilogram", kilogram: "kilogram", kilograms: "kilogram",
- oz: "ounce_mass", ounce: "ounce_mass", ounces: "ounce_mass", lb: "pound", lbs: "pound", pound: "pound", pounds: "pound",
- tsp: "teaspoon_us", teaspoon: "teaspoon_us", teaspoons: "teaspoon_us", tbsp: "tablespoon_us", tablespoon: "tablespoon_us", tablespoons: "tablespoon_us",
- c: "cup_us", cup: "cup_us", cups: "cup_us", ml: "milliliter", milliliter: "milliliter", milliliters: "milliliter",
- l: "liter", liter: "liter", liters: "liter", ea: "each", each: "each", clove: "each", cloves: "each",
-};
-
export function useRecipeStructure({
recipeId,
initial,
ingredients,
recipes,
- units,
autoYield = false,
weightRates = {},
}: RecipeEditorProps) {
@@ -33,10 +24,6 @@ export function useRecipeStructure({
const [rowQuery, setRowQuery] = useState>({});
const [focusedItem, setFocusedItem] = useState();
const [draftNotes, setDraftNotes] = useState>({});
- const [bulkOpen, setBulkOpen] = useState(false);
- const [bulkText, setBulkText] = useState("");
- const [bulkError, setBulkError] = useState("");
- const [bulkParsing, setBulkParsing] = useState(false);
const [prepOpen, setPrepOpen] = useState(false);
const [prepText, setPrepText] = useState("");
const [prepError, setPrepError] = useState("");
@@ -265,203 +252,6 @@ export function useRecipeStructure({
addLine(componentIndex, `recipe:${id}`);
};
- const identityTokens = (value: string) =>
- normal(value)
- .split(" ")
- .filter(Boolean)
- .map((token) =>
- token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token
- );
-
- const addBulkIngredients = async () => {
- if (!bulkText.trim()) {
- setBulkError("Enter at least one ingredient.");
- return;
- }
- setBulkParsing(true);
- setBulkError("");
- let parsed: {
- components: Array<{
- name: string;
- items: Array<{
- source_line: string;
- quantity: number | null;
- unit: string | null;
- ingredient: string;
- preparation: string | null;
- note: string | null;
- optional: boolean;
- alternatives: string[];
- }>;
- }>;
- warnings: string[];
- };
- try {
- const response = await fetch("/api/app/recipes/parse-ingredients", {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ text: bulkText }),
- });
- const result = await response.json();
- if (!response.ok)
- throw new Error(result.error ?? "Unable to parse ingredients.");
- parsed = result;
- } catch (error) {
- setBulkError(
- error instanceof Error ? error.message : "Unable to parse ingredients."
- );
- setBulkParsing(false);
- return;
- }
- const available = [
- ...[...ingredients, ...pendingIngredients].map((item) => ({
- ...item,
- kind: "ingredient" as const,
- })),
- ...recipes
- .filter((item) => item.id !== recipeId)
- .map((item) => ({ ...item, kind: "recipe" as const })),
- ];
- const components: RecipeStructure["components"] = [];
- const unmatched: string[] = [];
- const created: Option[] = [];
- const takenIds = new Set(
- [...ingredients, ...pendingIngredients].map((item) => item.id)
- );
- const newIngredient = (name: string) => {
- const base = normal(name).replace(/ /g, "_") || "imported_ingredient";
- let id = base,
- suffix = 2;
- while (takenIds.has(id)) id = `${base}_${suffix++}`;
- takenIds.add(id);
- const ingredient = { id, name: name.trim() };
- created.push(ingredient);
- const option = { ...ingredient, kind: "ingredient" as const };
- available.push(option);
- return option;
- };
- for (const parsedComponent of parsed.components) {
- const component = {
- id: uid("component"),
- name: parsedComponent.name || "Main",
- items: [],
- notes: [],
- } as RecipeStructure["components"][number];
- components.push(component);
- for (const parsedItem of parsedComponent.items) {
- const wanted = normal(parsedItem.ingredient);
- const tokens = identityTokens(wanted);
- const reducedTokens = tokens.filter(
- (token) =>
- ![
- "fresh",
- "dried",
- "flake",
- "leave",
- "chopped",
- "minced",
- "sliced",
- "crushed",
- "granulated",
- ].includes(token)
- );
- const labels = (item: (typeof available)[number]) =>
- [item.name, ...(item.aliases ?? [])].map(normal);
- const labelTokens = (item: (typeof available)[number]) =>
- labels(item).map(identityTokens);
- const match =
- available.find((item) => labels(item).includes(wanted)) ??
- available.find((item) =>
- labels(item).some(
- (label) => label.includes(wanted) || wanted.includes(label)
- )
- ) ??
- available.find((item) =>
- labelTokens(item).some((label) =>
- tokens.every((token) => label.includes(token))
- )
- ) ??
- available.find(
- (item) =>
- reducedTokens.length &&
- labelTokens(item).some((label) =>
- reducedTokens.every((token) => label.includes(token))
- )
- ) ??
- newIngredient(parsedItem.ingredient);
- const unitId = parsedItem.unit
- ? unitAliases[normal(parsedItem.unit)]
- : "each";
- if (!match || !unitId || !units.some((unit) => unit.id === unitId)) {
- unmatched.push(parsedItem.source_line);
- continue;
- }
- const notes = [
- parsedItem.preparation,
- parsedItem.note,
- parsedItem.alternatives.length
- ? `Alternatives: ${parsedItem.alternatives.join("; ")}`
- : null,
- ]
- .filter(Boolean)
- .join("; ");
- component.items.push({
- id: uid("line"),
- ...(match.kind === "ingredient"
- ? { ingredient_id: match.id }
- : { subrecipe_id: match.id }),
- quantity: parsedItem.quantity ?? 1,
- unit_id: unitId,
- basis_member: false,
- optional: parsedItem.optional,
- nutrition_retention_factor: 1,
- prep: [],
- ...(notes ? { notes } : {}),
- });
- }
- }
- const populated = components.filter((entry) => entry.items.length > 0);
- if (unmatched.length || !populated.length) {
- setBulkError(
- unmatched.length
- ? `Could not match: ${unmatched.join("; ")}`
- : "No ingredients could be matched."
- );
- setBulkParsing(false);
- return;
- }
- setPendingIngredients((current) => [...current, ...created]);
- setData((current) => {
- const additions = [...populated];
- const currentComponents = current.components.map((component) => ({
- ...component,
- items: [...component.items],
- }));
- if (
- additions[0]?.name.toLowerCase() === "main" &&
- currentComponents.length
- )
- currentComponents.at(-1)!.items.push(...additions.shift()!.items);
- return { ...current, components: [...currentComponents, ...additions] };
- });
- const creationMessage = created.length
- ? `${created.length} new canonical ingredient${
- created.length === 1 ? " is" : "s are"
- } pending (${created.map((item) => item.name).join(", ")}) and will be created when you save. `
- : "";
- setMessage(
- `${creationMessage}${
- parsed.warnings.length
- ? `Parser warnings: ${parsed.warnings.join(" ")}`
- : "Review imported ingredients before saving."
- }`
- );
- setBulkText("");
- setBulkError("");
- setBulkOpen(false);
- setBulkParsing(false);
- };
-
const addBulkPrepSteps = () => {
const lines = prepText
.split(/\r?\n/)
@@ -620,11 +410,10 @@ export function useRecipeStructure({
}, []);
useEffect(() => {
- if (!bulkOpen && !prepOpen) return;
+ if (!prepOpen) return;
const previousOverflow = document.body.style.overflow;
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
- setBulkOpen(false);
setPrepOpen(false);
}
};
@@ -634,7 +423,7 @@ export function useRecipeStructure({
document.body.style.overflow = previousOverflow;
document.removeEventListener("keydown", closeOnEscape);
};
- }, [bulkOpen, prepOpen]);
+ }, [prepOpen]);
useEffect(() => {
document.dispatchEvent(
@@ -670,13 +459,6 @@ export function useRecipeStructure({
setFocusedItem,
draftNotes,
setDraftNotes,
- bulkOpen,
- setBulkOpen,
- bulkText,
- setBulkText,
- bulkError,
- setBulkError,
- bulkParsing,
prepOpen,
setPrepOpen,
prepText,
@@ -705,7 +487,6 @@ export function useRecipeStructure({
createPendingRowIngredient,
createPendingIngredient,
createPendingRecipe,
- addBulkIngredients,
addBulkPrepSteps,
save,
unconvertedYieldItems,
diff --git a/src/lib/database.ts b/src/lib/database.ts
index 7ea08fc..dc00295 100644
--- a/src/lib/database.ts
+++ b/src/lib/database.ts
@@ -122,7 +122,7 @@ export function saveRecipeStructure(database: DatabaseSync, id: string, structur
database.exec("BEGIN IMMEDIATE");
try {
const ingredientInsert=database.prepare("INSERT INTO ingredients(id,schema_version,name,status,categories_json,tags_json,source_json) VALUES (?,2,?,'active','[]','[]',?)");
- for(const [ingredientId,name] of newIngredients)ingredientInsert.run(ingredientId,name,JSON.stringify({source_type:"ai_import",title:"Recipe ingredient import",reviewed:false}));
+ for(const [ingredientId,name] of newIngredients)ingredientInsert.run(ingredientId,name,JSON.stringify({source_type:"manual",title:"Recipe application",reviewed:true}));
const recipeInsert=database.prepare("INSERT INTO recipes(id,schema_version,save_version,title,summary,categories_json,tags_json,yield_quantity,yield_unit_id,yield_servings,yield_basis,notes_json,source_json) VALUES (?,2,1,?,NULL,'[]','[]',1,'gram',NULL,'theoretical','[]','{}')");
const newRecipeComponentInsert=database.prepare("INSERT INTO recipe_components(recipe_id,id,position,name,notes_json) VALUES (?,'main',1,'Main','[]')");
const newRecipeStepInsert=database.prepare("INSERT INTO recipe_steps(recipe_id,id,position,instruction,critical_control_point) VALUES (?,'step_1',1,'Placeholder.',0)");
diff --git a/src/lib/ingredient-parser.test.ts b/src/lib/ingredient-parser.test.ts
deleted file mode 100644
index 4c853b8..0000000
--- a/src/lib/ingredient-parser.test.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
- normalizeIngredientText,
- parseIngredientsDeterministic,
- validateIngredientParse,
-} from "./ingredient-parser";
-
-describe("ingredient parser", () => {
- it("normalizes copied checklist text and Unicode fractions", () => {
- expect(normalizeIngredientText("▢1 pound beef\n☐ ½ cup water\n▢salt, , to taste"))
- .toBe("1 pound beef\n1/2 cup water\nsalt, to taste");
- });
-
- it("validates a structured parser response", () => {
- expect(validateIngredientParse({
- components: [{ name:"Main", items:[{
- source_line:"1 pound beef", quantity:1, unit:"pound", ingredient:"ground beef",
- preparation:null, note:null, optional:false, alternatives:[],
- }] }], warnings:[],
- }, "1 pound beef")).toMatchObject({ normalized_text:"1 pound beef", components:[{name:"Main"}] });
- });
-
- it("rejects invented or malformed quantities", () => {
- expect(() => validateIngredientParse({
- components: [{ name:"Main", items:[{
- source_line:"salt", quantity:-1, unit:null, ingredient:"salt",
- preparation:null, note:null, optional:false, alternatives:[],
- }] }], warnings:[],
- }, "salt")).toThrow("invalid quantity");
- });
-
- it("deterministically parses ingredient lines with headers, quantities, units, and notes", () => {
- const raw = `
-Dry Mix:
-500g flour
-1/2 cup semolina
-salt to taste
-
-Wet:
-5 cloves garlic
-3 egg yolks
-olive oil (room temp)
- `;
-
- const result = parseIngredientsDeterministic(raw);
- expect(result.components).toHaveLength(2);
- expect(result.components[0].name).toBe("Dry Mix");
- expect(result.components[0].items).toHaveLength(3);
- expect(result.components[0].items[0]).toMatchObject({
- quantity: 500,
- unit: "gram",
- ingredient: "flour",
- });
- expect(result.components[0].items[1]).toMatchObject({
- quantity: 0.5,
- unit: "cup",
- ingredient: "semolina",
- });
- expect(result.components[0].items[2]).toMatchObject({
- ingredient: "salt",
- note: "to taste",
- });
-
- expect(result.components[1].name).toBe("Wet");
- expect(result.components[1].items).toHaveLength(3);
- expect(result.components[1].items[0]).toMatchObject({
- quantity: 5,
- unit: "clove",
- ingredient: "garlic",
- });
- expect(result.components[1].items[1]).toMatchObject({
- quantity: 3,
- ingredient: "egg yolks",
- });
- expect(result.components[1].items[2]).toMatchObject({
- ingredient: "olive oil",
- note: "room temp",
- });
- });
-});
diff --git a/src/lib/ingredient-parser.ts b/src/lib/ingredient-parser.ts
deleted file mode 100644
index 262e186..0000000
--- a/src/lib/ingredient-parser.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-export type ParsedIngredient = {
- source_line: string;
- quantity: number | null;
- unit: string | null;
- ingredient: string;
- preparation: string | null;
- note: string | null;
- optional: boolean;
- alternatives: string[];
-};
-
-export type ParsedIngredientComponent = {
- name: string;
- items: ParsedIngredient[];
-};
-
-export type IngredientParseResult = {
- normalized_text: string;
- components: ParsedIngredientComponent[];
- warnings: string[];
-};
-
-const FRACTIONS: Record = {
- "¼": "1/4", "½": "1/2", "¾": "3/4", "⅐": "1/7", "⅑": "1/9",
- "⅒": "1/10", "⅓": "1/3", "⅔": "2/3", "⅕": "1/5", "⅖": "2/5",
- "⅗": "3/5", "⅘": "4/5", "⅙": "1/6", "⅚": "5/6", "⅛": "1/8",
- "⅜": "3/8", "⅝": "5/8", "⅞": "7/8",
-};
-
-export function normalizeIngredientText(value: string): string {
- return value
- .replace(/[¼½¾⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞]/g, (match) => FRACTIONS[match] ?? match)
- .normalize("NFKC")
- .replace(/⁄/g, "/")
- .replace(/[\u200B-\u200D\u2060\uFEFF]/g, "")
- .split(/\r?\n/)
- .map((line) => line
- .replace(/^\s*(?:[▢□☐☑✓✔●•▪◦]|\[(?: |x|X)?\])\s*/, "")
- .replace(/\s*,\s*,+/g, ",")
- .replace(/[ \t]+/g, " ")
- .replace(/\s+,/g, ",")
- .trim())
- .filter(Boolean)
- .join("\n");
-}
-
-function parseFractionValue(raw: string): number | null {
- const trimmed = raw.trim();
- if (!trimmed) return null;
- if (/^\d+(?:\.\d+)?\s*-\s*\d+(?:\.\d+)?$/.test(trimmed)) {
- const [low, high] = trimmed.split("-").map((v) => Number(v.trim()));
- return (low + high) / 2;
- }
- const mixed = trimmed.match(/^(\d+)\s+([0-9]+)\/([0-9]+)$/);
- if (mixed) {
- const whole = Number(mixed[1]);
- const num = Number(mixed[2]);
- const den = Number(mixed[3]);
- if (den === 0) return null;
- return whole + num / den;
- }
- const frac = trimmed.match(/^([0-9]+)\/([0-9]+)$/);
- if (frac) {
- const num = Number(frac[1]);
- const den = Number(frac[2]);
- if (den === 0) return null;
- return num / den;
- }
- const num = Number(trimmed);
- return Number.isFinite(num) && num > 0 ? num : null;
-}
-
-const UNIT_MAP: Record = {
- g: "gram", gram: "gram", grams: "gram",
- kg: "kilogram", kilogram: "kilogram", kilograms: "kilogram",
- oz: "ounce", ounce: "ounce", ounces: "ounce",
- lb: "pound", lbs: "pound", pound: "pound", pounds: "pound",
- tsp: "teaspoon", tsps: "teaspoon", teaspoon: "teaspoon", teaspoons: "teaspoon",
- tbsp: "tablespoon", tbsps: "tablespoon", tbs: "tablespoon", tablespoon: "tablespoon", tablespoons: "tablespoon",
- c: "cup", cup: "cup", cups: "cup",
- ml: "milliliter", milliliter: "milliliter", milliliters: "milliliter",
- l: "liter", liter: "liter", liters: "liter",
- clove: "clove", cloves: "clove",
- ea: "each", each: "each",
- pinch: "pinch", pinches: "pinch",
- dash: "dash", dashes: "dash",
- slice: "slice", slices: "slice",
- sprig: "sprig", sprigs: "sprig",
- stalk: "stalk", stalks: "stalk",
- head: "head", heads: "head",
- bunch: "bunch", bunches: "bunch",
- can: "can", cans: "can",
- pkg: "package", package: "package", packages: "package",
- bottle: "bottle", bottles: "bottle",
-};
-
-export function parseSingleIngredientLine(sourceLine: string): ParsedIngredient {
- const line = sourceLine.trim();
- const optional = /\boptional\b/i.test(line);
-
- let workingLine = line;
- let note: string | null = null;
- const parenMatch = workingLine.match(/\(([^)]+)\)/);
- if (parenMatch) {
- const inside = parenMatch[1].trim();
- if (!/^\s*optional\s*$/i.test(inside)) {
- note = inside;
- }
- workingLine = workingLine.replace(parenMatch[0], " ").replace(/\s+/g, " ").trim();
- }
-
- let preparation: string | null = null;
- const commaIdx = workingLine.indexOf(",");
- if (commaIdx > 0) {
- const afterComma = workingLine.slice(commaIdx + 1).trim();
- workingLine = workingLine.slice(0, commaIdx).trim();
- if (afterComma) {
- if (/to taste/i.test(afterComma)) {
- note = note ? `${note}; ${afterComma}` : afterComma;
- } else {
- preparation = afterComma;
- }
- }
- }
-
- let quantity: number | null = null;
- let unit: string | null = null;
- let ingredientName = workingLine;
-
- const qtyUnitPattern = /^(\d+(?:\.\d+)?(?:\s+\d+\/\d+|\/\d+)?|\d+\/\d+|\d+(?:\.\d+)?\s*-\s*\d+(?:\.\d+)?)\s*([a-zA-Z]+)?\s+(.*)$/;
- const match1 = workingLine.match(qtyUnitPattern);
-
- if (match1) {
- const qtyVal = parseFractionValue(match1[1]);
- const possibleUnit = (match1[2] || "").toLowerCase();
- if (qtyVal != null) {
- quantity = qtyVal;
- if (possibleUnit && UNIT_MAP[possibleUnit]) {
- unit = UNIT_MAP[possibleUnit];
- ingredientName = match1[3].trim();
- } else if (possibleUnit) {
- ingredientName = `${possibleUnit} ${match1[3]}`.trim();
- } else {
- ingredientName = match1[3].trim();
- }
- }
- } else {
- const attachedMatch = workingLine.match(/^(\d+(?:\.\d+)?)\s*([a-zA-Z]+)\s*(.*)$/);
- if (attachedMatch) {
- const qtyVal = parseFractionValue(attachedMatch[1]);
- const possibleUnit = attachedMatch[2].toLowerCase();
- if (qtyVal != null && UNIT_MAP[possibleUnit]) {
- quantity = qtyVal;
- unit = UNIT_MAP[possibleUnit];
- ingredientName = attachedMatch[3].trim();
- }
- }
- }
-
- if (/to taste/i.test(ingredientName)) {
- ingredientName = ingredientName.replace(/\bto taste\b/i, "").trim();
- if (!note) note = "to taste";
- }
-
- ingredientName = ingredientName.replace(/^of\s+/i, "").trim();
- if (!ingredientName) ingredientName = line;
-
- return {
- source_line: line,
- quantity,
- unit,
- ingredient: ingredientName,
- preparation,
- note,
- optional,
- alternatives: [],
- };
-}
-
-export function parseIngredientsDeterministic(text: string): IngredientParseResult {
- const normalizedText = normalizeIngredientText(text);
- if (!normalizedText) {
- throw new Error("Enter at least one ingredient.");
- }
-
- const lines = normalizedText.split("\n").map((l) => l.trim()).filter(Boolean);
- const components: ParsedIngredientComponent[] = [];
- let currentComponent: ParsedIngredientComponent = {
- name: "Main",
- items: [],
- };
-
- for (const line of lines) {
- const isHeader = line.endsWith(":") || /^#+\s+/.test(line);
- if (isHeader) {
- const headerName = line.replace(/^#+\s*/, "").replace(/:$/, "").trim();
- if (currentComponent.items.length > 0 || currentComponent.name !== "Main") {
- if (currentComponent.items.length > 0) {
- components.push(currentComponent);
- }
- currentComponent = {
- name: headerName || "Main",
- items: [],
- };
- } else {
- currentComponent.name = headerName || "Main";
- }
- continue;
- }
-
- const item = parseSingleIngredientLine(line);
- currentComponent.items.push(item);
- }
-
- if (currentComponent.items.length > 0) {
- components.push(currentComponent);
- }
-
- if (components.length === 0) {
- throw new Error("The parser did not find any ingredients.");
- }
-
- return {
- normalized_text: normalizedText,
- components,
- warnings: [],
- };
-}
-
-export async function parseIngredientsWithOllama(text: string): Promise {
- return parseIngredientsDeterministic(text);
-}
-
-export function validateIngredientParse(value: unknown, normalizedText: string): IngredientParseResult {
- if (!value || typeof value !== "object") throw new Error("The parser returned an invalid document.");
- const source = value as Record;
- if (!Array.isArray(source.components) || !Array.isArray(source.warnings)) {
- throw new Error("The parser response is missing components or warnings.");
- }
- const components = source.components.map((entry) => {
- if (!entry || typeof entry !== "object") throw new Error("The parser returned an invalid component.");
- const component = entry as Record;
- if (typeof component.name !== "string" || !component.name.trim() || !Array.isArray(component.items)) {
- throw new Error("The parser returned an invalid component.");
- }
- const items = component.items.map((rawItem) => {
- if (!rawItem || typeof rawItem !== "object") throw new Error("The parser returned an invalid ingredient.");
- const item = rawItem as Record;
- if (typeof item.source_line !== "string") throw new Error("The parser returned an ingredient without its source line.");
- if (typeof item.ingredient !== "string" || !item.ingredient.trim()) throw new Error(`The parser returned an unnamed ingredient for: ${item.source_line}`);
- if (!(item.quantity === null || (typeof item.quantity === "number" && Number.isFinite(item.quantity) && item.quantity > 0))) {
- throw new Error(`The parser returned an invalid quantity for: ${item.source_line}`);
- }
- return {
- source_line: String(item.source_line),
- quantity: item.quantity as number | null,
- unit: item.unit ? String(item.unit) : null,
- ingredient: String(item.ingredient),
- preparation: item.preparation ? String(item.preparation) : null,
- note: item.note ? String(item.note) : null,
- optional: Boolean(item.optional),
- alternatives: Array.isArray(item.alternatives) ? (item.alternatives as string[]) : [],
- } satisfies ParsedIngredient;
- });
- return { name: component.name.trim(), items };
- }).filter((comp) => comp.items.length > 0);
-
- if (!components.length) throw new Error("The parser did not find any ingredients.");
- return { normalized_text: normalizedText, components, warnings: source.warnings as string[] };
-}
diff --git a/src/styles/components/recipe-edit.css b/src/styles/components/recipe-edit.css
index 3331dd0..af4873a 100644
--- a/src/styles/components/recipe-edit.css
+++ b/src/styles/components/recipe-edit.css
@@ -697,7 +697,6 @@
color: #202962;
text-decoration: underline;
}
-.application-body .bulk-add-trigger,
.application-body .bulk-prep-btn {
display: inline-flex;
align-items: center;
@@ -717,7 +716,6 @@
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;
@@ -741,7 +739,7 @@
font:italic 14px var(--sans);
}
.application-body .unified-recipe-editor .add-line-notes::placeholder { color:#a5a9c1; }
-.bulk-ingredient-backdrop {
+.bulk-prep-backdrop {
position:fixed;
z-index:1000;
inset:0;
@@ -750,10 +748,10 @@
padding:24px;
background:rgba(5,8,31,.52);
}
-.bulk-ingredient-dialog {
+.bulk-prep-dialog {
position:relative;
display:flex;
- width:min(780px,100%);
+ width:min(860px,100%);
max-height:calc(100vh - 48px);
padding:52px 68px 48px;
overflow:auto;
@@ -763,11 +761,11 @@
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 {
+.bulk-prep-dialog h2 { margin:0 0 24px; font-size:30px; font-weight:700; }
+.bulk-prep-dialog > p:not(.bulk-dialog-error) { margin:0 0 8px; color:#8b8d99; font-size:15px; font-weight:500; }
+.bulk-prep-dialog textarea {
width:100%;
- min-height:330px;
+ min-height:390px;
padding:18px 14px;
resize:vertical;
color:#050841;
@@ -777,7 +775,7 @@
border-radius:0;
font:15px/1.55 var(--sans);
}
-.bulk-ingredient-dialog textarea::placeholder { color:#a5a9c1; opacity:1; }
+.bulk-prep-dialog textarea::placeholder { color:#a5a9c1; opacity:1; }
.bulk-dialog-close {
position:absolute;
top:28px;
@@ -796,13 +794,11 @@
.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; }
+.bulk-prep-dialog footer { display:flex; align-items:center; justify-content:flex-end; gap:44px; }
+.bulk-prep-dialog footer button { min-height:48px; padding:10px 24px; cursor:pointer; border:0; font-weight:700; }
+.bulk-prep-dialog .bulk-cancel { color:#3d5df6; background:transparent; }
+.bulk-prep-dialog .bulk-submit { min-width:250px; color:#fff; background:#647df8; border-radius:28px; }
+.bulk-prep-dialog .bulk-submit:disabled { background:#aeb9f8; cursor:not-allowed; }
.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; }
diff --git a/src/styles/responsive.css b/src/styles/responsive.css
index 86f8abd..0c8bf57 100644
--- a/src/styles/responsive.css
+++ b/src/styles/responsive.css
@@ -621,13 +621,13 @@
padding-block: 8px !important;
}
- .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-prep-backdrop { padding: 0 !important; }
+ .bulk-prep-dialog { width: 100% !important; max-height: 100vh !important; min-height: 100vh !important; padding: 52px 22px 28px !important; border-radius: 0 !important; }
+ .bulk-prep-dialog h2 { font-size: 24px !important; }
+ .bulk-prep-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; }
+ .bulk-prep-dialog footer { gap: 12px !important; }
+ .bulk-prep-dialog .bulk-submit { min-width: 0 !important; flex: 1 !important; }
.application-body .recipe-book-workspace {