Recipe display: flat directions, tickable ingredient & direction checkboxes#308
Merged
spe1020 merged 13 commits intozapcooking:mainfrom Apr 19, 2026
Merged
Recipe display: flat directions, tickable ingredient & direction checkboxes#308spe1020 merged 13 commits intozapcooking:mainfrom
spe1020 merged 13 commits intozapcooking:mainfrom
Conversation
This was referenced Apr 19, 2026
4ee89d2 to
0ec9ad7
Compare
Tailwind's dark:hidden / dark:block on the logo <img> tags only take effect once the main CSS bundle loads. In dev (and occasional edge cases in prod) there's a brief window where both logos are visible and the light one paints first. Add a tiny inline <style> in app.html keyed on html.dark so the correct logo is selected before any external CSS arrives; tag the <img> pairs with logo-light / logo-dark.
A subtle "Add images by URL" toggle under the media grid lets users paste a direct image/video URL instead of uploading. First URL added becomes the cover photo (satisfying the publish requirement). Works across all three recipe editors (create, fork, gated) via the shared MediaUploader component.
DOMPurify can strip target/rel on <a> in some configurations, which caused markdown-preview links in the recipe editor to navigate in-place and destroy unsaved edits. Add an afterSanitizeAttributes hook that reapplies target=_blank + rel=noopener noreferrer on every anchor post-sanitization.
Watch the 11 draft fields via a JSON signature; 2s after the last edit, save to localStorage (and queue the debounced relay sync) so users don't lose work. Skips while empty and while a manual save is in flight; updates the URL with ?draft=<id> so reloads resume the same draft.
Auto-save was toggling isSavingDraft and writing a transient status message, which swapped the Save Draft button to "Saving…" and shifted the button row every 2s while typing — the editor appeared to jump around. The URL was also rewritten on every save. Make auto-save fully silent: no button state toggle, no message flash, and only write ?draft=<id> the first time a new draft gets an id. Manual save keeps its full feedback, and the Save Draft button's cloud icon still reflects relay sync state once the background publisher settles.
Two issues made reordering the last items nearly impossible: - handleDrop always inserted before the target, so dropping on the last item never moved the dragged item past it. Detect whether the cursor is in the target's top or bottom half and insert before or after accordingly. A top/bottom box-shadow marker shows which side the drop will land on (no layout shift). - The 8px flex gap between items was dead space — the drop zone only covered each pill. Drop gap-2 and give each <li> py-1 padding (first:pt-0 last:pb-0) so the hit area extends into the gap; the visible pill moves to an inner <div>. Total vertical spacing between items is unchanged.
- parser.ts: use a dedicated DOMPurify instance for markdown so the afterSanitizeAttributes hook doesn't leak target=_blank behavior into every other sanitize() call in the app (e.g. the longform editor). - MediaUploader.svelte: normalize the pasted URL via `parsed.toString()` before the dedupe check and before storing, so equivalent URLs (`example.com` vs `example.com/`) collapse to one entry. - MediaUploader.svelte: add `aria-expanded` + `aria-controls` to the "Add images by URL" disclosure toggle for screen-reader support. - create/+page.svelte: allow auto-save to persist when all fields are cleared on an existing draft (previously the delete-everything state was never saved, so reloading brought the old content back). - create/+page.svelte: declare `draftSignature` explicitly and debounce its computation to 250ms so we don't re-JSON.stringify the entire draft on every keystroke.
<svelte:window on:error> was catching native ErrorEvents but treating them as CustomEvents, so the details were lost and every spurious window error — including the ones iOS Safari fires during share / backgrounding / service-worker transitions — nuked the entire app with an "Oops! Something went wrong" screen. Remove the svelte:window binding and align the window 'error' listener with the existing 'unhandledrejection' listener: log-only, no fatal UI. The programmatic CustomEvent path is preserved for child components that want to surface a fallback deliberately.
After the earlier patch removed `<svelte:window on:error={handleError} />`
to stop benign window errors from nuking the app, `handleError` had no
caller and the entire fallback UI / retry / error-state code was
effectively dead — RecipeErrorBoundary and FeedErrorBoundary would
never render their fallback.
Re-wire `handleError` to a dedicated `window` CustomEvent
(`zc:fatal-error`). Child components that hit a genuinely fatal error
can dispatch:
window.dispatchEvent(new CustomEvent('zc:fatal-error', {
detail: { error, errorInfo }
}));
to surface the boundary's fallback UI. Native ErrorEvents are still
logged-only, same as before.
Typing `@<name>` in the new kind:1 post composer was not showing any suggestions. The dropdown component WAS rendering with the right state (`show=true`, populated `suggestions`), but it was positioned off-screen because of a CSS containing-block issue introduced by zapcooking#280. Root cause: zapcooking#280 switched MentionDropdown to `position: fixed` to escape the composer's overflow container. Per the CSS spec, `fixed` positions against the viewport unless an ancestor has `transform`, `filter`, or `perspective` — in which case that ancestor becomes the containing block. Modal's <dialog> uses Tailwind's `-translate-x-1/2 -translate-y-1/2` (a permanent `transform`), so the dropdown's `top`/`left` values — computed from the caret's `getBoundingClientRect()` in viewport coordinates — were being interpreted relative to the dialog box instead, pushing the dropdown off-screen. Fix: portal the dropdown directly to `document.body` when `show` is true. It now lives outside the transformed dialog subtree, so `position: fixed` is correctly viewport-relative and the caret-based coordinates land where expected. Also drop a redundant double-assignment in PostComposer's on:input handler (also from zapcooking#280) so the composer matches the working ReplyComposer pattern — the controller's `onTextChange` callback is already the single source of truth for composer text state.
The keyword heuristic was mis-classifying steps — e.g. a banana-bread
recipe would land "Heat oven to 350°" under Baking & Cooking and every
remaining step (including "Bake for 65 minutes") under Finishing &
Serving, which read as nonsense.
Drop PHASE_KEYWORDS and the phase-assignment loop. extractAndGroupDirections
now always returns a single { id: 'directions', title: 'Directions',
steps } phase — existing callers (Recipe, PrintRecipeModal) work
unchanged because both already handle the single-phase case. The
DirectionsPhases component now renders a plain numbered list; the
collapsibles, expand/collapse controls, and hash-scroll per phase are
gone with the phase concept.
Replace the bullet-list ingredients with a checkbox list so cooks can tick off items as they shop or cook. Checks persist per-recipe in localStorage keyed by the Nostr event id (`recipe_ingredients_checked:<id>`) so state survives reloads and navigation. Adds a "Clear" affordance once anything is checked. Implementation: extract the `## Ingredients` section out of the prose "before directions" chunk the same way `## Details` already is, parse its items into an array, and render them via a new Ingredients.svelte component between the prose and DirectionsPhases. Update scrollToIngredients to target the new section's DOM id. PrintRecipeModal pulls ingredients from event.content independently, so printing is unaffected. Closes zapcooking#223
Mirror the ingredients-checkbox pattern for directions so cooks can tick off steps as they work through the recipe. Checks persist per-recipe in localStorage keyed by step number (`recipe_directions_checked:<id>`). Shows a "Clear" affordance once anything is checked; checked steps get strikethrough + muted color but retain their step number for easy resumption. Note that this intentionally does not scale or transform step text — timing and instruction text is left as-is.
0ec9ad7 to
1d3c750
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three related recipe-display improvements on top of #306 → #305 → #304.
1. Remove auto-grouping of directions into phases
The keyword heuristic was mis-classifying steps — e.g. a banana-bread recipe landed
Heat oven to 350°under "Baking & Cooking" and every remaining step (includingBake for 65 minutes) under "Finishing & Serving", which read as nonsense.Drop
PHASE_KEYWORDSand the phase-assignment loop.extractAndGroupDirectionsnow always returns a single{ id: 'directions', title: 'Directions', steps }phase; the collapsibles, expand/collapse controls, and hash-scroll per phase are gone with the phase concept. Existing callers (Recipe, PrintRecipeModal) work unchanged because both already handle the single-phase case.2. Tickable ingredient checkboxes (closes #223)
Replace the bullet-list ingredients with a checkbox list so cooks can tick off items as they shop or cook. Checks persist per-recipe in
localStoragekeyed by the Nostr event id (recipe_ingredients_checked:<id>) so state survives reloads and navigation. A "Clear" affordance appears once anything is checked.Implementation: extract the
## Ingredientssection out of the prose "before directions" chunk the same way## Detailsalready is, parse its items, and render them via a newIngredients.sveltecomponent between the prose and DirectionsPhases. UpdatedscrollToIngredientsto target the new section's DOM id.PrintRecipeModalpulls ingredients fromevent.contentindependently, so printing is unaffected.3. Tickable direction checkboxes
Mirror the ingredients pattern for directions. Checks persist at
recipe_directions_checked:<id>; checked steps get strikethrough + muted color but retain their step number for easy resumption. Step text is left verbatim — no scaling, no transformation.Notes
parser.ts,Recipe.svelte,DirectionsPhases.svelte, and the newIngredients.svelte.Test plan
## Ingredientssection (they simply don't render the checkbox block).