fix(recipe-editor): allow decimal input for quantities and percentages; fix workspace filter alignment and mobile bottom-sheet modal

This commit is contained in:
2026-08-18 16:58:45 -05:00
parent 7c6563deda
commit 1be6be2be8
5 changed files with 551 additions and 54 deletions
+5 -5
View File
@@ -214,10 +214,10 @@ const tabs:Array<{type:string;label:string;count:number;kind:"recipe"|"ingredien
{type:"inventory",label:"Inventory",count:inventoryCounts?.c ?? 0,kind:"inventory",href:"/app/inventory/"},
];
const allSearchResults=normalizedQuery ? [
...recipes.filter((item)=>`${item.title} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && (selectedTags.length === 0 || parseItemTags(item.tags_json, item.categories_json).some(t => selectedTags.includes(t))) && (selectedIngredients.length === 0 || (recipeIngredientsMap.get(item.id) ?? new Set()).some(id => selectedIngredients.includes(id)))).map((item)=>({id:item.id,kind:"recipe" as const,label:"Recipe",name:item.title,detail:`${item.yield_quantity} ${item.yield_unit_id}`,href:`/app/recipes/${item.id}/`})),
...ingredients.filter((item)=>`${item.name} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && (selectedTags.length === 0 || parseItemTags(item.tags_json, item.categories_json).some(t => selectedTags.includes(t)))).map((item)=>({id:item.id,kind:"ingredient" as const,label:"Ingredient",name:titleCase(item.name),detail:`Used in ${item.recipe_count} ${item.recipe_count===1?"recipe":"recipes"}`,href:`/app/ingredients/${item.id}/`})),
...books.filter((item)=>`${item.name} ${item.description??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && selectedTags.length === 0).map((item)=>({id:item.id,kind:"book" as const,label:"Recipe book",name:item.name,detail:`${item.recipe_count} ${item.recipe_count===1?"recipe":"recipes"}`,href:`/app/recipe-books/${item.id}/`})),
...purchases.filter((item)=>`${item.name} ${item.ingredient_name} ${item.supplier_id??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && selectedTags.length === 0).map((item)=>({id:item.id,kind:"purchase" as const,label:"Purchase item",name:item.name,detail:titleCase(item.ingredient_name),href:`/app/ingredients/${item.ingredient_id}/#costs`})),
...recipes.filter((item)=>`${item.title} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && (selectedTags.length === 0 || parseItemTags(item.tags_json, item.categories_json).some(t => selectedTags.includes(t))) && (selectedIngredients.length === 0 || Array.from(recipeIngredientsMap.get(item.id) ?? []).some((id: string) => selectedIngredients.includes(id)))).map((item)=>({id:item.id,kind:"recipe" as const,label:"Recipe",name:item.title,detail:`${item.yield_quantity} ${item.yield_unit_id}`,href:`/app/recipes/${item.id}/`})),
...ingredients.filter((item)=>`${item.name} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && (selectedTags.length === 0 || parseItemTags(item.tags_json, item.categories_json).some(t => selectedTags.includes(t)))).map((item)=>({id:item.id,kind:"ingredient" as const,label:"Ingredient",name:titleCase(item.name),detail:`Used in ${item.recipe_count} ${item.recipe_count===1?"recipe":"recipes"}`,href:`/app/ingredients/${item.id}/`})),
...books.filter((item)=>`${item.name} ${item.description??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && selectedTags.length === 0).map((item)=>({id:item.id,kind:"book" as const,label:"Recipe book",name:item.name,detail:`${item.recipe_count} ${item.recipe_count===1?"recipe":"recipes"}`,href:`/app/recipe-books/${item.id}/`})),
...purchases.filter((item)=>`${item.name} ${item.ingredient_name} ${item.supplier_id??""} ${item.id}`.toLocaleLowerCase().includes(normalizedQuery) && selectedTags.length === 0).map((item)=>({id:item.id,kind:"purchase" as const,label:"Purchase item",name:item.name,detail:titleCase(item.ingredient_name),href:`/app/ingredients/${item.ingredient_id}/#costs`})),
].sort((a,b)=>a.name.localeCompare(b.name)):[];
const searchResults=filteringSearchTypes?allSearchResults.filter((result)=>selectedSearchTypes.includes(result.kind)):allSearchResults;
const searchRows=searchResults.map(({id,kind,name,href,label,detail})=>({id,name,href,kind,detail:`${label} · ${detail}`}));
@@ -255,7 +255,7 @@ const purchaseRows=purchases.map(item=>({id:item.id,name:item.name,href:`/app/in
{tabs.map((tab)=><a class:list={{active:type===tab.type}} href={tab.href ?? `/app/?type=${tab.type}`}><span class={`workspace-pill-icon ${tab.kind}`}><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d={TYPE_ICONS[tab.kind]} style={TYPE_ICON_TRANSFORMS[tab.kind]?{transform:TYPE_ICON_TRANSFORMS[tab.kind]}:undefined}/></svg></span><span>{tab.label}</span><small>{tab.count}</small></a>)}
<WorkspaceFilterBar
client:load
type={type}
type={type ?? undefined}
query={query}
recipeCounts={recipeAttentionCounts}
ingredientCounts={ingredientAttentionCounts}
+212 -23
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from "preact/hooks";
import { createPortal } from "preact/compat";
export interface FilterOption {
id: string;
@@ -72,19 +73,26 @@ export default function WorkspaceFilterBar({
const [openChipId, setOpenChipId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
// Mobile detection state
const [activeMobileCatId, setActiveMobileCatId] = useState<string | null>(null);
const [menuAlignRight, setMenuAlignRight] = useState(false);
const [chipAlignRight, setChipAlignRight] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
// Close menus when clicking outside
// Close menus when clicking outside (desktop)
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setMenuOpen(false);
}
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
const target = e.target as HTMLElement;
if (!target.closest(".filter-rule-chip-main")) {
setOpenChipId(null);
if (window.innerWidth > 980) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setMenuOpen(false);
}
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
const target = e.target as HTMLElement;
if (!target.closest(".filter-rule-chip-main")) {
setOpenChipId(null);
}
}
}
}
@@ -92,6 +100,15 @@ export default function WorkspaceFilterBar({
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleToggleMenu = () => {
if (!menuOpen && menuRef.current) {
const rect = menuRef.current.getBoundingClientRect();
setMenuAlignRight(rect.left + 280 > window.innerWidth - 16);
}
setActiveMobileCatId(null);
setMenuOpen(!menuOpen);
};
// Parse active filter parameters
const isRecipe = type === "recipe";
const isIngredient = type === "ingredient";
@@ -175,7 +192,7 @@ export default function WorkspaceFilterBar({
const categoriesWithActiveSelections = categories.filter(c => hasActiveOptions(c));
const totalActiveCount = categoriesWithActiveSelections.length;
// The chips visible on screen: categories with active selections + the currently open chip (if user just opened one from the menu)
// The chips visible on screen
const visibleCategoryIds = Array.from(
new Set([
...categoriesWithActiveSelections.map(c => c.id),
@@ -188,13 +205,18 @@ export default function WorkspaceFilterBar({
};
const handleAddCategory = (catId: string) => {
setOpenChipId(catId);
setMenuOpen(false);
if (window.innerWidth <= 980) {
setActiveMobileCatId(catId);
} else {
setOpenChipId(catId);
setMenuOpen(false);
}
setSearchTerm("");
};
const handleRemoveCategory = (catId: string) => {
if (openChipId === catId) setOpenChipId(null);
if (activeMobileCatId === catId) setActiveMobileCatId(null);
const params = new URLSearchParams(window.location.search);
const cat = categories.find(c => c.id === catId);
@@ -216,6 +238,7 @@ export default function WorkspaceFilterBar({
const handleClearAll = () => {
setOpenChipId(null);
setMenuOpen(false);
setActiveMobileCatId(null);
const params = new URLSearchParams();
if (type) params.set("type", type);
if (query) params.set("q", query);
@@ -258,6 +281,29 @@ export default function WorkspaceFilterBar({
navigateWithParams(params);
};
const handleOpenChip = (catId: string, e?: MouseEvent) => {
if (window.innerWidth <= 980) {
setActiveMobileCatId(catId);
setMenuOpen(true);
} else {
const isOpening = openChipId !== catId;
if (isOpening) {
const target = (e?.currentTarget as HTMLElement)?.closest(".filter-rule-item-group");
if (target) {
const rect = target.getBoundingClientRect();
setChipAlignRight(rect.left + 270 > window.innerWidth - 16);
}
}
setOpenChipId(openChipId === catId ? null : catId);
}
setSearchTerm("");
};
const activeMobileCat = activeMobileCatId ? categories.find(c => c.id === activeMobileCatId) : null;
const activeMobileFilteredOptions = activeMobileCat
? activeMobileCat.options.filter(opt => opt.name.toLowerCase().includes(searchTerm.toLowerCase()))
: [];
return (
<div class="workspace-filter-system">
{/* 1. Main Filter Button Trigger in Pills Row */}
@@ -265,7 +311,7 @@ export default function WorkspaceFilterBar({
<button
type="button"
class={`workspace-filter-btn ${totalActiveCount > 0 ? "active" : ""}`}
onClick={() => setMenuOpen(!menuOpen)}
onClick={handleToggleMenu}
aria-expanded={menuOpen}
aria-haspopup="menu"
>
@@ -277,9 +323,9 @@ export default function WorkspaceFilterBar({
<span>Filter{totalActiveCount > 0 ? ` (${totalActiveCount})` : ""}</span>
</button>
{/* Filter Categories Menu */}
{/* Desktop Filter Categories Menu */}
{menuOpen && (
<div class="workspace-filter-menu-popover" role="menu">
<div class={`workspace-filter-menu-popover desktop-only-filter-popover ${menuAlignRight ? "align-right" : "align-left"}`} role="menu">
<div class="filter-menu-header">
<h4>Filter</h4>
</div>
@@ -325,7 +371,153 @@ export default function WorkspaceFilterBar({
)}
</div>
{/* 2. Secondary Active Filter Rule Chips Bar */}
{/* 2. Mobile Filter & Sort Modal (Meez Parity) */}
{menuOpen && typeof document !== "undefined" && createPortal(
<div class="mobile-filter-backdrop" role="dialog" aria-modal="true" aria-label="Filter and Sort">
<div class="mobile-filter-sheet">
<header class="mobile-filter-header">
{activeMobileCat ? (
<button
type="button"
class="mobile-filter-back-btn"
onClick={() => {
setActiveMobileCatId(null);
setSearchTerm("");
}}
aria-label="Back to categories"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
</button>
) : (
<div class="mobile-filter-header-spacer" />
)}
<h3>{activeMobileCat ? activeMobileCat.label : "Filter & Sort"}</h3>
<button
type="button"
class="mobile-filter-close-btn"
onClick={() => {
setMenuOpen(false);
setActiveMobileCatId(null);
}}
aria-label="Close filter"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={ICONS.close} />
</svg>
</button>
</header>
<div class="mobile-filter-body">
{activeMobileCat ? (
/* Category Options View */
<div class="mobile-filter-category-view">
{activeMobileCat.options.length > 0 ? (
<>
<div class="filter-chip-search">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={ICONS.search} />
</svg>
<input
type="search"
placeholder={`Search ${activeMobileCat.label.toLowerCase()}`}
value={searchTerm}
onInput={e => setSearchTerm((e.target as HTMLInputElement).value)}
autoFocus
/>
</div>
<ul class="filter-chip-options-list mobile-options-list">
{activeMobileFilteredOptions.length > 0 ? (
activeMobileFilteredOptions.map(opt => (
<li key={opt.id}>
<label class="filter-chip-option-label">
<input
type="checkbox"
checked={opt.checked}
onChange={() => handleToggleOption(activeMobileCat.id, opt.id, opt.checked)}
/>
<span class="option-name">{opt.name}</span>
{typeof opt.count === "number" && (
<span class="option-count">({opt.count})</span>
)}
</label>
</li>
))
) : (
<li class="filter-chip-empty">No matching options.</li>
)}
</ul>
</>
) : (
<div class="filter-chip-empty-message">
{activeMobileCat.emptyMessage || "No additional filter options available."}
</div>
)}
</div>
) : (
/* Main Filter & Sort Menu */
<div class="mobile-filter-main-view">
<div class="mobile-filter-section-heading">
<svg viewBox="0 0 18 21" aria-hidden="true" class="mobile-section-icon">
{ICONS.filter.map((d, i) => (
<path key={i} fill="currentColor" d={d} />
))}
</svg>
<span>Filter</span>
</div>
<div class="mobile-filter-card">
{categories.map(cat => {
const isActive = hasActiveOptions(cat);
const activeOptsCount = cat.options.filter(o => o.checked).length;
return (
<button
key={cat.id}
type="button"
class={`mobile-filter-card-item ${isActive ? "active" : ""}`}
onClick={() => handleAddCategory(cat.id)}
>
<div class="filter-item-left">
<span class="filter-item-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={cat.icon} />
</svg>
</span>
<span class="filter-item-label">{cat.label}</span>
{activeOptsCount > 0 && (
<span class="mobile-active-badge">({activeOptsCount})</span>
)}
</div>
<span class="mobile-item-chevron" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" />
</svg>
</span>
</button>
);
})}
</div>
<div class="mobile-clear-card">
<button
type="button"
class="mobile-clear-btn"
disabled={totalActiveCount === 0}
onClick={handleClearAll}
>
Clear all
</button>
</div>
</div>
)}
</div>
</div>
</div>,
document.body
)}
{/* 3. Secondary Active Filter Rule Chips Bar (Horizontal Scroll on Mobile) */}
{visibleCategoryIds.length > 0 && (
<div class="filter-rules-row" role="region" aria-label="Active filters">
{visibleCategoryIds.map((catId, index) => {
@@ -340,14 +532,11 @@ export default function WorkspaceFilterBar({
<div key={catId} class="filter-rule-item-group">
{index > 0 && <span class="filter-connector-and">And</span>}
<div class={`filter-rule-chip ${isOpen ? "open" : ""}`}>
<div class={`filter-rule-chip ${isOpen ? "open" : ""}`} data-cat-id={catId}>
<button
type="button"
class="filter-rule-chip-main"
onClick={() => {
setOpenChipId(isOpen ? null : catId);
setSearchTerm("");
}}
onClick={(e) => handleOpenChip(catId, e as any)}
aria-expanded={isOpen}
>
<span class="filter-chip-icon">
@@ -375,9 +564,9 @@ export default function WorkspaceFilterBar({
</button>
</div>
{/* Dropdown Popover for Active Chip */}
{/* Dropdown Popover for Active Chip (Desktop) */}
{isOpen && (
<div class="filter-chip-popover" ref={popoverRef}>
<div class={`filter-chip-popover desktop-only-filter-popover ${chipAlignRight ? "align-right" : "align-left"}`} ref={popoverRef}>
{cat.options.length > 0 ? (
<>
<div class="filter-chip-search">
@@ -398,7 +587,7 @@ export default function WorkspaceFilterBar({
<li key={opt.id}>
<label class="filter-chip-option-label">
<input
type="checkbox"
type="checkbox"
checked={opt.checked}
onChange={() => handleToggleOption(catId, opt.id, opt.checked)}
/>
+54 -6
View File
@@ -1,3 +1,4 @@
import { useEffect, useRef } from "preact/hooks";
import type { RecipeStructure } from "../../lib/database";
import type { DragState, Option, PercentMode } from "./types";
@@ -38,6 +39,12 @@ type Props = {
const normal = (value: string) =>
value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
/** Commit a number input's value, returning the parsed number or undefined. */
const commitNumericInput = (input: HTMLInputElement): number | undefined => {
const num = Number(input.value);
return Number.isFinite(num) ? num : undefined;
};
export default function RecipeItemRow({
item,
componentIndex,
@@ -67,6 +74,31 @@ export default function RecipeItemRow({
onCreatePendingIngredient,
onSetPercent,
}: Props) {
// Uncontrolled refs for numeric inputs — avoids Preact fighting
// intermediate values like "2." during decimal entry.
const qtyInputRef = useRef<HTMLInputElement>(null);
const qtyTracker = useRef(item.quantity);
useEffect(() => {
// Sync DOM value when parent changes externally (e.g. percent-driven)
if (item.quantity !== qtyTracker.current && qtyInputRef.current) {
qtyTracker.current = item.quantity;
if (document.activeElement !== qtyInputRef.current) {
qtyInputRef.current.value = String(item.quantity);
}
}
}, [item.quantity]);
const pctInputRef = useRef<HTMLInputElement>(null);
const percentDisplay = percentValue != null ? Math.round(percentValue * 1000) / 1000 : undefined;
const pctTracker = useRef(percentDisplay);
useEffect(() => {
if (percentDisplay !== pctTracker.current && pctInputRef.current) {
pctTracker.current = percentDisplay;
if (document.activeElement !== pctInputRef.current) {
pctInputRef.current.value = percentDisplay != null ? String(percentDisplay) : "";
}
}
}, [percentDisplay]);
const isPending = Boolean(
item.ingredient_id &&
pendingIngredients.some((entry) => entry.id === item.ingredient_id)
@@ -121,14 +153,21 @@ export default function RecipeItemRow({
>
<td class="quantity-column">
<input
ref={qtyInputRef}
type="number"
min="0"
step="any"
aria-label="Quantity"
value={item.quantity}
onInput={(event) =>
onUpdateQuantity(Number(event.currentTarget.value))
}
defaultValue={item.quantity}
onBlur={(event) => {
const num = commitNumericInput(event.currentTarget);
if (num != null) {
qtyTracker.current = num;
onUpdateQuantity(num);
} else {
event.currentTarget.value = String(item.quantity);
}
}}
/>
</td>
<td class="unit-column">
@@ -246,6 +285,7 @@ export default function RecipeItemRow({
<span title="A weight equivalency is required"></span>
) : (
<input
ref={pctInputRef}
aria-label={`${label} percentage`}
type="number"
min="0"
@@ -255,8 +295,16 @@ export default function RecipeItemRow({
: undefined
}
step="any"
value={Math.round(percentValue * 1000) / 1000}
onInput={(event) => onSetPercent(Number(event.currentTarget.value))}
defaultValue={percentDisplay}
onBlur={(event) => {
const num = commitNumericInput(event.currentTarget);
if (num != null) {
pctTracker.current = num;
onSetPercent(num);
} else {
event.currentTarget.value = percentDisplay != null ? String(percentDisplay) : "";
}
}}
/>
)}
</td>
+243 -8
View File
@@ -219,15 +219,17 @@ input[type="search"]::-webkit-search-results-decoration,
}
.application-body .workspace-pills > a {
min-height: 40px;
padding: 8px 16px 8px 12px;
min-height: 38px;
padding: 6px 14px 6px 10px;
color: rgba(0,0,0,0.87);
background: #fff;
border: 1px solid #ececec;
border-radius: 999px;
box-shadow: none;
font-size: 16px;
font-size: 15px;
font-weight: 400;
white-space: nowrap;
flex: 0 0 auto;
transition: background-color .15s cubic-bezier(0.4,0,0.2,1), border-color .15s cubic-bezier(0.4,0,0.2,1), color .15s ease, box-shadow .15s ease;
}
@@ -316,7 +318,8 @@ input[type="search"]::-webkit-search-results-decoration,
.application-body .workspace-filter-trigger-wrapper {
position: relative;
margin-left: auto;
display: inline-flex;
align-items: center;
flex: none;
}
@@ -361,7 +364,8 @@ input[type="search"]::-webkit-search-results-decoration,
position: absolute;
z-index: 60;
top: calc(100% + 6px);
right: 0;
left: 0;
right: auto;
width: 280px;
max-width: min(280px, calc(100vw - 32px));
max-height: calc(100vh - 120px);
@@ -375,6 +379,16 @@ input[type="search"]::-webkit-search-results-decoration,
animation: meez-menu-in 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.application-body .workspace-filter-menu-popover.align-right {
left: auto;
right: 0;
}
.application-body .workspace-filter-menu-popover.align-left {
left: 0;
right: auto;
}
.application-body .filter-menu-header {
padding: 4px 18px 8px;
}
@@ -492,6 +506,7 @@ input[type="search"]::-webkit-search-results-decoration,
flex-wrap: wrap;
gap: 8px;
width: 100%;
flex-basis: 100%;
padding: 4px 0 0;
}
@@ -620,6 +635,7 @@ input[type="search"]::-webkit-search-results-decoration,
z-index: 70;
top: calc(100% + 6px);
left: 0;
right: auto;
min-width: 260px;
max-width: min(340px, calc(100vw - 32px));
max-height: calc(100vh - 120px);
@@ -633,6 +649,16 @@ input[type="search"]::-webkit-search-results-decoration,
animation: meez-menu-in 0.18s cubic-bezier(0.4, 0, 0.2, 1);
}
.application-body .filter-chip-popover.align-right {
left: auto;
right: 0;
}
.application-body .filter-chip-popover.align-left {
left: 0;
right: auto;
}
.application-body .filter-chip-search {
display: flex;
align-items: center;
@@ -710,10 +736,219 @@ input[type="search"]::-webkit-search-results-decoration,
.application-body .filter-chip-empty-message {
padding: 16px 12px;
color: #050841;
font-size: 14px;
font-weight: 400;
text-align: left;
font-size: 13px;
font-style: italic;
text-align: center;
}
.application-body .filter-chip-empty {
padding: 12px 8px;
color: #8b93a7;
font-size: 13px;
text-align: center;
list-style: none;
}
/* Mobile Filter & Sort Modal (Meez Parity) */
.mobile-filter-backdrop {
display: none;
}
@media (max-width: 980px) {
.desktop-only-filter-popover {
display: none !important;
}
.mobile-filter-backdrop {
position: fixed;
inset: 0;
z-index: 999999;
display: flex;
flex-direction: column;
justify-content: flex-end;
background: rgba(5, 8, 65, 0.45);
backdrop-filter: blur(2px);
animation: meez-fade-in 0.2s ease;
}
@keyframes meez-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.mobile-filter-sheet {
display: flex;
flex-direction: column;
width: 100%;
max-height: 85vh;
background: #ffffff !important;
border-radius: 16px 16px 0 0;
box-shadow: 0 -8px 32px rgba(5, 8, 65, 0.18);
animation: meez-slide-up 0.25s cubic-bezier(0.16, 1, 0.3, 1);
overflow: hidden;
position: relative;
z-index: 1000000;
}
@keyframes meez-slide-up {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.mobile-filter-header {
display: grid;
grid-template-columns: 44px 1fr 44px;
align-items: center;
min-height: 56px;
padding: 0 8px;
background: #ffffff !important;
border-bottom: 1px solid #edf0f5;
}
.mobile-filter-header h3 {
margin: 0;
font-size: 17px;
font-weight: 600;
color: #050841;
text-align: center;
}
.mobile-filter-back-btn,
.mobile-filter-close-btn {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border: 0;
background: transparent;
color: #050841;
cursor: pointer;
border-radius: 50%;
}
.mobile-filter-back-btn svg,
.mobile-filter-close-btn svg {
width: 22px;
height: 22px;
}
.mobile-filter-body {
padding: 16px;
background: #ffffff !important;
overflow-y: auto;
max-height: calc(85vh - 56px);
}
.mobile-filter-main-view,
.mobile-filter-category-view {
background: #ffffff !important;
}
.mobile-filter-section-heading {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
color: #050841;
font-size: 14px;
font-weight: 600;
}
.mobile-section-icon {
width: 16px;
height: 18px;
color: #3d5df6;
}
.mobile-filter-card {
display: flex;
flex-direction: column;
background: #ffffff;
border: 1px solid #edf0f5;
border-radius: 8px;
overflow: hidden;
margin-bottom: 16px;
}
.mobile-filter-card-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 52px;
padding: 12px 16px;
border: 0;
border-bottom: 1px solid #edf0f5;
background: #ffffff;
color: #050841;
font-family: var(--meez-font-sans);
font-size: 15px;
font-weight: 400;
cursor: pointer;
}
.mobile-filter-card-item:last-child {
border-bottom: 0;
}
.mobile-filter-card-item.active {
color: #3d5df6;
font-weight: 500;
}
.mobile-active-badge {
margin-left: 6px;
color: #3d5df6;
font-size: 13px;
font-weight: 600;
}
.mobile-item-chevron {
color: #a5a9c1;
display: grid;
place-items: center;
width: 20px;
height: 20px;
}
.mobile-item-chevron svg {
width: 20px;
height: 20px;
}
.mobile-clear-card {
background: #ffffff;
border: 1px solid #edf0f5;
border-radius: 8px;
overflow: hidden;
}
.mobile-clear-btn {
display: block;
width: 100%;
min-height: 48px;
padding: 12px 16px;
border: 0;
background: transparent;
color: #3d5df6;
font-family: var(--meez-font-sans);
font-size: 14px;
font-weight: 500;
text-align: left;
cursor: pointer;
}
.mobile-clear-btn:disabled {
color: #a5a9c1;
cursor: not-allowed;
}
.mobile-options-list {
max-height: calc(85vh - 160px);
overflow-y: auto;
}
}
.application-body .entity-directory-table { overflow:visible; border:0; background:#fff; }
.application-body .entity-directory-toolbar { grid-template-columns:36px 64px minmax(0,1fr) auto; gap:8px; min-height:57px; padding:0; background:#ffffff; border-bottom:1px solid #edf0f5; }
.application-body .entity-directory-toolbar.has-selection { grid-template-columns:36px minmax(0,1fr); }
+37 -12
View File
@@ -519,7 +519,7 @@
border-radius: 6px !important;
}
/* Workspace Pills on Mobile */
/* Workspace Pills on Mobile: Smooth Horizontal Scrolling */
.application-body .directory-workspace {
width: calc(100% - 32px) !important;
margin-inline: auto !important;
@@ -528,11 +528,17 @@
.application-body .workspace-pills {
display: flex !important;
align-items: center !important;
flex-wrap: wrap !important;
overflow: visible !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
scrollbar-width: none !important;
gap: 8px !important;
padding: 0 0 16px !important;
padding: 0 0 4px !important;
margin: 0 !important;
width: 100% !important;
}
.application-body .workspace-pills::-webkit-scrollbar {
display: none !important;
}
.application-body .workspace-pills > a {
min-height: 36px !important;
@@ -541,20 +547,39 @@
flex: 0 0 auto !important;
white-space: nowrap !important;
}
.application-body .workspace-filter-system {
display: contents !important;
}
.application-body .workspace-filter-trigger-wrapper {
display: inline-flex !important;
align-items: center !important;
flex: 0 0 auto !important;
margin-left: 0 !important;
}
.application-body .workspace-filter-btn {
min-height: 36px !important;
padding: 6px 12px !important;
font-size: 14px !important;
white-space: nowrap !important;
flex: 0 0 auto !important;
}
.application-body .workspace-filter-menu-popover {
max-width: calc(100vw - 32px) !important;
max-height: calc(100vh - 120px) !important;
overflow-y: auto !important;
.application-body .filter-rules-row {
display: flex !important;
align-items: center !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
scrollbar-width: none !important;
gap: 8px !important;
padding: 2px 0 8px !important;
width: 100% !important;
flex-basis: 100% !important;
}
.application-body .filter-chip-popover {
max-width: calc(100vw - 32px) !important;
max-height: calc(100vh - 120px) !important;
overflow-y: auto !important;
.application-body .filter-rules-row::-webkit-scrollbar {
display: none !important;
}
.application-body .filter-rule-item-group {
flex: 0 0 auto !important;
}
/* Directory Table Mobile Enhancements */