Let the owner manage categories, and give the menus a shelf
Categories were baked into config/<site>.site.json, so adding one meant a rebuild and a redeploy. They are now data: data/sections.json, seeded from the config the first time, and managed in the admin (add, rename, reorder, remove). A category's web address is fixed when it is created because that is what a QR code points at, and a category cannot be removed while it still holds menus. The landing and the top bar were the ugliest parts. The landing was a column of hairlines with tiny meta — a broadsheet, and the paper that makes this app worth looking at was nowhere on it. It is now a shelf: each category is a row whose right edge is a sliver of that menu's first page, bleeding off the screen, with the shelf edge in brass and the name in heavy tight Archivo. The bar was a row of teal pills that would not survive more categories: it now names the menu you are reading and opens a sheet listing the rest, each with its own paper sliver, and the current one is the only teal thing on screen. The sheet is a link to the landing page, so navigating still works without the script. Also fixes the category manager's own layout, which I shipped unstyled, and the markup contract the route tests described.
This commit is contained in:
1 parent
7bf67cc299
commit
c31b73d553
25 files changed
+1225
-190
No files matched your search
+101
-29
@@ -24,6 +24,23 @@ export function mediaUrls(page: PageRecord): MediaUrls {
|
||||
return { webp, jpeg, thumb: `/media/thumb-${page.id}.${page.hash}.webp`, view: webp };
|
||||
}
|
||||
|
||||
export function menuLinks(
|
||||
sections: readonly SectionConfig[],
|
||||
current: ReadonlyMap<string, StoredSet>,
|
||||
): MenuLink[] {
|
||||
return sections
|
||||
.filter((section) => current.has(section.slug))
|
||||
.map((section) => {
|
||||
const set = current.get(section.slug)!;
|
||||
return { section, cover: coverUrl(set), pages: set.manifest.pages.length };
|
||||
});
|
||||
}
|
||||
|
||||
export function coverUrl(set: StoredSet): string | null {
|
||||
const first = set.manifest.pages[0];
|
||||
return first === undefined ? null : mediaUrls(first).thumb;
|
||||
}
|
||||
|
||||
export function pageCountLabel(count: number): string {
|
||||
return count === 1 ? '1 page' : `${count} pages`;
|
||||
}
|
||||
@@ -47,22 +64,60 @@ export function setDateLabel(manifest: SetManifest): string {
|
||||
return 'not published';
|
||||
}
|
||||
|
||||
function renderRail(config: SiteConfig, currentSection: string, published: ReadonlySet<string>): Html {
|
||||
return html`<nav class="rail" aria-label="Menus">
|
||||
${join(
|
||||
config.sections
|
||||
.filter((section) => published.has(section.slug))
|
||||
.map(
|
||||
(section) =>
|
||||
html`<a
|
||||
class="rail__item${section.slug === currentSection ? ' is-current' : ''}"
|
||||
href="/${section.slug}/"
|
||||
${section.slug === currentSection ? html`aria-current="page"` : ''}
|
||||
>${section.shortLabel}</a
|
||||
>`,
|
||||
),
|
||||
)}
|
||||
</nav>`;
|
||||
interface SheetEntry {
|
||||
section: SectionConfig;
|
||||
current: boolean;
|
||||
cover: string | null;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The bar names the menu you are reading and opens a sheet with the rest. A row
|
||||
* of tabs was unreadable at six categories and would not survive more.
|
||||
* The switcher is a link to the landing page, so it still works without the
|
||||
* script; the script opens the sheet instead of navigating.
|
||||
*/
|
||||
function renderBar(config: SiteConfig, currentLabel: string): Html {
|
||||
return html`<header class="bar">
|
||||
<a class="bar__brand" href="/" aria-label="${config.brand.name} — all menus">
|
||||
<img src="${config.brand.logo ?? '/assets/favicon.svg'}" alt="" width="24" height="24" />
|
||||
</a>
|
||||
<a class="switcher" href="/" data-switcher aria-expanded="false" aria-controls="menu-sheet">
|
||||
<span class="switcher__label">${currentLabel}</span>
|
||||
<span class="switcher__caret" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="pour" aria-hidden="true"><span class="pour__fill" data-pour></span></div>
|
||||
</header>`;
|
||||
}
|
||||
|
||||
function renderSheet(config: SiteConfig, entries: readonly SheetEntry[]): Html {
|
||||
return html`<section class="sheet" id="menu-sheet" data-sheet hidden aria-label="Menus">
|
||||
<div class="sheet__inner">
|
||||
<p class="sheet__title">${config.brand.name}</p>
|
||||
<ul class="sheet__list">
|
||||
${join(
|
||||
entries.map(
|
||||
(entry) => html`<li>
|
||||
<a
|
||||
class="sheet__item${entry.current ? ' is-current' : ''}"
|
||||
href="/${entry.section.slug}/"
|
||||
${entry.current ? html`aria-current="page"` : ''}
|
||||
>
|
||||
<span class="sheet__peek" aria-hidden="true">
|
||||
${entry.cover === null
|
||||
? ''
|
||||
: html`<img src="${entry.cover}" alt="" loading="lazy" decoding="async" />`}
|
||||
</span>
|
||||
<span class="sheet__name">${entry.section.label}</span>
|
||||
<span class="sheet__state">${entry.current ? 'Showing' : pageCountLabel(entry.pages)}</span>
|
||||
</a>
|
||||
</li>`,
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
<button class="sheet__close" type="button" data-sheet-close>Close</button>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function renderPages(section: SectionConfig, pages: readonly PageRecord[]): Html {
|
||||
@@ -125,10 +180,17 @@ function renderOlderMenus(section: SectionConfig, archives: readonly StoredSet[]
|
||||
</details>`;
|
||||
}
|
||||
|
||||
export interface MenuLink {
|
||||
section: SectionConfig;
|
||||
/** Top of the first Page, for the paper sliver. */
|
||||
cover: string | null;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface LandingInput {
|
||||
config: SiteConfig;
|
||||
/** Sections that have a Current Menu Set, in config order. */
|
||||
live: readonly { section: SectionConfig; set: StoredSet }[];
|
||||
/** Sections that have a Current Menu Set, in the order customers should see. */
|
||||
live: readonly MenuLink[];
|
||||
}
|
||||
|
||||
export function renderLanding(input: LandingInput): Html {
|
||||
@@ -136,12 +198,17 @@ export function renderLanding(input: LandingInput): Html {
|
||||
const rows =
|
||||
input.live.length === 0
|
||||
? html`<p class="empty">The menus are being updated. Please ask a member of staff.</p>`
|
||||
: html`<nav class="rows" aria-label="Menus">
|
||||
: html`<nav class="shelves" aria-label="Menus">
|
||||
${join(
|
||||
input.live.map(
|
||||
({ section, set }) => html`<a class="row" href="/${section.slug}/">
|
||||
<span class="row__label">${section.label}</span>
|
||||
<span class="row__meta">${pageCountLabel(set.manifest.pages.length)}</span>
|
||||
(entry) => html`<a class="shelf" href="/${entry.section.slug}/">
|
||||
${entry.cover === null
|
||||
? ''
|
||||
: html`<span class="shelf__peek" aria-hidden="true"
|
||||
><img src="${entry.cover}" alt="" loading="lazy" decoding="async"
|
||||
/></span>`}
|
||||
<span class="shelf__name">${entry.section.label}</span>
|
||||
<span class="shelf__pages">${pageCountLabel(entry.pages)}</span>
|
||||
</a>`,
|
||||
),
|
||||
)}
|
||||
@@ -171,8 +238,8 @@ export interface SectionInput {
|
||||
config: SiteConfig;
|
||||
section: SectionConfig;
|
||||
set: StoredSet;
|
||||
/** Section slugs that have a Current Menu Set, for the rail. */
|
||||
published: ReadonlySet<string>;
|
||||
/** Every Section with a Current Menu Set, in order, for the switcher. */
|
||||
menu: readonly MenuLink[];
|
||||
current: boolean;
|
||||
archives: readonly StoredSet[];
|
||||
/** Draft preview for the admin: adds a banner and skips the archive fold. */
|
||||
@@ -184,11 +251,16 @@ export function renderSectionPage(input: SectionInput): Html {
|
||||
const currentHref = `/${section.slug}/`;
|
||||
const total = set.manifest.pages.length;
|
||||
|
||||
const body = html`<header class="bar">
|
||||
<a class="bar__brand" href="/">${config.brand.name}</a>
|
||||
${renderRail(config, section.slug, input.published)}
|
||||
<div class="pour" aria-hidden="true"><span class="pour__fill" data-pour></span></div>
|
||||
</header>
|
||||
const body = html`${renderBar(config, section.label)}
|
||||
${renderSheet(
|
||||
config,
|
||||
input.menu.map((entry) => ({
|
||||
section: entry.section,
|
||||
current: entry.section.slug === section.slug,
|
||||
cover: entry.cover,
|
||||
pages: entry.pages,
|
||||
})),
|
||||
)}
|
||||
${input.preview
|
||||
? html`<p class="notice notice--admin">
|
||||
Preview of a Draft — customers cannot see this yet.
|
||||
|
||||
Reference in new issue
Block a user