Add collapsible multi-column panels
This commit is contained in:
@@ -37,6 +37,7 @@ This project replaces the Excel/VBA workflow named `Catalog maker - 20` with a N
|
||||
- Prefer Tailwind utility classes and Tailwind-style component patterns for buttons, panels, dialogs and responsive layouts.
|
||||
- Whenever a reusable pattern appears, prefer Tailwind component patterns (`@layer components` with `@apply`) over handwritten legacy CSS; keep one-off layout details as utility classes where practical.
|
||||
- Whenever possible, use Tailwind UI / Tailwind Plus application components and patterns for shells, multi-column layouts, sidebars, drawers, dialogs, navigation and forms; adapt them to this project's dense catalog workflow instead of inventing a separate visual pattern.
|
||||
- Multi-column panels must use a shared collapse pattern: collapsing a panel removes its grid track, lets the adjacent workspace expand, preserves an accessible expand/collapse control, and stacks predictably on mobile.
|
||||
- Use Playwright to inspect the rendered result after every visual change at desktop and narrow viewport sizes.
|
||||
|
||||
## TypeScript workflow
|
||||
|
||||
@@ -50,6 +50,9 @@ const testLocalDbButton = document.querySelector("#testLocalDbButton");
|
||||
const localDbTestResult = document.querySelector("#localDbTestResult");
|
||||
const lightContrastSelect = document.querySelector("#lightContrastSelect");
|
||||
const darkContrastSelect = document.querySelector("#darkContrastSelect");
|
||||
const catalogScreen = document.querySelector(".catalog-screen");
|
||||
const secondaryPanelToggle = document.querySelector("#secondaryPanelToggle");
|
||||
const mainWorkspaceToggle = document.querySelector("#mainWorkspaceToggle");
|
||||
const nativeFetch = window.fetch.bind(window);
|
||||
window.fetch = (input, init = {}) => {
|
||||
const url = typeof input === "string" ? input : input.url;
|
||||
@@ -164,6 +167,8 @@ loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
|
||||
findSourcesButton.addEventListener("click", findSourcesForCurrentProduct);
|
||||
getPicturesButton.addEventListener("click", getPicturesForCurrentProduct);
|
||||
parserDrawerToggle.addEventListener("click", toggleParserDrawer);
|
||||
secondaryPanelToggle.addEventListener("click", toggleSecondaryPanel);
|
||||
mainWorkspaceToggle.addEventListener("click", toggleMainWorkspace);
|
||||
sidebarOpenButton.addEventListener("click", openSidebar);
|
||||
sidebarCloseButton.addEventListener("click", closeSidebar);
|
||||
sidebarOverlay.addEventListener("click", closeSidebar);
|
||||
@@ -855,6 +860,7 @@ function openSidebar() {
|
||||
sidebarOpenButton.hidden = true;
|
||||
sidebarOpenButton.setAttribute("aria-expanded", "true");
|
||||
sidebarDrawer.setAttribute("aria-hidden", "false");
|
||||
catalogScreen.classList.remove("sidebar-collapsed");
|
||||
}
|
||||
function closeSidebar() {
|
||||
sidebarDrawer.classList.remove("is-open");
|
||||
@@ -862,6 +868,21 @@ function closeSidebar() {
|
||||
sidebarOpenButton.hidden = false;
|
||||
sidebarOpenButton.setAttribute("aria-expanded", "false");
|
||||
sidebarDrawer.setAttribute("aria-hidden", "true");
|
||||
catalogScreen.classList.add("sidebar-collapsed");
|
||||
}
|
||||
function toggleSecondaryPanel() {
|
||||
const isCollapsed = catalogScreen.classList.toggle("secondary-collapsed");
|
||||
secondaryPanelToggle.setAttribute("aria-expanded", String(!isCollapsed));
|
||||
secondaryPanelToggle.setAttribute("aria-label", isCollapsed ? "Expand product details" : "Collapse product details");
|
||||
secondaryPanelToggle.title = isCollapsed ? "Expand product details" : "Collapse product details";
|
||||
secondaryPanelToggle.textContent = isCollapsed ? "›" : "‹";
|
||||
}
|
||||
function toggleMainWorkspace() {
|
||||
const isCollapsed = catalogScreen.classList.toggle("workspace-collapsed");
|
||||
mainWorkspaceToggle.setAttribute("aria-expanded", String(!isCollapsed));
|
||||
mainWorkspaceToggle.setAttribute("aria-label", isCollapsed ? "Expand main workspace" : "Collapse main workspace");
|
||||
mainWorkspaceToggle.title = isCollapsed ? "Expand main workspace" : "Collapse main workspace";
|
||||
mainWorkspaceToggle.textContent = isCollapsed ? "‹" : "›";
|
||||
}
|
||||
function loadTheme() {
|
||||
const isDark = localStorage.getItem("catalog-maker:theme") === "dark";
|
||||
|
||||
@@ -50,6 +50,9 @@ const testLocalDbButton = document.querySelector("#testLocalDbButton");
|
||||
const localDbTestResult = document.querySelector("#localDbTestResult");
|
||||
const lightContrastSelect = document.querySelector("#lightContrastSelect");
|
||||
const darkContrastSelect = document.querySelector("#darkContrastSelect");
|
||||
const catalogScreen = document.querySelector(".catalog-screen");
|
||||
const secondaryPanelToggle = document.querySelector("#secondaryPanelToggle");
|
||||
const mainWorkspaceToggle = document.querySelector("#mainWorkspaceToggle");
|
||||
|
||||
const nativeFetch = window.fetch.bind(window);
|
||||
window.fetch = (input, init = {}) => {
|
||||
@@ -170,6 +173,8 @@ loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
|
||||
findSourcesButton.addEventListener("click", findSourcesForCurrentProduct);
|
||||
getPicturesButton.addEventListener("click", getPicturesForCurrentProduct);
|
||||
parserDrawerToggle.addEventListener("click", toggleParserDrawer);
|
||||
secondaryPanelToggle.addEventListener("click", toggleSecondaryPanel);
|
||||
mainWorkspaceToggle.addEventListener("click", toggleMainWorkspace);
|
||||
sidebarOpenButton.addEventListener("click", openSidebar);
|
||||
sidebarCloseButton.addEventListener("click", closeSidebar);
|
||||
sidebarOverlay.addEventListener("click", closeSidebar);
|
||||
@@ -942,6 +947,7 @@ function openSidebar() {
|
||||
sidebarOpenButton.hidden = true;
|
||||
sidebarOpenButton.setAttribute("aria-expanded", "true");
|
||||
sidebarDrawer.setAttribute("aria-hidden", "false");
|
||||
catalogScreen.classList.remove("sidebar-collapsed");
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
@@ -950,6 +956,23 @@ function closeSidebar() {
|
||||
sidebarOpenButton.hidden = false;
|
||||
sidebarOpenButton.setAttribute("aria-expanded", "false");
|
||||
sidebarDrawer.setAttribute("aria-hidden", "true");
|
||||
catalogScreen.classList.add("sidebar-collapsed");
|
||||
}
|
||||
|
||||
function toggleSecondaryPanel() {
|
||||
const isCollapsed = catalogScreen.classList.toggle("secondary-collapsed");
|
||||
secondaryPanelToggle.setAttribute("aria-expanded", String(!isCollapsed));
|
||||
secondaryPanelToggle.setAttribute("aria-label", isCollapsed ? "Expand product details" : "Collapse product details");
|
||||
secondaryPanelToggle.title = isCollapsed ? "Expand product details" : "Collapse product details";
|
||||
secondaryPanelToggle.textContent = isCollapsed ? "›" : "‹";
|
||||
}
|
||||
|
||||
function toggleMainWorkspace() {
|
||||
const isCollapsed = catalogScreen.classList.toggle("workspace-collapsed");
|
||||
mainWorkspaceToggle.setAttribute("aria-expanded", String(!isCollapsed));
|
||||
mainWorkspaceToggle.setAttribute("aria-label", isCollapsed ? "Expand main workspace" : "Collapse main workspace");
|
||||
mainWorkspaceToggle.title = isCollapsed ? "Expand main workspace" : "Collapse main workspace";
|
||||
mainWorkspaceToggle.textContent = isCollapsed ? "‹" : "›";
|
||||
}
|
||||
|
||||
function loadTheme() {
|
||||
|
||||
+9
-1
@@ -97,12 +97,20 @@
|
||||
</aside>
|
||||
|
||||
<section class="product-panel secondary-panel" aria-label="Product details">
|
||||
<div class="column-header">
|
||||
<span>Product details</span>
|
||||
<button class="column-toggle" id="secondaryPanelToggle" type="button" aria-expanded="true" aria-controls="fieldGrid" aria-label="Collapse product details" title="Collapse product details">‹</button>
|
||||
</div>
|
||||
<div class="field-grid" id="fieldGrid"></div>
|
||||
</section>
|
||||
|
||||
<section class="data-panel main-workspace" aria-label="Catalog workspace">
|
||||
<div class="column-header workspace-header">
|
||||
<span>Main workspace</span>
|
||||
<button class="column-toggle" id="mainWorkspaceToggle" type="button" aria-expanded="true" aria-controls="variantGrid" aria-label="Collapse main workspace" title="Collapse main workspace">›</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<div class="variant-grid" role="table" aria-label="Product combinations">
|
||||
<div class="variant-grid" id="variantGrid" role="table" aria-label="Product combinations">
|
||||
<div class="variant-header" id="variantHeader" role="row"></div>
|
||||
<div class="variant-rows" id="variantRows" role="rowgroup">
|
||||
<div class="empty-row" id="emptyTableMessage" role="row">Select manufacturer to load data.</div>
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+21
-2
@@ -41,7 +41,10 @@
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.catalog-screen { @apply relative grid min-h-screen grid-cols-[minmax(240px,292px)_minmax(0,1fr)] bg-catalog-surface pl-72; }
|
||||
.catalog-screen { @apply relative grid min-h-screen bg-catalog-surface pl-72; --secondary-track: minmax(240px,292px); --workspace-track: minmax(0,1fr); grid-template-columns: var(--secondary-track) var(--workspace-track); }
|
||||
.catalog-screen.sidebar-collapsed { @apply pl-0; }
|
||||
.catalog-screen.secondary-collapsed { --secondary-track: 44px; }
|
||||
.catalog-screen.workspace-collapsed { --workspace-track: 44px; }
|
||||
.sidebar-drawer { @apply fixed inset-y-0 left-0 z-40 flex w-72 min-h-screen flex-col gap-1.5 overflow-y-auto bg-slate-900 p-1.5 text-[11px] text-slate-200 shadow-xl transition-transform duration-200 ease-out; }
|
||||
.sidebar-drawer-header { @apply flex items-center justify-between border-b border-slate-700 pb-1; }
|
||||
.sidebar-drawer-title { @apply px-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400; }
|
||||
@@ -110,7 +113,15 @@
|
||||
.mapping-source-status.is-ok { @apply bg-emerald-100 text-emerald-700; }
|
||||
.mapping-source-status.is-bad { @apply bg-red-100 text-red-700; }
|
||||
.mapping-source-status.is-muted { @apply bg-slate-200 text-slate-600; }
|
||||
.secondary-panel { @apply min-w-0 border-r border-slate-400 bg-slate-300 pt-[22px]; }
|
||||
.secondary-panel { @apply min-w-0 border-r border-slate-400 bg-slate-300; }
|
||||
.column-header { @apply flex h-7 items-center justify-between gap-1 border-b border-slate-400 px-1 text-[10px] font-semibold uppercase tracking-wide text-slate-600; }
|
||||
.column-toggle { @apply inline-flex h-5 w-5 items-center justify-center rounded border border-slate-400 bg-white text-sm leading-none text-slate-700 hover:bg-slate-200; }
|
||||
.secondary-panel .field-grid { @apply overflow-hidden; }
|
||||
.secondary-collapsed .secondary-panel .column-header { @apply h-full flex-col justify-start py-1; writing-mode: vertical-rl; }
|
||||
.secondary-collapsed .secondary-panel .field-grid { display: none; }
|
||||
.workspace-collapsed .main-workspace .workspace-header { @apply h-full flex-col justify-start py-1; writing-mode: vertical-rl; }
|
||||
.workspace-collapsed .main-workspace .workspace-header span,
|
||||
.workspace-collapsed .main-workspace .table-wrap { display: none; }
|
||||
.field-grid { @apply grid grid-cols-[132px_minmax(0,168px)] content-start; }
|
||||
.field-label, .field-value { @apply min-h-[22px] border-b border-slate-300 px-1 py-0.5 text-[11px] leading-tight; }
|
||||
.field-label { @apply bg-slate-200 text-slate-800; }
|
||||
@@ -183,6 +194,12 @@
|
||||
.secondary-panel { @apply order-1; }
|
||||
.main-workspace { @apply order-2; }
|
||||
.table-wrap { @apply max-h-[70vh]; }
|
||||
.column-header { @apply h-7; writing-mode: horizontal-tb; }
|
||||
.secondary-collapsed .secondary-panel .column-header,
|
||||
.workspace-collapsed .main-workspace .workspace-header { @apply h-7 flex-row py-0; writing-mode: horizontal-tb; }
|
||||
.secondary-collapsed .secondary-panel .column-header span,
|
||||
.workspace-collapsed .main-workspace .workspace-header span { display: inline; }
|
||||
.workspace-collapsed .main-workspace .table-wrap { display: none; }
|
||||
.variant-grid { @apply min-w-0; }
|
||||
.variant-header { @apply hidden; }
|
||||
.variant-row { @apply grid grid-cols-2 gap-x-2 border-b border-slate-300 p-2; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
@@ -222,6 +239,7 @@
|
||||
html:not(.dark) .app-status { @apply border-slate-300 bg-white text-slate-700; }
|
||||
html:not(.dark) .manufacturer-trigger { @apply border-slate-300 bg-white text-slate-700; }
|
||||
html:not(.dark) .manufacturer-trigger:hover { @apply border-slate-400 bg-slate-200; }
|
||||
html:not(.dark) .column-header { @apply border-slate-300 text-slate-600; }
|
||||
html.contrast-light-high:not(.dark) .control-panel { @apply bg-slate-200 text-slate-900; }
|
||||
html.contrast-light-high:not(.dark) .action-button,
|
||||
html.contrast-light-high:not(.dark) .arrow-button,
|
||||
@@ -250,6 +268,7 @@
|
||||
html.dark .manufacturer-search { @apply border-slate-600 bg-slate-800 text-slate-100; }
|
||||
html.dark .manufacturer-option { @apply text-slate-200 hover:bg-slate-800; }
|
||||
html.dark .manufacturer-option[aria-selected="true"] { @apply bg-slate-700 text-white; }
|
||||
html.dark .column-header { @apply border-slate-700 text-slate-300; }
|
||||
html.contrast-dark-high.dark .control-panel { @apply bg-slate-950 text-white; }
|
||||
html.contrast-dark-high.dark .action-button,
|
||||
html.contrast-dark-high.dark .arrow-button,
|
||||
|
||||
@@ -57,6 +57,22 @@ test("left controls open in a responsive sidebar drawer", async ({ page }) => {
|
||||
await expect(page.locator("#sidebarOverlay")).toBeHidden();
|
||||
});
|
||||
|
||||
test("workspace columns collapse and expand without leaving a gap", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("#sidebarCloseButton").click();
|
||||
await expect(page.locator(".catalog-screen")).toHaveClass(/sidebar-collapsed/);
|
||||
|
||||
await page.locator("#secondaryPanelToggle").click();
|
||||
await expect(page.locator(".catalog-screen")).toHaveClass(/secondary-collapsed/);
|
||||
await page.locator("#mainWorkspaceToggle").click();
|
||||
await expect(page.locator(".catalog-screen")).toHaveClass(/workspace-collapsed/);
|
||||
|
||||
await page.locator("#secondaryPanelToggle").click();
|
||||
await page.locator("#mainWorkspaceToggle").click();
|
||||
await expect(page.locator(".catalog-screen")).not.toHaveClass(/secondary-collapsed/);
|
||||
await expect(page.locator(".catalog-screen")).not.toHaveClass(/workspace-collapsed/);
|
||||
});
|
||||
|
||||
test("theme toggle switches and persists dark mode", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
|
||||
Reference in New Issue
Block a user