Skip to content

[codex] Improve mobile catalog UX#27

Merged
philosophercode merged 8 commits into
mainfrom
philosophercode/mobile-chat-filters-ui
Jun 2, 2026
Merged

[codex] Improve mobile catalog UX#27
philosophercode merged 8 commits into
mainfrom
philosophercode/mobile-chat-filters-ui

Conversation

@philosophercode

Copy link
Copy Markdown
Owner

Summary

  • Reworks the gallery filter UI from large chip rows into compact dropdown facets, with mobile filters collapsed behind a Filters toggle.
  • Tightens the mobile navigation into brand + compact language/theme buttons plus tab navigation.
  • Improves mobile tool detail readability, makes the chat sheet full-screen on phones, reduces FAB obstruction, and lets the mobile status strip scroll away.
  • Serves local tool images unoptimized so public /tool-images assets render reliably in cards, tables, and detail heroes.

Why

Mobile users were losing too much viewport height to filters and chrome, and the chat/detail surfaces did not use the phone screen efficiently. During screenshot review, the desktop laser hero image also failed through the Next image optimizer at larger generated sizes.

Validation

  • npm run lint
  • npm run typecheck
  • npm run build
  • Screenshot audit saved under .context/screenshots/audit/ covering gallery, filters open/closed, Form 4 detail, Trotec laser detail, Projects/About samples, and chat open/sent states.

Notes

Chat UI behavior was verified locally, but live assistant responses require ANTHROPIC_API_KEY; without it the local API returns the expected error state.

@vercel

vercel Bot commented May 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
makerlab-tools Ready Ready Preview, Comment Jun 2, 2026 6:00am
makerlab-tools-v5 Ready Ready Preview, Comment Jun 2, 2026 6:00am

…table table

- Grid: 2 cols mobile → 3/4/5 at sm/lg/xl, smaller cards
- Category + Materials are multi-select chips; remove Training filter; Location stays single-select
- Clear button inline on desktop
- Mobile table renders as labeled cards instead of a broken stack
- Sortable column headers (Tool/Category/Zone/Training)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dense table degrades to an oversized card stack on phones. Below 860px
the gallery now always renders the grid and the Grid/Table switcher is hidden.
The table (with sortable headers) is unchanged on desktop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Materials now collapses behind an "All materials / N selected" toggle that
opens a panel grouping the chips by type (Plastics & Polymers, Wood, Metal,
Other). Clicking a type heading selects or clears the whole group; individual
chips still multi-select. Taxonomy is defined in MATERIAL_GROUPS since the
catalog has no material-type field; unknown values fall into "Other".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Category now matches Materials/Location: a collapsible "All categories / N
selected" dropdown instead of an always-expanded chip wall. All three filters
are now consistent dropdowns on desktop, and on mobile they sit behind the
Filters toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Filters now sit side by side and wrap only when they don't fit; open dropdown
panels float (absolute) so opening one doesn't shift the row. On mobile they
keep stacking with in-flow panels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Category and Materials dropdowns now open a vertical menu of option rows with
checkbox affordances (filled when selected), hover highlight, scrolling, and
grouped section headers for materials — instead of a chip cloud. Added
click-outside and Escape to close, like a native <select>. Multi-select is
preserved; no UI library added (would clash with the custom brutalist styling).
Removed the now-unused chip CSS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… table

- Move Clear into the same toolbar as the Grid/Table switcher so they sit on
  one line (centered together); remove the old standalone Clear + dead CSS.
- Drop the dropdown toggle's max-width so Category/Materials/Location are all
  equal full width (they were capped while Location filled 100%).
- Restore the table view on mobile: instead of forcing the grid, keep the
  desktop columnar table and let it scroll horizontally; un-hide the toggle.
- Mobile toolbar wraps: Filters on its own row, Clear + Grid/Table below.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@philosophercode philosophercode marked this pull request as ready for review June 2, 2026 06:01
Copilot AI review requested due to automatic review settings June 2, 2026 06:01
@philosophercode philosophercode merged commit f92e4ee into main Jun 2, 2026
4 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Reworks the gallery and chrome for mobile: facet chips become compact dropdowns hidden behind a Filters toggle on phones, the top nav collapses into brand + tab nav + compact utility buttons, tool detail and chat surfaces use phone real estate more efficiently, and local images in cards/table/hero are served unoptimized to avoid Next image optimizer failures.

Changes:

  • GalleryShell adopts multi-select Category and Materials dropdowns, a Location <select>, a mobile-only Filters toggle with active-count, outside-click/Escape close behavior, and sortable table column headers.
  • Heavy globals.css rewrite for the filter console, two-column mobile grid, scrollable table, full-screen mobile chat sheet, and tightened mobile nav/status strip.
  • Local images in ToolCard / DetailShell / gallery table thumb pass unoptimized to bypass the Next image optimizer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
v5/src/components/GalleryShell.tsx New dropdown facets, multi-select state, sort state, outside-click handling, table sort headers
v5/src/components/ToolCard.tsx Adds unoptimized to the card image
v5/src/components/DetailShell.tsx Adds unoptimized to the hero image
v5/src/styles/globals.css Adds filter-dropdown styles, sortable header styles, mobile grid/nav/chat/detail refinements

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +247 to +253
Filters{activeFilterCount > 0 ? ` (${activeFilterCount})` : ""}
</button>

