Skip to content

Show extra costs, and open screenshots full screen (store detail) - #424

Merged
onel merged 2 commits into
devfrom
store-detail-costs-and-screenshots
Aug 26, 2026
Merged

Show extra costs, and open screenshots full screen (store detail)#424
onel merged 2 commits into
devfrom
store-detail-costs-and-screenshots

Conversation

@onel

@onel onel commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What & why

The store detail page (/store/:id) now shows what a third party charges for an app, and opens its screenshots full screen. Both are ported from the marketing store's app page in ../cloud (internal/web/templates/pages/app.html, internal/web/static/app-gallery.js), so the two surfaces that show the same catalog show it the same way.

external_costs has reached the box API since catalog-wire-external-costs, and nothing rendered it — ten catalog apps declare one, listmonk needs a paid mail provider before it sends anything, and a person learned about that bill after installing. The screenshot strip was inert: a click did nothing, so the only view of an app was a 224px crop.

Spec(s) touched

None. APP_MANIFEST.md already describes external_costs and APP_STORE.md already describes the catalog fields this renders. No wire change, no brain change, no new dependency — display side only.

What was tested

  • make check-web green (typecheck + production build).
  • The three new Tailwind variants checked in the built CSS rather than assumed: group-open:rotate-90, backdrop:bg-black/90, pl-4.5 all emit rules.
  • Checked against the real store checkout, not a fixture: 10 apps declare external_costs; listmonk's is required, postiz's optional with 3 screenshots — so both branches of the pricing panel and the viewer's prev/next have real content.
  • Not yet clicked in a browser by the author. Being run now via make dev; anything it turns up lands on this branch before merge.

Known gaps & deviations

  • No automated test. web-ui has no test runner, so this is covered by the type checker and the build only — the standing gap for every view in this directory.
  • Cloud's progressive-enhancement half is deliberately not ported. There the thumbnails are <a> links to the full-size asset, so a blocked script still opens the image. That page is server-rendered HTML; this one is a Vue view that needs script to exist at all, so the thumbnails are buttons.
  • Price is a constant in the view ("Free"), not a wire field. Every app is free today and what malmo charges is authored in the curation source, not published. Mirrors cloud's appPrice; one line to change when a price does reach the wire.
  • Tooltip copy differs from cloud on purpose. Cloud says "these never appear on your malmo bill"; an appliance box has no malmo bill, so it reads "malmo never charges you for these."
  • The install dialog does not repeat the costs. Someone who lands straight on Install from a card never passes this panel. Whether a required cost belongs in the consent screen too is a design question, not a wire one.
  • No issue number. This was requested directly, not off the tracker, so there is no Closes #N line — it closes the "No UI yet" gap recorded in docs/progress/catalog-wire-external-costs.md.

Platform gaps

None — not a catalog PR.

