Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions docs/shell-and-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ Status: **implemented in one PR.** Scope is the signed-in merchant workspace she

## What shipped

| Area | Behaviour |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Grouped sidebar | Overview, then **Run service** (Orders, Point of Sale, Kitchen, Dining floor), **Menu** (Menu Studio, QR codes), **Insights** (Reports), **Admin** (Organization, Settings). Destinations the plan or role does not include collapse into an **Add-ons** disclosure with a plain hint. |
| Mobile bottom bar | Orders, Till, Kitchen, Floor and **More**, filtered by what the user can use. A menu-only outlet is back-filled with Overview / Menu so the bar is never nearly empty. More holds everything else, plus Search and Notifications. |
| Command palette | `Ctrl/Cmd+K` or the topbar button. Pages, actions ("Open till", "Add a dish"), dishes ("Mark X sold out" / "Restore X"), orders by number, tables, and an "Ask Ember" hand-off. |
| Topbar | Search, **+ New** menu, **till chip**, **connection and last-sync** indicator, **notification bell**. |
| Notification centre | One drawer: status, aggregated alerts, and the single sound and mute control. |
| Dining floor page | `/dashboard/floor`: the tables and guest-request panels that were only an overview widget, so the bar, sidebar and palette have somewhere to go. |
| Area | Behaviour |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Grouped sidebar | Overview, then **Run service** (Orders, Point of Sale, Kitchen, Dining floor), **Menu** (Menu Studio, QR codes), **Insights** (Reports), **Admin** (Organization, Settings). Destinations the plan or role does not include collapse into an **Add-ons** disclosure with a plain hint. |
| Mobile bottom bar | Menus, QR Codes, Orders, Floor and **More**, filtered by what the user can use. An outlet without ordering is back-filled with Overview / POS so the bar is never nearly empty. More holds everything else (POS, Kitchen, Reports, Settings…), plus Search, Notifications and **Sign out**. The logo header is hidden on phones; the shared topbar remains. |
| Command palette | `Ctrl/Cmd+K` or the topbar button. Pages, actions ("Open till", "Add a dish"), dishes ("Mark X sold out" / "Restore X"), orders by number, tables, and an "Ask Ember" hand-off. |
| Topbar | Search, **+ New** menu, **till chip**, **connection and last-sync** indicator, **notification bell**. |
| Notification centre | One drawer: status, aggregated alerts, and the single sound and mute control. |
| Dining floor page | `/dashboard/floor`: the tables and guest-request panels that were only an overview widget, so the bar, sidebar and palette have somewhere to go. |

## Decisions

Expand All @@ -37,7 +37,7 @@ Status: **implemented in one PR.** Scope is the signed-in merchant workspace she
## Accessibility

- Palette is a real combobox + listbox (`aria-activedescendant`, `aria-selected`, grouped options, result-count announcement). Focus returns to whatever opened it (see below).
- The bottom bar is a `nav` landmark. Visible labels are short ("Till"); accessible names stay full ("Point of Sale", "Kitchen Setup", "Orders, 3 waiting").
- The bottom bar is a `nav` landmark. Visible labels are short ("Menus", "QR Codes"); accessible names stay full ("Point of Sale", "Kitchen Setup", "Orders, 3 waiting").
- Status is never colour alone: connection, till, alert severity, dish availability and table state all carry an icon and a word.
- 44 px minimum targets, safe-area insets, reduced motion honoured.