<div className="filter-group">
<span>{t("location")}</span>
<div className="chip-row">
{locations.map((locationName) => (
<button
className={location === locationName ? "chip chip-active" : "chip"}
key={locationName}
type="button"
aria-pressed={location === locationName}
onClick={() =>
setLocation((selected) => (selected === locationName ? null : locationName))
}
>
{locationName}
</button>
))}
</div>
</div>
{activeFilterCount > 0 ? (
<button className="filter-clear" type="button" onClick={clearFilters}>
Clear {activeFilterCount}
</button>
Comment on lines +288 to +384
<span>
{selectedCategories.length > 0 ? `${selectedCategories.length} selected` : "All categories"}
</span>
<span className="filter-dropdown-caret" aria-hidden="true">
{categoryOpen ? "▲" : "▼"}
</span>
</button>

{categoryOpen ? (
<div className="filter-dropdown-panel" role="listbox" aria-multiselectable="true">
{categories.map((categoryName) => {
const active = selectedCategories.includes(categoryName);
return (
<button
key={categoryName}
type="button"
role="option"
aria-selected={active}
className="filter-option"
onClick={() => toggleCategory(categoryName)}
>
<span className="filter-option-check" aria-hidden="true">{active ? "✓" : ""}</span>
<span>{categoryName}</span>
</button>
);
})}
</div>
) : null}
</div>

<div className="filter-group filter-dropdown-group" role="group" aria-label={t("materials")} ref={materialsRef}>
<span>{t("materials")}</span>
<button
type="button"
className={
materialsOpen || selectedMaterials.length > 0
? "filter-dropdown-toggle is-active"
: "filter-dropdown-toggle"
}
aria-expanded={materialsOpen}
onClick={() => setMaterialsOpen((open) => !open)}
>
<span>
{selectedMaterials.length > 0 ? `${selectedMaterials.length} selected` : "All materials"}
</span>
<span className="filter-dropdown-caret" aria-hidden="true">
{materialsOpen ? "▲" : "▼"}
</span>
</button>

{materialsOpen ? (
<div className="filter-dropdown-panel" role="listbox" aria-multiselectable="true">
{materialGroups.map((group) => {
const allSelected = group.items.every((value) => selectedMaterials.includes(value));
return (
<div className="filter-menu-section" key={group.label}>
<button
type="button"
className="filter-menu-section-head"
aria-pressed={allSelected}
onClick={() => toggleMaterialGroup(group.items, allSelected)}
>
{group.label}
</button>
{group.items.map((materialName) => {
const active = selectedMaterials.includes(materialName);
return (
<button
key={materialName}
type="button"
role="option"
aria-selected={active}
className="filter-option"
onClick={() => toggleMaterial(materialName)}
>
<span className="filter-option-check" aria-hidden="true">{active ? "✓" : ""}</span>
<span>{materialName}</span>
</button>
);
})}
</div>
);
})}
</div>
) : null}
</div>

<label className="filter-group filter-select-group">
<span>{t("location")}</span>
<select value={location ?? ""} onChange={(event) => setLocation(event.target.value || null)}>
<option value="">All</option>
{locations.map((locationName) => (
<option key={locationName} value={locationName}>
{locationName}
</option>
))}
</select>
Comment on lines +47 to +57
const MATERIAL_GROUPS: ReadonlyArray<{ label: string; values: string[] }> = [
{
label: "Plastics & Polymers",
values: ["ABS", "Acrylic", "Composite", "Nylon", "PETG", "PLA", "Plastic", "Polycarbonate", "PVC", "Resin", "TPU", "Vinyl"],
},
{ label: "Wood", values: ["Hardwood", "Softwood", "Plywood", "MDF", "Veneer", "Laminate", "Wood"] },
{ label: "Metal", values: ["Aluminum", "Brass", "Copper", "Steel"] },
{ label: "Other", values: ["Cardboard", "Ceramic", "Fabric", "Foam", "Glass", "Leather", "Paper", "Rubber"] },
];

const OTHER_GROUP_LABEL = "Other";
Comment on lines +62 to 66
// Category and materials are multi-select facets (OR within each facet);
// location stays single-select.
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([]);
const [location, setLocation] = useState<string | null>(null);

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1de6e4b075

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

aria-controls="gallery-filter-controls"
onClick={() => setFiltersOpen((open) => !open)}
>
Filters{activeFilterCount > 0 ? ` (${activeFilterCount})` : ""}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Localize the new filter labels

In non-English locales, the newly added filter toolbar now renders literal English strings such as Filters, and the same pattern is used for Clear, selected, All categories, All materials, and the material group headings. Since this component otherwise uses next-intl, these labels bypass the message files and leave a prominent part of the mobile filtering UI untranslated for every supported locale.

Useful? React with 👍 / 👎.

Comment on lines +169 to +172
return [...searched].sort((a, b) => {
const left = String(a[sort.key] ?? "").toLowerCase();
const right = String(b[sort.key] ?? "").toLowerCase();
const comparison = left.localeCompare(right);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rank training levels explicitly when sorting

When users click the Training column, the generic string comparison sorts the ordinal training levels alphabetically, so ascending order becomes Advanced, Beginner, Intermediate instead of easiest-to-hardest. Since trainingLevel is a fixed enum, this makes the new sortable table misleading for users looking for tools by training requirement.

Useful? React with 👍 / 👎.

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.

2 participants