Skip to content
Closed
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
4 changes: 3 additions & 1 deletion apps/web/src/screens/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export function HomeScreen() {
const others = places.filter(place => place.id !== first.id);
const needs = places.flatMap((hub) => hub.now.map((item) => ({ hub, item })))
.sort((a, b) => (a.item.dueOn ?? "9999").localeCompare(b.item.dueOn ?? "9999")).slice(0, 6);
// Name the commune on each row only when the list spans more than one.
const severalCommunes = new Set(needs.map(({ hub }) => hub.id)).size > 1;
const activity = places.flatMap((hub) => hub.activity.map((item) => ({ hub, item })))
.sort((a, b) => (Date.parse(b.item.createdAt ?? "") || 0) - (Date.parse(a.item.createdAt ?? "") || 0)).slice(0, 6);
const incomplete = places.some((hub) => hub.activityState !== "ready");
Expand All @@ -71,7 +73,7 @@ export function HomeScreen() {
<div className="hub-feed">
{needs.map(({ hub, item }, index) => (
<button key={`${hub.id}-${item.title}-${index}`} className="hub-feed-row" type="button" onClick={() => go(item.go, { hub: hub.id, expense: item.expenseId, operation: item.operationId })}>
<Pill tone={item.pill === "warn" ? "warn" : "open"}>{hub.name}</Pill>
{severalCommunes && <Pill tone={item.pill === "warn" ? "warn" : "open"}>{hub.name}</Pill>}
{item.dueOn && <CalendarDate date={item.dueOn} label="Due" />}
<div><strong>{item.title}</strong><div className="when">{item.when}</div></div>
<span className="hub-feed-go">Open</span>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/screens/Hub.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@
.c-hub-location span { margin-right: 8px; opacity: .75; }
.c-hub-people-link { display: flex; align-items: center; gap: 12px; width: fit-content; margin-top: 17px; padding: 0; border: 0; background: none; color: inherit; text-shadow: none; font-size: 12px !important; }
.c-hub-people-link .avatar-stack { display: flex; align-items: center; }
.c-hub-people-link .avatar { width: 28px; height: 28px; margin-right: -5px; border: 2px solid #fff; font-size: 9px; }
.c-hub-people-link .avatar:last-child { margin-right: 0; }
.c-hub-people-link .avatar { width: 28px; height: 28px; border: 2px solid #fff; font-size: 9px; }
.c-hub-hero-actions { position: absolute; right: 22px; top: 20px; display: flex; gap: 7px; }
.c-hub-settings { display: flex; align-items: center; gap: 6px; padding: 9px 12px; min-height: 38px; border: 1px solid #ffffff55; border-radius: var(--radius-control); background: #15221c66; color: #fff; font-size: 12px !important; }

Expand Down Expand Up @@ -89,7 +88,8 @@
.c-hub-count { flex: none; color: var(--hub-muted); font-size: 11px; white-space: nowrap; }
.c-hub-link { padding: 4px 0; border: 0; background: none; color: var(--hub-action); font-size: 12px !important; font-weight: 500 !important; text-decoration: none; }
.c-hub-link:hover { text-decoration-color: currentColor; }
.c-hub-more { margin-top: 14px; }
.c-hub-more { display: block; margin-top: 14px; }
.c-hub-more + .c-hub-more { margin-top: 10px; }
.c-hub-invite { display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; border: 1px solid #6f9b87; border-radius: var(--radius-control); background: var(--hub-panel); color: var(--hub-ink); font-size: 12px !important; }
.c-hub-invite:hover { background: var(--hub-quiet); }
.c-hub-empty { padding: 14px 0; border-top: 1px solid var(--hub-line); color: var(--hub-muted); font-size: 13px; }
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/screens/Money.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export function SettleScreen() {
<Avatar tone={receiving ? debtor?.cls ?? "house" : payer?.cls ?? "house"} initials={receiving ? debtor?.ini ?? "?" : payer?.ini ?? "?"} />
<div>
<div className="names">{receiving ? `${debtor?.name ?? "Member"} owes you` : `You owe ${payer?.name ?? "the bill payer"}`}</div>
<div className="why">{expense.name}{expense.dueOn ? ` 路 due ${expense.dueOn}` : ""}</div>
<div className="why">{expense.name}{expense.dueOn ? ` 路 due ${expense.due}` : ""}</div>
{share.settledMinor > 0 ? <div className="why">{formatMoney(share.settledMinor, hub.currency)} of {formatMoney(share.amountMinor, hub.currency)} received</div> : null}
{report ? <div className="why c-report-line">{receiving
? `${reportSummary(report, hub.currency, debtor?.name ?? "A member")}. Not yet confirmed.`
Expand Down
90 changes: 43 additions & 47 deletions apps/web/src/shell/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { IconName } from "../ui/Icon";
import { Icon } from "../ui/Icon";
import { Avatar } from "../ui/widgets";
import { useApp } from "../state";
import type { ScreenId, WindowId } from "../data/household";
import type { ScreenId } from "../data/household";
import { QuickSearch } from "../ui/QuickSearch";
import { cx } from "../ui/cx";
import { ReminderUsageMeter } from "../ui/ReminderUsage";
Expand All @@ -27,6 +27,12 @@ const BASIC: NavItem[] = [
{ id: "operations", label: "Operations", icon: "operations" },
];

// The phone menu has no other route to these in basic mode.
const DRAWER_BASIC: NavItem[] = [
{ id: "members", label: "Members", icon: "members" },
{ id: "templates", label: "Templates", icon: "templates" },
];

const MONEY: NavItem[] = [
{ id: "expenses", label: "Expenses", icon: "expenses" },
{ id: "settle", label: "Settle", icon: "settle" },
Expand Down Expand Up @@ -163,8 +169,12 @@ function NavButton({
);
}

export function Sidebar({ screen }: { screen: ScreenId }) {
const { go, hub, you, reminderUsage } = useApp();
export function Sidebar({ screen, drawer }: { screen: ScreenId; drawer?: boolean }) {
const { go, hub, you, reminderUsage, setMoreOpen } = useApp();
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (drawer) closeRef.current?.focus();
}, [drawer]);
const accountRole = hub.youMeta?.split(" 路 ")[0];
const active = (id: ScreenId) =>
screen === id ||
Expand All @@ -174,17 +184,29 @@ export function Sidebar({ screen }: { screen: ScreenId }) {
(id === "expenses" && screen === "expenses-empty");

return (
<aside className="a-side">
<button className="a-brand" type="button" onClick={() => go("home")}>
<span className="a-wordmark">commune.</span>
</button>
<aside
className={cx("a-side", drawer && "a-drawer")}
role={drawer ? "dialog" : undefined}
aria-modal={drawer ? true : undefined}
aria-label={drawer ? "Menu" : undefined}
>
<div className="a-side-head">
<button className="a-brand" type="button" onClick={() => go("home")}>
<span className="a-wordmark">commune.</span>
</button>
{drawer ? (
<button ref={closeRef} className="a-drawer-x" type="button" aria-label="Close menu" onClick={() => setMoreOpen(false)}>
<Icon name="close" size={16} />
</button>
) : null}
</div>

<nav className="a-nav" aria-label="Primary">
<NavButton item={{ id: "home", label: "Home", icon: "home" }} active={active("home")} onClick={() => go("home")} />
<NavButton item={{ id: "hub", label: "This Hub", icon: "hub" }} active={active("hub")} onClick={() => go("hub")} />

<div className="a-nav-basic">
{BASIC.map((item) => (
{(drawer ? [...BASIC, ...DRAWER_BASIC] : BASIC).map((item) => (
<NavButton key={item.id} item={item} active={active(item.id)} onClick={() => go(item.id)} />
))}
</div>
Expand Down Expand Up @@ -263,10 +285,9 @@ export function Header({ screen }: { screen: ScreenId }) {
}, [searchOpen, currentWindow]);
return (
<header className={cx("a-header", (screen === "home" || screen === "home-empty") && "a-header-home")}>
<button className="a-icon-btn a-more-btn" type="button" aria-label="More" onClick={() => setMoreOpen(true)}>
<button className="a-icon-btn a-more-btn" type="button" aria-label="Menu" aria-haspopup="dialog" onClick={() => setMoreOpen(true)}>
<Icon name="menu" size={16} />
</button>
<img className="a-header-mark" src="/logo-mark.svg" width={18} height={18} alt="" />
<div className="a-header-title">
<h1>{pageTitle}</h1>
{pageLede ? <p>{pageLede}</p> : null}
Expand Down Expand Up @@ -322,47 +343,22 @@ export function PhoneTabs({ screen }: { screen: ScreenId }) {
);
}

const MORE: Array<{ id: ScreenId | WindowId; title: string; hint: string; icon: IconName; dash?: boolean }> = [
{ id: "commune-new", title: "Create a commune", hint: "A home, a studio, or a trip", icon: "plus", dash: true },
{ id: "communes", title: "Places", hint: "Switch house, studio, or trip", icon: "hub" },
{ id: "search", title: "Search", hint: "Find expenses, notes and jobs across your communes", icon: "search" },
{ id: "notes", title: "Notes", hint: "Shared details to remember", icon: "notes" },
{ id: "funds", title: "Funds", hint: "Contributions and commitments", icon: "funds" },
{ id: "recurring", title: "Recurring", hint: "Regular bills, recorded after payment", icon: "refresh" },
{ id: "templates", title: "Saved splits", hint: "Participants, percentages and share weights", icon: "templates" },
{ id: "operations", title: "Operations", hint: "What鈥檚 late, what鈥檚 yours, then the week", icon: "operations" },
{ id: "members", title: "Members", hint: "Who鈥檚 here this cycle", icon: "members" },
{ id: "settings", title: "You & settings", hint: "Profile and preferences", icon: "settings" },
];

export function MoreSheet() {
const { setMoreOpen, go } = useApp();
const { moreOpen, setMoreOpen, screen } = useApp();
useEffect(() => {
if (!moreOpen) return;
const onKey = (event: KeyboardEvent) => {
// An open commune list inside the drawer closes first.
if (event.key === "Escape" && !document.querySelector(".a-drawer .a-hub-menu")) setMoreOpen(false);
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [moreOpen, setMoreOpen]);
if (!moreOpen) return null;
return (
<>
<div className="a-sheet-scrim" onClick={() => setMoreOpen(false)} />
<div className="a-more" role="dialog" aria-label="More">
<div className="a-more-head">
<div>
<p className="a-more-kicker">More</p>
<p className="a-more-lede">Notes, funds, and the rest of your commune. The four tabs stay the spine.</p>
</div>
<button className="a-more-x" type="button" aria-label="Close More" onClick={() => setMoreOpen(false)}>
<Icon name="close" size={16} />
</button>
</div>
{MORE.map((row) => (
<button key={row.id} type="button" onClick={() => go(row.id)}>
<span className={cx("a-more-ic", row.dash && "a-more-ic-dash")} aria-hidden="true">
<Icon name={row.icon} />
</span>
<span className="a-more-copy">
<strong>{row.title}</strong>
<span>{row.hint}</span>
</span>
<Icon name="chevron" size={16} />
</button>
))}
</div>
<Sidebar screen={screen} drawer />
</>
);
}
Loading