Definition of done

  • Behavior works in the inner loop (make dev) — being verified now, not yet confirmed by the author.
  • Tests at the right layer: none available (web-ui has no test runner); make check-web green. No Go change, so make check is untouched by this diff.
  • No test deleted or newly skipped — no *_test.go in the diff.
  • Progress entry written (docs/progress/store-detail-costs-and-screenshots.md); progress index updated.
  • Catalog change: this touches a store view, not the wire. The other store surface (cloud's marketing app page) is where this came from — it already renders both, so nothing is owed there.
  • Catalog change, per-app field: not applicable — no field added to the wire; external_costs already ships on it.
  • Spec doc: nothing realized or diverged, so no spec edit and no DECISIONS.md entry.
  • No section-sign symbol, no hard-wrapped markdown, conventions per CLAUDE.md. No log/slog surface — frontend only.
  • Branch off dev, PR into dev.

Ports both recent app-page changes from ../cloud onto the box UI, so the
two surfaces that show the same catalog show it the same way.

external_costs has reached the box API since catalog-wire-external-costs
and nothing rendered it: ten catalog apps declare one, listmonk needs a
paid mail provider before it sends anything, and a person learned about
that bill after installing. A pricing panel above Information now shows a
Price row plus each cost as a <details> disclosure, required ones open
(the one you have to read to decide) and optional ones collapsed, with
the manifest's rate estimate and prose. The "you pay the provider, not
malmo" note is a CSS-only tooltip (group-hover / group-focus-within), so
it opens for a pointer, a keyboard and a tap with no script.

Price stays a constant in the view, not a wire field. Every app is free
today and what malmo charges is authored in the curation source, so this
is the one line to change when it does reach the wire, matching cloud's
appPrice.

The screenshot strip was inert. Each shot now opens fit to the viewport,
whole rather than a zoom of the thumbnail in place, with arrow keys,
prev/next and a live "2 of 3" counter that wraps both ways. It is a
native <dialog>, not the fixed-overlay idiom AppMenuDialog hand-writes:
the browser supplies the top layer, the backdrop, Esc, the focus trap and
focus restore, leaving an index and a src swap. showModal does not stop
every browser's wheel scroll, so the document is pinned while it is open
and released on close, once, for every dismissal path.

Cloud's progressive-enhancement half is deliberately not ported: that
page is server-rendered HTML, this one is a Vue view that needs script to
exist at all, so the thumbnails are buttons rather than links. Hover
lifts with a shadow instead of darkening the hairline border, for the
same reason cloud changed it: a pale line vanishes into a light
screenshot and frames a dark one.

No brain change, no wire change, no new dependency.

Claude-Session: https://claude.ai/code/session_012y33wKNxu7iUd1wLa95TXg
@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

The PR is not yet safe to merge because the screenshot modal still permits the signed-in page’s actual scroll container to move behind it.

The existing scroll-lock finding remains outstanding: AppDetailView hides overflow on the document root, while AppShell’s independently scrollable main element continues to own page scrolling.

Files Needing Attention: web-ui/src/views/AppDetailView.vue

Important Files Changed

Filename Overview
web-ui/src/views/AppDetailView.vue Adds pricing and screenshot-viewer behavior; the stale-index fix is effective, but the previously reported scroll-lock target remains incorrect.
docs/progress/store-detail-costs-and-screenshots.md Documents the new pricing and screenshot features, implementation choices, validation, and known gaps.
docs/progress/README.md Adds the completed work to the chronological progress index.

Reviews (2): Last reviewed commit: "Lift the screenshot viewer's scroll pin ..." | Re-trigger Greptile

Comment on lines +173 to +174
restoreOverflow = document.documentElement.style.overflow;
document.documentElement.style.overflow = "hidden";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Scroll lock misses scroller

When a screenshot is open, openShot hides overflow on documentElement, but the signed-in console scrolls through AppShell's independent overflow-y-auto <main>. The store page therefore continues moving behind the modal and can return the user to a different position when the viewer closes.

Knowledge Base Used: Web console experience

Comment thread web-ui/src/views/AppDetailView.vue
Review of the port found a real bug it introduced. The document scroll
was pinned on open and released on the dialog's close event only, but a
dialog can stop being on screen without closing: taking an open <dialog>
out of the DOM fires no close event. The page this was ported from is one
server-rendered document and can only be left by a full load; this is a
router route, so the Back button unmounts the component mid-viewer. The
pin then outlived the dialog and every other page in the dashboard was
unscrollable until a reload.

The pin now has its own flag and three release sites: the dialog's close,
onUnmounted, and a watcher on the shot list — a background refetch that
returns the same app with no screenshots removes the open dialog through
the v-if, which is the same hole again. That watcher also clamps the
index, so a shorter list cannot render a broken image under a "6 of 3"
counter.

Claude-Session: https://claude.ai/code/session_012y33wKNxu7iUd1wLa95TXg
@onel
onel merged commit 429f986 into dev Aug 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant