web: mark SAVE PHOTO PRO and keep MY PHOTOS for the accounts that have one

Saving into the account's own folder has always been the account's act —
the button opened the way in and the API answers an unproven address with
a 403 — but nothing on the button said so, so it read as a button that
quietly did nothing. It now wears the same PRO marker the chips do, and
only while the folder is not the visitor's.

MY PHOTOS is that folder's listing, so the tab is only offered once an
account can hold one. A guest loses the tab entirely rather than opening
it on an empty folder that could never fill; an account that has signed
up but not proven its address keeps the tab, and the tab keeps offering
the way to prove it.
This commit is contained in:
2026-09-22 21:46:05 +07:00
parent b9ac7746aa
commit 500068e63e
4 changed files with 17 additions and 3 deletions
+2 -1
View File
@@ -2244,6 +2244,7 @@ export function Workspace() {
<div className="shell">
<TopBar
recipeName={recipe.name}
pro={pro}
user={user}
exporting={exporting}
savingPhoto={savingPhoto}
@@ -2286,7 +2287,7 @@ export function Workspace() {
) : null}
<div className="workspace">
<ToolRail tab={tab} onTab={setTab} onDropRecipe={favoriteById} />
<ToolRail tab={tab} onTab={setTab} onDropRecipe={favoriteById} signedIn={!!user} />
<div className="chips">
{/* column 1 — the tab's own chips, RESET ruled off at the foot */}
+4 -1
View File
@@ -141,7 +141,10 @@
/* Edited value, strip closed: the text glows amber, the border does not. */
.chip.amber, .chip.amber .val { color: var(--accent); }
.chip .val { color: var(--text-dim); font-family: var(--mono); font-size: 11px; }
.chip .pro { font-size: 9px; letter-spacing: 0.08em; color: var(--accent); }
.chip .pro, .btn .pro { font-size: 9px; letter-spacing: 0.08em; color: var(--accent); }
/* A button is not a flex row the way a chip is, so the marker carries its own
gap off the label it qualifies. */
.btn .pro { margin-left: 6px; }
.chip:disabled { opacity: 0.4; cursor: not-allowed; }
/* The font group as a dropdown: one row that shows the current pairing instead
of three chips that all have to fit. */
+6 -1
View File
@@ -28,17 +28,22 @@ export function ToolRail({
tab,
onTab,
onDropRecipe,
signedIn = false,
}: {
tab: TabId;
onTab: (tab: TabId) => void;
// A recipe chip dragged onto FAVORITED is filed there (see App's favorite()).
onDropRecipe?: (id: string) => void;
// MY PHOTOS is the account's own folder, so the tab is only offered once
// there is an account to hold one.
signedIn?: boolean;
}) {
const { t } = useI18n();
const favorited = (id: TabId) => id === 'favorited';
const tabs = signedIn ? TABS : TABS.filter((entry) => entry.id !== 'photos');
return (
<nav className="rail" aria-label="tool tabs">
{TABS.map((entry) => (
{tabs.map((entry) => (
<button
key={entry.id}
type="button"
+5
View File
@@ -8,6 +8,7 @@ import type { User } from '../api';
// the web build adds — theme (light/dark + accent group) and language.
export function TopBar({
recipeName,
pro,
user,
exporting,
savingPhoto,
@@ -25,6 +26,9 @@ export function TopBar({
onLogout,
}: {
recipeName: string;
// Saving into the account's own folder is the account's own act, so the
// button wears the same PRO marker the chips do until the address is proven.
pro: boolean;
user: User | null;
exporting: boolean;
savingPhoto: boolean;
@@ -110,6 +114,7 @@ export function TopBar({
: photoMax === null
? t('save.photoNoMax', { n: photoCount })
: t('save.photo', { n: photoCount, max: photoMax })}
{!pro ? <span className="pro">PRO</span> : null}
</button>
<button type="button" className="btn primary" data-key="export-photo" disabled={exporting} onClick={onExport}>
{exporting ? t('act.exporting') : t('act.export')}