Expand Down
6 changes: 3 additions & 3 deletions src/app/(dashboard)/dashboard/pos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,16 @@ export default async function PosPage() {
: null;
return (
<>
{workspace.context.enabled && pwaOperationalEnabledForOutlet(outlet.id) ? (
<OperationalPwa workspace="POS" />
) : null}
<TheTill
key={outlet.id}
organizationId={outlet.organizationId}
outletId={outlet.id}
initial={workspace}
publishedSummary={publishedSummary}
/>
{workspace.context.enabled && pwaOperationalEnabledForOutlet(outlet.id) ? (
<OperationalPwa workspace="POS" />
) : null}
</>
);
}
2 changes: 1 addition & 1 deletion src/app/(dashboard)/dashboard/pos/pos-editorial.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* POS editorial workspace: matches the Dashboard and Tabletop Press without changing finance behavior. */
.mm-workspace-main:has(> .mm-shared-topbar + .till-page.pos-editorial) {
.mm-workspace-main:has(> .till-page.pos-editorial) {
max-width: none;
margin: 0;
padding: 0;
Expand Down
41 changes: 39 additions & 2 deletions src/app/(dashboard)/workspace-bottom-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import * as Dialog from "@radix-ui/react-dialog";
import {
Bell,
CircleHelp,
Ellipsis,
ExternalLink,
LogOut,
Search,
ShieldAlert,
UserRound,
Expand All @@ -19,12 +21,13 @@ import {
moreItems,
type ResolvedNavItem,
} from "@/modules/dashboard/nav-model";
import { authClient } from "@/lib/auth-client";
import { useReturnFocus } from "@/lib/use-return-focus";
import { WorkspaceIcon } from "./workspace-icons";
import { useWorkspaceShell } from "./workspace-shell";

/**
* Phone navigation: Orders, POS, Kitchen and Floor (whichever the user can use), then More.
* Phone navigation: Menus, QR Codes, Orders and Floor (whichever the user can use), then More.
* Visible text is short; the accessible name stays the full label (for example "Kitchen Setup").
*/
export function WorkspaceBottomBar({
Expand All @@ -35,6 +38,23 @@ export function WorkspaceBottomBar({
publicHref?: string | null;
}) {
const pathname = usePathname();
const router = useRouter();
const [signingOut, setSigningOut] = useState(false);
const [signOutError, setSignOutError] = useState("");
async function signOut() {
if (signingOut) return;
setSigningOut(true);
setSignOutError("");
try {
const result = await authClient.signOut();
if (result.error) throw new Error("not confirmed");
router.replace("/login");
router.refresh();
} catch {
setSignOutError("Couldn't sign out. Please try again.");
setSigningOut(false);
}
}
const returnFocus = useReturnFocus();
const {
capabilities,
Expand Down Expand Up @@ -199,6 +219,23 @@ export function WorkspaceBottomBar({
<span>Help &amp; guides (new tab)</span>
</Link>
</li>
<li>
<button
type="button"
className="mm-sheet-link"
style={{ width: "100%", textAlign: "left", font: "inherit" }}
disabled={signingOut}
onClick={() => void signOut()}
>
<LogOut size={19} strokeWidth={1.9} aria-hidden="true" />
<span>{signingOut ? "Signing out…" : "Sign out"}</span>
</button>
{signOutError && (
<p role="alert" className="mm-sheet-link" style={{ color: "#b42318" }}>
{signOutError}
</p>
)}
</li>
</ul>
</Dialog.Content>
</Dialog.Portal>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(dashboard)/workspace-ux.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
--mm-content-wide: 1480px;
--mm-content-gutter: clamp(16px, 3vw, 44px);
}
.mm-workspace-main:has(> .mm-shared-topbar + :is(.pos-editorial, .reports-workspace, .qr-studio)) {
.mm-workspace-main:has(> :is(.pos-editorial, .reports-workspace, .qr-studio)) {
max-width: none;
margin: 0;
padding: 0;
Expand Down
9 changes: 5 additions & 4 deletions src/app/(dashboard)/workspace.css
Original file line number Diff line number Diff line change
Expand Up @@ -1083,13 +1083,14 @@
border-bottom: 1px solid var(--decorative);
}

/* Phones/tablets use the bottom bar and shared topbar; the logo header only wasted space. */
.mm-workspace-brand {
min-height: 60px;
margin: 0 16px;
display: none;
}

.mm-workspace-brand img {
width: 150px;
/* Nothing left to show (single outlet): remove the empty header and its border. */
.mm-workspace-sidebar:not(:has(.mm-workspace-outlet)) {
display: none;
}

.mm-workspace-outlet {
Expand Down
4 changes: 2 additions & 2 deletions src/app/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { getRestaurantsByUserId } from "@/modules/restaurants/service";
// Always read the authenticated session on the server, including after sign-out/revocation.
// This route intentionally stays network-only in the service worker.
export const dynamic = "force-dynamic";
/** Menu Studio. Single source for where the installed app and Play TWA land after sign-in. */
const APP_HOME_PATH = "/dashboard/menu";
/** Overview. Single source for where the installed app and Play TWA land after sign-in. */
const APP_HOME_PATH = "/dashboard";
export const metadata = {
title: "Open MenuMinute",
robots: { index: false, follow: false },
Expand Down
11 changes: 11 additions & 0 deletions src/app/kitchen/kitchen.css
Original file line number Diff line number Diff line change
Expand Up @@ -2814,3 +2814,14 @@
flex-basis: min(82%, 250px);
}
}

/* Ember launcher wrapper: transparent on larger screens, removed from the phone layout. */
.kk-ember-slot {
display: contents;
}

@media (max-width: 700px) {
.kk-ember-slot {
display: none;
}
}
12 changes: 7 additions & 5 deletions src/app/kitchen/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ export default async function KitchenPage() {
<OperationalPwa workspace="Kitchen" />
) : null}
{showEmber && (
<EmberLauncher
organizationId={outlet.organizationId}
outletId={outlet.id}
outletName={outlet.name}
/>
<div className="kk-ember-slot">
<EmberLauncher
organizationId={outlet.organizationId}
outletId={outlet.id}
outletName={outlet.name}
/>
</div>
)}
<KitchenBoard
organizationId={outlet.organizationId}
Expand Down
23 changes: 21 additions & 2 deletions src/app/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { z } from "zod";
import { authClient } from "@/lib/auth-client";
import { AuthShell } from "@/components/auth/auth-shell";
import { authNextKey, safeReturnPath } from "@/lib/auth-return";
import { TERMS_VERSION } from "@/content/legal";
const schema = z.object({
email: z.email("Enter a valid email address."),
password: z.string().min(1, "Password is required."),
Expand Down Expand Up @@ -66,8 +67,13 @@ export function LoginForm({ googleEnabled = false }: { googleEnabled?: boolean }
try {
const result = await authClient.signIn.social({
provider: "google",
callbackURL: next || "/dashboard",
// One seamless flow: unknown Google accounts are registered, with the
// disclosed current terms recorded server-side, then routed via /login
// (outlets -> dashboard overview, none -> onboarding).
requestSignUp: true,
callbackURL: "/login" + (next ? "?next=" + encodeURIComponent(next) : ""),
errorCallbackURL: "/login" + (next ? "?next=" + encodeURIComponent(next) : ""),
additionalData: { acceptTerms: true, termsVersion: TERMS_VERSION },
});
if (result.error)
setError("Google sign-in did not finish. Please retry or sign in with email.");
Expand All @@ -84,6 +90,19 @@ export function LoginForm({ googleEnabled = false }: { googleEnabled?: boolean }
{googleBusy ? "Connecting to Google…" : "Continue with Google"}
</button>
)}
{googleEnabled && (
<p className="mm-auth-entry-help">
New here? Continuing with Google creates your account and means you agree to the{" "}
<Link href="/terms" target="_blank" rel="noopener noreferrer">
Terms of Service
</Link>{" "}
and acknowledge the{" "}
<Link href="/privacy" target="_blank" rel="noopener noreferrer">
Privacy Policy
</Link>
.
</p>
)}
<Link
className="mm-auth-create-account"
href={next ? "/register?next=" + encodeURIComponent(next) : "/register"}
Expand Down Expand Up @@ -176,7 +195,7 @@ export function LoginForm({ googleEnabled = false }: { googleEnabled?: boolean }
? "That Google account is not connected yet. Sign in with your password, then link Google in Account Security."
: params.get("error") === "signup_disabled" ||
params.get("error") === "terms_required"
? "New to MenuMinute? Choose Create a free account to register with Google or email."
? "We could not create your account with Google. Please retry, or choose Create a free account to register with email."
: "Google sign-in could not be completed. If your account is not yet linked, sign in with your password first."}
</p>
)}
Expand Down
5 changes: 4 additions & 1 deletion src/app/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ export default async function OnboardingPage({
className="wizard-welcome-mobile"
/>
</div>
<OnboardingForm initialQuick={quick === "1"} />
<OnboardingForm
initialQuick={quick === "1"}
prefill={{ name: session.user.name, email: session.user.email }}
/>
</AuthShell>
);
}
10 changes: 8 additions & 2 deletions src/app/onboarding/setup-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { OnboardingWizard } from "@/components/onboarding/onboarding-wizard";
* Super Onboarding Wizard setup form with progressive disclosure, GPS geocoordinates,
* mediawork placeholders, and server-owned draft reconciliation.
*/
export function OnboardingForm({ initialQuick = false }: { initialQuick?: boolean }) {
return <OnboardingWizard initialQuick={initialQuick} />;
export function OnboardingForm({
initialQuick = false,
prefill,
}: {
initialQuick?: boolean;
prefill?: { name?: string; email?: string };
}) {
return <OnboardingWizard initialQuick={initialQuick} prefill={prefill} />;
}
2 changes: 1 addition & 1 deletion src/app/register/register-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export function RegistrationForm({ googleEnabled = false }: { googleEnabled?: bo
const result = await authClient.signIn.social({
provider: "google",
requestSignUp: true,
callbackURL: next || "/onboarding",
callbackURL: "/login" + (next ? "?next=" + encodeURIComponent(next) : ""),
errorCallbackURL:
"/register" + (next ? "?next=" + encodeURIComponent(next) : ""),
additionalData: { acceptTerms: true, termsVersion: TERMS_VERSION },
Expand Down
24 changes: 19 additions & 5 deletions src/components/onboarding/onboarding-wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,18 @@ function validateStep(step: number, draft: WizardDraft): string | null {
interface OnboardingWizardProps {
onSuccess?: (outletId: string) => void;
initialQuick?: boolean;
/** Account profile (e.g. from Google) used only to suggest empty contact fields. */
prefill?: { name?: string; email?: string };
}

export function OnboardingWizard({ onSuccess, initialQuick = false }: OnboardingWizardProps) {
export function OnboardingWizard({
onSuccess,
initialQuick = false,
prefill,
}: OnboardingWizardProps) {
const router = useRouter();
const prefillName = prefill?.name;
const prefillEmail = prefill?.email;
const [currentStep, setCurrentStep] = useState<number>(0);
const [quickStart, setQuickStart] = useState(initialQuick);
const quickLinkRequested = useRef(initialQuick);
Expand Down Expand Up @@ -183,7 +191,7 @@ export function OnboardingWizard({ onSuccess, initialQuick = false }: Onboarding
} catch {
/* Storage may be unavailable. */
}
router.replace("/dashboard/get-started");
router.replace("/dashboard");
router.refresh();
}
return;
Expand Down Expand Up @@ -220,7 +228,13 @@ export function OnboardingWizard({ onSuccess, initialQuick = false }: Onboarding
window.navigator.language,
Intl.DateTimeFormat().resolvedOptions().timeZone
);
const fresh = { ...initialWizardDraft, ...(browser ?? {}) };
const fresh = {
...initialWizardDraft,
...(browser ?? {}),
// Suggestions from the signed-in account; saved drafts are never overwritten.
operationsContactName: prefillName?.trim() ?? "",
operationsContactEmail: prefillEmail?.trim() ?? "",
};
setDraft(shouldQuick ? quickStartDraft(fresh) : fresh);
}
let storedStep = Number.NaN;
Expand Down Expand Up @@ -251,7 +265,7 @@ export function OnboardingWizard({ onSuccess, initialQuick = false }: Onboarding
} finally {
reconciliation.current = false;
}
}, [router, onSuccess]);
}, [router, onSuccess, prefillName, prefillEmail]);

useEffect(() => {
void Promise.resolve().then(reconcile);
Expand Down Expand Up @@ -409,7 +423,7 @@ export function OnboardingWizard({ onSuccess, initialQuick = false }: Onboarding
} catch {
/* Storage may be unavailable. */
}
router.replace("/dashboard/get-started");
router.replace("/dashboard");
router.refresh();
}
}
Expand Down
Loading
Loading