@open-mercato/ui 0.5.1-develop.2975.ccbadc8198 → 0.5.1-develop.2996.ce62fd491c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/backend/AppShell.js +274 -697
  3. package/dist/backend/AppShell.js.map +3 -3
  4. package/dist/backend/CrudForm.js +1 -1
  5. package/dist/backend/CrudForm.js.map +2 -2
  6. package/dist/backend/crud/CollapsibleZoneLayout.js +23 -3
  7. package/dist/backend/crud/CollapsibleZoneLayout.js.map +2 -2
  8. package/dist/backend/section-page/SectionNav.js +10 -8
  9. package/dist/backend/section-page/SectionNav.js.map +2 -2
  10. package/dist/backend/section-page/SectionPage.js +2 -2
  11. package/dist/backend/section-page/SectionPage.js.map +2 -2
  12. package/dist/backend/sidebar/SidebarCustomizationEditor.js +1303 -0
  13. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +7 -0
  14. package/dist/backend/sidebar/customization-helpers.js +150 -0
  15. package/dist/backend/sidebar/customization-helpers.js.map +7 -0
  16. package/dist/primitives/switch.js +1 -2
  17. package/dist/primitives/switch.js.map +2 -2
  18. package/jest.setup.ts +13 -0
  19. package/package.json +3 -3
  20. package/src/backend/AppShell.tsx +245 -732
  21. package/src/backend/CrudForm.tsx +1 -1
  22. package/src/backend/__tests__/AppShell.test.tsx +1 -1
  23. package/src/backend/__tests__/CollapsibleZoneLayout.test.tsx +101 -0
  24. package/src/backend/__tests__/CrudForm.navigation.test.tsx +42 -0
  25. package/src/backend/__tests__/SidebarCustomizationEditor.test.tsx +200 -0
  26. package/src/backend/crud/CollapsibleZoneLayout.tsx +28 -3
  27. package/src/backend/section-page/SectionNav.tsx +14 -10
  28. package/src/backend/section-page/SectionPage.tsx +15 -10
  29. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +1562 -0
  30. package/src/backend/sidebar/customization-helpers.ts +203 -0
  31. package/src/primitives/switch.tsx +1 -2
@@ -4,20 +4,20 @@ import * as React from "react";
4
4
  import { createContext, useContext } from "react";
5
5
  import Link from "next/link";
6
6
  import Image from "next/image";
7
- import { ChevronUp, ChevronDown } from "lucide-react";
7
+ import { ChevronDown, Search, X } from "lucide-react";
8
8
  import { Button } from "../primitives/button.js";
9
9
  import { IconButton } from "../primitives/icon-button.js";
10
- import { Checkbox } from "../primitives/checkbox.js";
10
+ import { Input } from "../primitives/input.js";
11
11
  import { FlashMessages } from "./FlashMessages.js";
12
12
  import { QueryProvider } from "../theme/QueryProvider.js";
13
13
  import { usePathname, useSearchParams } from "next/navigation";
14
- import { apiCall } from "./utils/apiCall.js";
15
14
  import { LastOperationBanner } from "./operations/LastOperationBanner.js";
16
15
  import { ProgressTopBar } from "./progress/ProgressTopBar.js";
17
16
  import { UpgradeActionBanner } from "./upgrades/UpgradeActionBanner.js";
18
17
  import { PartialIndexBanner } from "./indexes/PartialIndexBanner.js";
19
18
  import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
20
19
  import { slugifySidebarId } from "@open-mercato/shared/modules/navigation/sidebarPreferences";
20
+ import { cloneSidebarGroups } from "./sidebar/customization-helpers.js";
21
21
  import { InjectionSpot } from "./injection/InjectionSpot.js";
22
22
  import { LEGACY_GLOBAL_MUTATION_INJECTION_SPOT_ID } from "./injection/mutationEvents.js";
23
23
  import { mergeMenuItems } from "./injection/mergeMenuItems.js";
@@ -266,7 +266,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
266
266
  const locale = useLocale();
267
267
  const { payload: chromePayload, isReady: isChromeReady, isLoading: isChromeLoading } = useBackendChrome();
268
268
  const resolvedGroups = React.useMemo(
269
- () => AppShell.cloneGroups(chromePayload?.groups ?? groups),
269
+ () => cloneSidebarGroups(chromePayload?.groups ?? groups),
270
270
  [chromePayload?.groups, groups]
271
271
  );
272
272
  const resolvedSettingsSections = chromePayload?.settingsSections ?? settingsSections;
@@ -285,19 +285,44 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
285
285
  const [openGroups, setOpenGroups] = React.useState(
286
286
  () => Object.fromEntries(resolvedGroups.map((g) => [resolveGroupKey(g), true]))
287
287
  );
288
- const [customizing, setCustomizing] = React.useState(false);
289
- const [customDraft, setCustomDraft] = React.useState(null);
290
- const [loadingPreferences, setLoadingPreferences] = React.useState(false);
291
- const [savingPreferences, setSavingPreferences] = React.useState(false);
292
- const [customizationError, setCustomizationError] = React.useState(null);
293
- const [availableRoleTargets, setAvailableRoleTargets] = React.useState([]);
294
- const [selectedRoleIds, setSelectedRoleIds] = React.useState([]);
295
- const [canApplyToRoles, setCanApplyToRoles] = React.useState(false);
296
- const originalNavRef = React.useRef(null);
297
288
  const [headerTitle, setHeaderTitle] = React.useState(currentTitle);
298
289
  const [headerBreadcrumb, setHeaderBreadcrumb] = React.useState(breadcrumb);
299
- const effectiveCollapsed = customizing ? false : collapsed;
300
- const expandedSidebarWidth = customizing ? "320px" : "240px";
290
+ const [navQuery, setNavQuery] = React.useState("");
291
+ const navQueryNorm = navQuery.trim().toLowerCase();
292
+ const navQueryActive = navQueryNorm.length > 0;
293
+ const matchesQuery = React.useCallback((label) => {
294
+ if (!navQueryActive) return true;
295
+ if (!label) return false;
296
+ return label.toLowerCase().includes(navQueryNorm);
297
+ }, [navQueryActive, navQueryNorm]);
298
+ const effectiveCollapsed = collapsed;
299
+ const expandedSidebarWidth = "240px";
300
+ const sidebarAsideRef = React.useRef(null);
301
+ const [sidebarScrollState, setSidebarScrollState] = React.useState("down");
302
+ React.useEffect(() => {
303
+ const aside = sidebarAsideRef.current;
304
+ if (!aside) return;
305
+ const target = aside.querySelector('[data-sidebar-scroll="true"]');
306
+ if (!target) return;
307
+ const update = () => {
308
+ const { scrollTop, scrollHeight, clientHeight } = target;
309
+ const canScroll = scrollHeight > clientHeight + 1;
310
+ if (!canScroll) {
311
+ setSidebarScrollState("none");
312
+ return;
313
+ }
314
+ const atBottom = scrollTop + clientHeight >= scrollHeight - 8;
315
+ setSidebarScrollState(atBottom ? "up" : "down");
316
+ };
317
+ update();
318
+ target.addEventListener("scroll", update, { passive: true });
319
+ const ro = new ResizeObserver(update);
320
+ ro.observe(target);
321
+ return () => {
322
+ target.removeEventListener("scroll", update);
323
+ ro.disconnect();
324
+ };
325
+ }, [pathname, effectiveCollapsed]);
301
326
  const injectionContext = React.useMemo(
302
327
  () => ({
303
328
  path: pathname ?? "",
@@ -346,226 +371,8 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
346
371
  }
347
372
  }, [resolvedGroups]);
348
373
  const toggleGroup = (groupId) => setOpenGroups((prev) => ({ ...prev, [groupId]: prev[groupId] === false }));
349
- const updateDraft = React.useCallback((updater) => {
350
- setCustomDraft((prev) => {
351
- if (!prev) return prev;
352
- const next = updater(prev);
353
- if (originalNavRef.current) {
354
- setNavGroups(applyCustomizationDraft(originalNavRef.current, next));
355
- }
356
- return next;
357
- });
358
- }, []);
359
- const startCustomization = React.useCallback(async () => {
360
- if (customizing || loadingPreferences) return;
361
- setCustomizationError(null);
362
- setLoadingPreferences(true);
363
- try {
364
- const baseSnapshot = filterMainSidebarGroups(AppShell.cloneGroups(navGroups));
365
- const call = await apiCall("/api/auth/sidebar/preferences");
366
- const data = call.ok ? call.result ?? null : null;
367
- const rawSettings = data?.settings;
368
- const responseOrder = Array.isArray(rawSettings?.groupOrder) ? rawSettings.groupOrder.map((id) => typeof id === "string" ? id.trim() : "").filter((id) => id.length > 0) : [];
369
- const responseGroupLabels = {};
370
- if (rawSettings?.groupLabels && typeof rawSettings.groupLabels === "object") {
371
- for (const [key, value] of Object.entries(rawSettings.groupLabels)) {
372
- if (typeof value !== "string") continue;
373
- const trimmedKey = key.trim();
374
- if (!trimmedKey) continue;
375
- responseGroupLabels[trimmedKey] = value;
376
- }
377
- }
378
- const responseItemLabels = {};
379
- if (rawSettings?.itemLabels && typeof rawSettings.itemLabels === "object") {
380
- for (const [key, value] of Object.entries(rawSettings.itemLabels)) {
381
- if (typeof value !== "string") continue;
382
- const trimmedKey = key.trim();
383
- if (!trimmedKey) continue;
384
- responseItemLabels[trimmedKey] = value;
385
- }
386
- }
387
- const responseHiddenItems = Array.isArray(rawSettings?.hiddenItems) ? rawSettings.hiddenItems.map((itemId) => typeof itemId === "string" ? itemId.trim() : "").filter((itemId) => itemId.length > 0) : [];
388
- const canManageRoles = data?.canApplyToRoles === true;
389
- setCanApplyToRoles(canManageRoles);
390
- if (canManageRoles) {
391
- const roles = Array.isArray(data?.roles) ? data.roles.filter((role) => typeof role?.id === "string" && typeof role?.name === "string") : [];
392
- const mappedRoles = roles.map((role) => ({
393
- id: role.id,
394
- name: role.name,
395
- hasPreference: role.hasPreference === true
396
- }));
397
- setAvailableRoleTargets(mappedRoles);
398
- setSelectedRoleIds(mappedRoles.filter((role) => role.hasPreference).map((role) => role.id));
399
- } else {
400
- setAvailableRoleTargets([]);
401
- setSelectedRoleIds([]);
402
- }
403
- const currentIds = baseSnapshot.map((group) => resolveGroupKey(group));
404
- const order = mergeGroupOrder(responseOrder, currentIds);
405
- const { itemDefaults } = collectSidebarDefaults(baseSnapshot);
406
- const hiddenItemIds = {};
407
- for (const itemId of responseHiddenItems) {
408
- if (!itemDefaults.has(itemId)) continue;
409
- hiddenItemIds[itemId] = true;
410
- }
411
- const draft = {
412
- order,
413
- groupLabels: { ...responseGroupLabels },
414
- itemLabels: { ...responseItemLabels },
415
- hiddenItemIds
416
- };
417
- originalNavRef.current = baseSnapshot;
418
- setCustomDraft(draft);
419
- setNavGroups(applyCustomizationDraft(baseSnapshot, draft));
420
- setCustomizing(true);
421
- } catch (error) {
422
- console.error("Failed to load sidebar preferences", error);
423
- setCustomizationError(t("appShell.sidebarCustomizationLoadError"));
424
- } finally {
425
- setLoadingPreferences(false);
426
- }
427
- }, [customizing, loadingPreferences, navGroups, t]);
428
- const cancelCustomization = React.useCallback(() => {
429
- setCustomizing(false);
430
- setCustomDraft(null);
431
- setCustomizationError(null);
432
- setAvailableRoleTargets([]);
433
- setSelectedRoleIds([]);
434
- setCanApplyToRoles(false);
435
- if (originalNavRef.current) {
436
- setNavGroups(AppShell.cloneGroups(originalNavRef.current));
437
- }
438
- originalNavRef.current = null;
439
- }, []);
440
- const resetCustomization = React.useCallback(() => {
441
- if (!originalNavRef.current) return;
442
- const base = AppShell.cloneGroups(originalNavRef.current);
443
- const order = base.map((group) => resolveGroupKey(group));
444
- const draft = { order, groupLabels: {}, itemLabels: {}, hiddenItemIds: {} };
445
- originalNavRef.current = base;
446
- setCustomDraft(draft);
447
- setNavGroups(applyCustomizationDraft(base, draft));
448
- if (canApplyToRoles) {
449
- setSelectedRoleIds(availableRoleTargets.filter((role) => role.hasPreference).map((role) => role.id));
450
- }
451
- }, [availableRoleTargets, canApplyToRoles]);
452
- const saveCustomization = React.useCallback(async () => {
453
- if (!customDraft) return;
454
- setSavingPreferences(true);
455
- setCustomizationError(null);
456
- try {
457
- const baseGroups = originalNavRef.current ?? filterMainSidebarGroups(AppShell.cloneGroups(navGroups));
458
- const { groupDefaults, itemDefaults } = collectSidebarDefaults(baseGroups);
459
- const sanitizedGroupLabels = {};
460
- for (const [key, value] of Object.entries(customDraft.groupLabels)) {
461
- const trimmed = value.trim();
462
- const base = groupDefaults.get(key);
463
- if (!trimmed || !base) continue;
464
- if (trimmed !== base) sanitizedGroupLabels[key] = trimmed;
465
- }
466
- const sanitizedItemLabels = {};
467
- for (const [itemId, value] of Object.entries(customDraft.itemLabels)) {
468
- const trimmed = value.trim();
469
- const base = itemDefaults.get(itemId);
470
- if (!trimmed || !base) continue;
471
- if (trimmed !== base) sanitizedItemLabels[itemId] = trimmed;
472
- }
473
- const sanitizedHiddenItems = [];
474
- for (const [itemId, hidden] of Object.entries(customDraft.hiddenItemIds)) {
475
- if (!hidden) continue;
476
- if (!itemDefaults.has(itemId)) continue;
477
- sanitizedHiddenItems.push(itemId);
478
- }
479
- const applyToRolesPayload = canApplyToRoles ? [...selectedRoleIds] : [];
480
- const clearRoleIdsPayload = canApplyToRoles ? availableRoleTargets.filter((role) => role.hasPreference && !selectedRoleIds.includes(role.id)).map((role) => role.id) : [];
481
- const payload = {
482
- groupOrder: customDraft.order,
483
- groupLabels: sanitizedGroupLabels,
484
- itemLabels: sanitizedItemLabels,
485
- hiddenItems: sanitizedHiddenItems
486
- };
487
- if (canApplyToRoles) {
488
- payload.applyToRoles = applyToRolesPayload;
489
- payload.clearRoleIds = clearRoleIdsPayload;
490
- }
491
- const call = await apiCall("/api/auth/sidebar/preferences", {
492
- method: "PUT",
493
- headers: { "content-type": "application/json" },
494
- body: JSON.stringify(payload)
495
- });
496
- if (!call.ok) {
497
- setCustomizationError(t("appShell.sidebarCustomizationSaveError"));
498
- return;
499
- }
500
- const data = call.result ?? null;
501
- if (data?.canApplyToRoles !== void 0) {
502
- setCanApplyToRoles(data.canApplyToRoles === true);
503
- }
504
- if (Array.isArray(data?.roles)) {
505
- const mappedRoles = data.roles.filter((role) => typeof role?.id === "string" && typeof role?.name === "string").map((role) => ({
506
- id: role.id,
507
- name: role.name,
508
- hasPreference: role.hasPreference === true
509
- }));
510
- setAvailableRoleTargets(mappedRoles);
511
- setSelectedRoleIds(mappedRoles.filter((role) => role.hasPreference).map((role) => role.id));
512
- }
513
- originalNavRef.current = applyCustomizationDraft(baseGroups, customDraft);
514
- setNavGroups(AppShell.cloneGroups(originalNavRef.current));
515
- setCustomizing(false);
516
- setCustomDraft(null);
517
- try {
518
- window.dispatchEvent(new Event("om:refresh-sidebar"));
519
- } catch {
520
- }
521
- } catch (error) {
522
- console.error("Failed to save sidebar preferences", error);
523
- setCustomizationError(t("appShell.sidebarCustomizationSaveError"));
524
- } finally {
525
- setSavingPreferences(false);
526
- }
527
- }, [customDraft, navGroups, t]);
528
- const moveGroup = React.useCallback((groupId, offset) => {
529
- updateDraft((draft) => {
530
- const order = [...draft.order];
531
- const index = order.indexOf(groupId);
532
- if (index === -1) return draft;
533
- const nextIndex = Math.max(0, Math.min(order.length - 1, index + offset));
534
- if (nextIndex === index) return draft;
535
- order.splice(index, 1);
536
- order.splice(nextIndex, 0, groupId);
537
- return { ...draft, order };
538
- });
539
- }, [updateDraft]);
540
- const setGroupLabel = React.useCallback((groupId, value) => {
541
- updateDraft((draft) => {
542
- const next = { ...draft.groupLabels };
543
- if (value.trim().length === 0) delete next[groupId];
544
- else next[groupId] = value;
545
- return { ...draft, groupLabels: next };
546
- });
547
- }, [updateDraft]);
548
- const setItemLabel = React.useCallback((itemId, value) => {
549
- updateDraft((draft) => {
550
- const next = { ...draft.itemLabels };
551
- if (value.trim().length === 0) delete next[itemId];
552
- else next[itemId] = value;
553
- return { ...draft, itemLabels: next };
554
- });
555
- }, [updateDraft]);
556
- const setItemHidden = React.useCallback((itemId, hidden) => {
557
- updateDraft((draft) => {
558
- const next = { ...draft.hiddenItemIds };
559
- if (hidden) next[itemId] = true;
560
- else delete next[itemId];
561
- return { ...draft, hiddenItemIds: next };
562
- });
563
- }, [updateDraft]);
564
- const toggleRoleSelection = React.useCallback((roleId) => {
565
- setSelectedRoleIds((prev) => prev.includes(roleId) ? prev.filter((id) => id !== roleId) : [...prev, roleId]);
566
- }, []);
567
- const asideWidth = effectiveCollapsed ? "72px" : expandedSidebarWidth;
568
- const asideClassesBase = `border-r bg-background/80 py-4 min-h-svh`;
374
+ const asideWidth = effectiveCollapsed ? "80px" : expandedSidebarWidth;
375
+ const asideClassesBase = `border-r bg-background py-4`;
569
376
  React.useEffect(() => {
570
377
  try {
571
378
  localStorage.setItem("om:sidebarCollapsed", collapsed ? "1" : "0");
@@ -601,107 +408,138 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
601
408
  }
602
409
  }, [pathname]);
603
410
  React.useEffect(() => {
604
- if (customizing && customDraft && originalNavRef.current) {
605
- originalNavRef.current = filterMainSidebarGroups(AppShell.cloneGroups(resolvedGroups));
606
- setNavGroups(applyCustomizationDraft(originalNavRef.current, customDraft));
607
- return;
608
- }
609
- setNavGroups(AppShell.cloneGroups(resolvedGroups));
610
- }, [resolvedGroups, customizing, customDraft]);
411
+ setNavGroups(cloneSidebarGroups(resolvedGroups));
412
+ }, [resolvedGroups]);
611
413
  function renderSectionSidebar(sections, title, compact, hideHeader) {
612
414
  const sortedSections = [...sections].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
613
415
  const lastVisibleIndex = sortedSections.length - 1;
614
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-full gap-3", children: [
615
- !hideHeader && /* @__PURE__ */ jsx("div", { className: `flex items-center ${compact ? "justify-center" : "justify-between"} mb-2`, children: /* @__PURE__ */ jsxs(Link, { href: "/backend", className: "flex items-center gap-2", "aria-label": t("appShell.goToDashboard"), children: [
616
- /* @__PURE__ */ jsx(Image, { src: logo?.src ?? "/open-mercato.svg", alt: logo?.alt ?? resolvedProductName, width: 32, height: 32, className: "rounded m-4" }),
617
- !compact && /* @__PURE__ */ jsx("div", { className: "text-m font-semibold", children: resolvedProductName })
618
- ] }) }),
619
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-3 overflow-y-auto pr-1", children: [
620
- /* @__PURE__ */ jsxs(
621
- Link,
622
- {
623
- href: "/backend",
624
- className: `flex items-center gap-2 ${compact ? "justify-center px-2" : "px-2"} py-1 text-sm text-muted-foreground hover:text-foreground transition-colors`,
625
- "aria-label": t("backend.nav.backToMain", "Back"),
626
- children: [
627
- /* @__PURE__ */ jsx("span", { className: "flex items-center justify-center shrink-0", children: BackArrowIcon }),
628
- !compact && /* @__PURE__ */ jsx("span", { children: title })
629
- ]
630
- }
631
- ),
632
- /* @__PURE__ */ jsx("nav", { className: "flex flex-col gap-2", children: sortedSections.map((section, sectionIndex) => {
633
- const visibleItems = section.items;
634
- if (visibleItems.length === 0) return null;
635
- const sortedItems = [...visibleItems].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
636
- const sectionLabel = section.labelKey ? t(section.labelKey, section.label) : section.label;
637
- const sectionKey = `settings:${section.id}`;
638
- const open = openGroups[sectionKey] !== false;
639
- const sortSectionItems = (items = []) => [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
640
- const renderSectionItem = (item, depth = 0) => {
641
- const label = item.labelKey ? t(item.labelKey, item.label) : item.label;
642
- const childItems = sortSectionItems(item.children);
643
- const isOnItemBranch = !!pathname && (pathname === item.href || pathname.startsWith(`${item.href}/`));
644
- const hasActiveChild = !!(pathname && childItems.some((child) => pathname === child.href || pathname.startsWith(`${child.href}/`)));
645
- const showChildren = childItems.length > 0 && isOnItemBranch;
646
- const isActive = isOnItemBranch || hasActiveChild;
647
- const base = compact ? "w-10 h-10 justify-center" : "py-1 gap-2";
648
- const spacingStyle = !compact ? {
649
- paddingLeft: `${8 + depth * 16}px`,
650
- paddingRight: "8px"
651
- } : void 0;
652
- return /* @__PURE__ */ jsxs(React.Fragment, { children: [
653
- /* @__PURE__ */ jsxs(
654
- Link,
655
- {
656
- href: item.href,
657
- className: `relative text-sm rounded inline-flex items-center ${base} ${isActive ? "bg-background border shadow-sm" : "hover:bg-accent hover:text-accent-foreground"}`,
658
- style: spacingStyle,
659
- title: compact ? label : void 0,
660
- "data-menu-item-id": item.id,
661
- onClick: () => setMobileOpen(false),
662
- children: [
663
- isActive && /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }),
664
- /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: renderIcon(
665
- item.icon,
666
- item.iconName,
667
- item.iconMarkup,
668
- item.href.includes("/backend/entities/user/") && item.href.endsWith("/records") ? DataTableIcon : DefaultIcon
669
- ) }),
670
- !compact && /* @__PURE__ */ jsx("span", { className: "truncate", children: label })
671
- ]
672
- }
673
- ),
674
- showChildren ? childItems.map((child) => renderSectionItem(child, depth + 1)) : null
675
- ] }, item.id);
676
- };
677
- return /* @__PURE__ */ jsxs("div", { children: [
416
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col gap-3", children: [
417
+ !hideHeader && /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxs(
418
+ Link,
419
+ {
420
+ href: "/backend",
421
+ className: `flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? "p-2 justify-center" : "p-3"}`,
422
+ "aria-label": t("appShell.goToDashboard"),
423
+ children: [
424
+ /* @__PURE__ */ jsx(Image, { src: logo?.src ?? "/open-mercato.svg", alt: logo?.alt ?? resolvedProductName, width: 40, height: 40, className: "rounded-full shrink-0" }),
425
+ !compact && /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: resolvedProductName })
426
+ ]
427
+ }
428
+ ) }),
429
+ !compact && /* @__PURE__ */ jsx(
430
+ Input,
431
+ {
432
+ type: "text",
433
+ value: navQuery,
434
+ onChange: (e) => setNavQuery(e.target.value),
435
+ placeholder: t("appShell.searchNavPlaceholder", "Search..."),
436
+ "aria-label": t("appShell.searchNavAria", "Search navigation"),
437
+ leftIcon: /* @__PURE__ */ jsx(Search, { "aria-hidden": true }),
438
+ rightIcon: navQueryActive ? /* @__PURE__ */ jsx(
439
+ IconButton,
440
+ {
441
+ type: "button",
442
+ variant: "ghost",
443
+ size: "xs",
444
+ onClick: () => setNavQuery(""),
445
+ "aria-label": t("appShell.searchNavClear", "Clear search"),
446
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5", "aria-hidden": true })
447
+ }
448
+ ) : void 0,
449
+ className: "mb-2"
450
+ }
451
+ ),
452
+ /* @__PURE__ */ jsx("div", { "data-sidebar-scroll": "true", className: `flex flex-1 flex-col gap-3 overflow-y-auto scrollbar-hide pr-1 ${compact ? "-ml-2 pl-2" : "-ml-3 pl-3"}`, children: /* @__PURE__ */ jsx("nav", { className: "flex flex-col gap-2", children: sortedSections.map((section, sectionIndex) => {
453
+ const matchesItemQuery = (item) => {
454
+ if (!navQueryActive) return true;
455
+ const label = item.labelKey ? t(item.labelKey, item.label) : item.label;
456
+ if (matchesQuery(label)) return true;
457
+ return Array.isArray(item.children) && item.children.some(matchesItemQuery);
458
+ };
459
+ const visibleItems = navQueryActive ? section.items.filter(matchesItemQuery) : section.items;
460
+ if (visibleItems.length === 0) return null;
461
+ const sortedItems = [...visibleItems].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
462
+ const sectionLabel = section.labelKey ? t(section.labelKey, section.label) : section.label;
463
+ const sectionKey = `settings:${section.id}`;
464
+ const open = openGroups[sectionKey] !== false;
465
+ const sortSectionItems = (items = []) => [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
466
+ const filterChildren = (children2) => {
467
+ if (!children2) return [];
468
+ if (!navQueryActive) return [...children2];
469
+ return children2.filter(matchesItemQuery);
470
+ };
471
+ const renderSectionItem = (item, depth = 0) => {
472
+ const label = item.labelKey ? t(item.labelKey, item.label) : item.label;
473
+ const childItems = sortSectionItems(filterChildren(item.children));
474
+ const isOnItemBranch = !!pathname && (pathname === item.href || pathname.startsWith(`${item.href}/`));
475
+ const hasActiveChild = !!(pathname && childItems.some((child) => pathname === child.href || pathname.startsWith(`${child.href}/`)));
476
+ const showChildren = childItems.length > 0 && (isOnItemBranch || navQueryActive);
477
+ const isActive = isOnItemBranch || hasActiveChild;
478
+ const base = compact ? "w-10 h-10 justify-center" : "w-full py-2 gap-2";
479
+ const spacingStyle = !compact ? {
480
+ paddingLeft: `${12 + depth * 16}px`,
481
+ paddingRight: "12px"
482
+ } : void 0;
483
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
678
484
  /* @__PURE__ */ jsxs(
679
- Button,
485
+ Link,
680
486
  {
681
- variant: "muted",
682
- onClick: () => toggleGroup(sectionKey),
683
- className: `w-full ${compact ? "px-0 justify-center" : "px-2 justify-between"} flex text-xs uppercase text-muted-foreground/90 py-2`,
684
- "aria-expanded": open,
487
+ href: item.href,
488
+ className: `relative text-sm font-medium rounded-lg inline-flex items-center ${base} ${isActive ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted"}`,
489
+ style: spacingStyle,
490
+ title: compact ? label : void 0,
491
+ "data-menu-item-id": item.id,
492
+ onClick: () => setMobileOpen(false),
685
493
  children: [
686
- !compact && /* @__PURE__ */ jsx("span", { children: sectionLabel }),
687
- !compact && /* @__PURE__ */ jsx(Chevron, { open })
494
+ isActive && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: `absolute ${compact ? "left-[-20px]" : "left-[-12px]"} top-2 w-1 h-5 rounded-r bg-foreground` }),
495
+ /* @__PURE__ */ jsx("span", { className: "flex items-center justify-center shrink-0", children: renderIcon(
496
+ item.icon,
497
+ item.iconName,
498
+ item.iconMarkup,
499
+ item.href.includes("/backend/entities/user/") && item.href.endsWith("/records") ? DataTableIcon : DefaultIcon
500
+ ) }),
501
+ !compact && /* @__PURE__ */ jsx("span", { className: "truncate", children: label })
688
502
  ]
689
503
  }
690
504
  ),
691
- open && /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1 ${!compact ? "pl-1" : ""}`, children: sortedItems.map((item) => renderSectionItem(item)) }),
692
- sectionIndex !== lastVisibleIndex && /* @__PURE__ */ jsx("div", { className: "my-2 border-t border-dotted" })
693
- ] }, section.id);
694
- }) })
695
- ] })
505
+ showChildren ? childItems.map((child) => renderSectionItem(child, depth + 1)) : null
506
+ ] }, item.id);
507
+ };
508
+ return /* @__PURE__ */ jsxs("div", { children: [
509
+ !compact && /* @__PURE__ */ jsxs(
510
+ Button,
511
+ {
512
+ variant: "muted",
513
+ onClick: () => toggleGroup(sectionKey),
514
+ className: "w-full px-1 justify-between flex text-xs font-medium uppercase tracking-wider text-muted-foreground/70 py-1",
515
+ "aria-expanded": open,
516
+ children: [
517
+ /* @__PURE__ */ jsx("span", { children: sectionLabel }),
518
+ /* @__PURE__ */ jsx(Chevron, { open })
519
+ ]
520
+ }
521
+ ),
522
+ (open || compact) && /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1`, children: sortedItems.map((item) => renderSectionItem(item)) }),
523
+ sectionIndex !== lastVisibleIndex && /* @__PURE__ */ jsx("div", { className: `my-2 border-t ${compact ? "-ml-2 -mr-3" : "-ml-3 -mr-4"}` })
524
+ ] }, section.id);
525
+ }) }) })
696
526
  ] });
697
527
  }
698
528
  function renderSidebar(compact, hideHeader) {
699
529
  if (!isChromeReady && isChromeLoading && resolvedGroups.length === 0) {
700
530
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-full gap-3", "data-testid": "backend-chrome-loading", children: [
701
- !hideHeader ? /* @__PURE__ */ jsx("div", { className: `flex items-center ${compact ? "justify-center" : "justify-between"} mb-2`, children: /* @__PURE__ */ jsxs(Link, { href: "/backend", className: "flex items-center gap-2", "aria-label": t("appShell.goToDashboard"), children: [
702
- /* @__PURE__ */ jsx(Image, { src: logo?.src ?? "/open-mercato.svg", alt: logo?.alt ?? resolvedProductName, width: 32, height: 32, className: "rounded m-4" }),
703
- !compact && /* @__PURE__ */ jsx("div", { className: "text-m font-semibold", children: resolvedProductName })
704
- ] }) }) : null,
531
+ !hideHeader ? /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxs(
532
+ Link,
533
+ {
534
+ href: "/backend",
535
+ className: `flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? "p-2 justify-center" : "p-3"}`,
536
+ "aria-label": t("appShell.goToDashboard"),
537
+ children: [
538
+ /* @__PURE__ */ jsx(Image, { src: logo?.src ?? "/open-mercato.svg", alt: logo?.alt ?? resolvedProductName, width: 40, height: 40, className: "rounded-full shrink-0" }),
539
+ !compact && /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: resolvedProductName })
540
+ ]
541
+ }
542
+ ) }) : null,
705
543
  /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-3 pr-1", children: [
706
544
  /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
707
545
  /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/50" }),
@@ -749,180 +587,19 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
749
587
  }
750
588
  const isMobileVariant = !!hideHeader;
751
589
  const shouldRenderSidebarInjectionSpots = !isMobileVariant;
752
- const baseGroupsForDefaults = originalNavRef.current ?? mainNavGroupsWithInjected;
753
- const baseGroupMap = /* @__PURE__ */ new Map();
754
- for (const group of baseGroupsForDefaults) {
755
- baseGroupMap.set(resolveGroupKey(group), group);
756
- }
757
- const localeLabel = (locale || "").toUpperCase();
758
- const orderedGroupIds = customDraft ? mergeGroupOrder(customDraft.order, Array.from(baseGroupMap.keys())) : mainNavGroupsWithInjected.map((group) => resolveGroupKey(group));
759
- const lastVisibleGroupIndex = (() => {
760
- for (let idx = navGroups.length - 1; idx >= 0; idx -= 1) {
761
- if (navGroups[idx].items.some((item) => item.hidden !== true)) return idx;
762
- }
763
- return -1;
764
- })();
765
- const renderEditableItems = (baseItems, currentItems, depth = 0) => {
766
- if (!customDraft) return null;
767
- return baseItems.map((baseItem) => {
768
- const itemKey = resolveItemKey(baseItem);
769
- const current = currentItems.find((item) => item.href === baseItem.href) ?? baseItem;
770
- const placeholder = baseItem.defaultTitle ?? baseItem.title;
771
- const value = customDraft.itemLabels[itemKey] ?? "";
772
- const hidden = customDraft.hiddenItemIds[itemKey] === true;
773
- return /* @__PURE__ */ jsxs(
774
- "div",
775
- {
776
- className: `flex flex-col gap-1 ${hidden ? "opacity-60" : ""}`,
777
- style: depth ? { marginLeft: depth * 16 } : void 0,
778
- children: [
779
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: placeholder }),
780
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
781
- /* @__PURE__ */ jsx(
782
- Checkbox,
783
- {
784
- checked: !hidden,
785
- onCheckedChange: (next) => setItemHidden(itemKey, next !== true),
786
- disabled: savingPreferences,
787
- "aria-label": t("appShell.sidebarCustomizationShowItem"),
788
- title: t("appShell.sidebarCustomizationShowItem")
789
- }
790
- ),
791
- /* @__PURE__ */ jsx(
792
- "input",
793
- {
794
- value,
795
- onChange: (event) => setItemLabel(itemKey, event.target.value),
796
- placeholder,
797
- disabled: savingPreferences,
798
- className: "h-8 flex-1 rounded border bg-background px-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
799
- }
800
- )
801
- ] }),
802
- baseItem.children && baseItem.children.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: renderEditableItems(baseItem.children, current.children ?? [], depth + 1) }) : null
803
- ]
804
- },
805
- itemKey
806
- );
807
- });
808
- };
809
- const customizationEditor = customizing ? customDraft ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded border border-dashed bg-muted/30 p-3", children: [
810
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
811
- /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: t("appShell.sidebarCustomizationHeading") }),
812
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
813
- /* @__PURE__ */ jsx(
814
- Button,
815
- {
816
- variant: "outline",
817
- size: "sm",
818
- onClick: resetCustomization,
819
- disabled: savingPreferences,
820
- children: t("appShell.sidebarCustomizationReset")
821
- }
822
- ),
823
- /* @__PURE__ */ jsx(
824
- Button,
825
- {
826
- variant: "outline",
827
- size: "sm",
828
- onClick: cancelCustomization,
829
- disabled: savingPreferences,
830
- children: t("appShell.sidebarCustomizationCancel")
831
- }
832
- ),
833
- /* @__PURE__ */ jsx(
834
- Button,
835
- {
836
- size: "sm",
837
- className: "bg-foreground text-background hover:bg-foreground/90",
838
- onClick: saveCustomization,
839
- disabled: savingPreferences,
840
- children: savingPreferences ? t("appShell.sidebarCustomizationSaving") : t("appShell.sidebarCustomizationSave")
841
- }
842
- )
843
- ] })
844
- ] }),
845
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("appShell.sidebarCustomizationHint", { locale: localeLabel }) }),
846
- canApplyToRoles ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded border bg-background/80 p-3 shadow-sm", children: [
847
- /* @__PURE__ */ jsxs("div", { children: [
848
- /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: t("appShell.sidebarApplyToRolesTitle") }),
849
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("appShell.sidebarApplyToRolesDescription") })
850
- ] }),
851
- availableRoleTargets.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: availableRoleTargets.map((role) => {
852
- const checked = selectedRoleIds.includes(role.id);
853
- const willClear = role.hasPreference && !checked;
854
- return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 rounded-md border bg-background px-2 py-1 text-sm shadow-sm cursor-pointer", children: [
855
- /* @__PURE__ */ jsx(
856
- Checkbox,
857
- {
858
- checked,
859
- onCheckedChange: () => toggleRoleSelection(role.id),
860
- disabled: savingPreferences
861
- }
862
- ),
863
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: role.name }),
864
- role.hasPreference ? /* @__PURE__ */ jsx("span", { className: `text-xs ${willClear ? "text-destructive" : "text-muted-foreground"}`, children: willClear ? t("appShell.sidebarRoleWillClear") : t("appShell.sidebarRoleHasPreset") }) : null
865
- ] }, role.id);
866
- }) }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("appShell.sidebarApplyToRolesEmpty") })
867
- ] }) : null,
868
- customizationError ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: customizationError }) : null,
869
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: orderedGroupIds.map((groupId, index) => {
870
- const baseGroup = baseGroupMap.get(groupId);
871
- if (!baseGroup) return null;
872
- const currentGroup = navGroups.find((group) => resolveGroupKey(group) === groupId) ?? baseGroup;
873
- const placeholder = baseGroup.defaultName ?? baseGroup.name;
874
- const value = customDraft.groupLabels[groupId] ?? "";
875
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded border bg-background p-3 shadow-sm", children: [
876
- /* @__PURE__ */ jsxs("div", { className: `flex ${compact ? "flex-col gap-2" : "items-center gap-2"}`, children: [
877
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
878
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: t("appShell.sidebarCustomizationGroupLabel") }),
879
- /* @__PURE__ */ jsx(
880
- "input",
881
- {
882
- value,
883
- onChange: (event) => setGroupLabel(groupId, event.target.value),
884
- placeholder,
885
- disabled: savingPreferences,
886
- className: "mt-1 h-8 w-full rounded border bg-background px-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
887
- }
888
- )
889
- ] }),
890
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 self-start", children: [
891
- /* @__PURE__ */ jsx(
892
- IconButton,
893
- {
894
- variant: "outline",
895
- size: "sm",
896
- className: "text-muted-foreground hover:text-foreground",
897
- onClick: () => moveGroup(groupId, -1),
898
- disabled: index === 0 || savingPreferences,
899
- "aria-label": t("appShell.sidebarCustomizationMoveUp"),
900
- children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-4" })
901
- }
902
- ),
903
- /* @__PURE__ */ jsx(
904
- IconButton,
905
- {
906
- variant: "outline",
907
- size: "sm",
908
- className: "text-muted-foreground hover:text-foreground",
909
- onClick: () => moveGroup(groupId, 1),
910
- disabled: index === orderedGroupIds.length - 1 || savingPreferences,
911
- "aria-label": t("appShell.sidebarCustomizationMoveDown"),
912
- children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
913
- }
914
- )
915
- ] })
916
- ] }),
917
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: renderEditableItems(baseGroup.items, currentGroup.items) })
918
- ] }, groupId);
919
- }) })
920
- ] }) : /* @__PURE__ */ jsx("div", { className: "rounded border border-dashed bg-muted/30 p-3 text-sm text-muted-foreground", children: t("appShell.sidebarCustomizationLoading") }) : null;
921
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-full gap-3", children: [
922
- !hideHeader && /* @__PURE__ */ jsx("div", { className: `flex items-center ${compact ? "justify-center" : "justify-between"} mb-2`, children: /* @__PURE__ */ jsxs(Link, { href: "/backend", className: "flex items-center gap-2", "aria-label": t("appShell.goToDashboard"), children: [
923
- /* @__PURE__ */ jsx(Image, { src: logo?.src ?? "/open-mercato.svg", alt: logo?.alt ?? resolvedProductName, width: 32, height: 32, className: "rounded m-4" }),
924
- !compact && /* @__PURE__ */ jsx("div", { className: "text-m font-semibold", children: resolvedProductName })
925
- ] }) }),
590
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col gap-3", children: [
591
+ !hideHeader && /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxs(
592
+ Link,
593
+ {
594
+ href: "/backend",
595
+ className: `flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? "p-2 justify-center" : "p-3"}`,
596
+ "aria-label": t("appShell.goToDashboard"),
597
+ children: [
598
+ /* @__PURE__ */ jsx(Image, { src: logo?.src ?? "/open-mercato.svg", alt: logo?.alt ?? resolvedProductName, width: 40, height: 40, className: "rounded-full shrink-0" }),
599
+ !compact && /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: resolvedProductName })
600
+ ]
601
+ }
602
+ ) }),
926
603
  shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
927
604
  InjectionSpot,
928
605
  {
@@ -930,7 +607,30 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
930
607
  context: injectionContext
931
608
  }
932
609
  ) : null,
933
- /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col gap-3 pr-1", children: customizing ? customizationEditor : (() => {
610
+ !compact && /* @__PURE__ */ jsx(
611
+ Input,
612
+ {
613
+ type: "text",
614
+ value: navQuery,
615
+ onChange: (e) => setNavQuery(e.target.value),
616
+ placeholder: t("appShell.searchNavPlaceholder", "Search..."),
617
+ "aria-label": t("appShell.searchNavAria", "Search navigation"),
618
+ leftIcon: /* @__PURE__ */ jsx(Search, { "aria-hidden": true }),
619
+ rightIcon: navQueryActive ? /* @__PURE__ */ jsx(
620
+ IconButton,
621
+ {
622
+ type: "button",
623
+ variant: "ghost",
624
+ size: "xs",
625
+ onClick: () => setNavQuery(""),
626
+ "aria-label": t("appShell.searchNavClear", "Clear search"),
627
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5", "aria-hidden": true })
628
+ }
629
+ ) : void 0,
630
+ className: "mb-2"
631
+ }
632
+ ),
633
+ /* @__PURE__ */ jsx("div", { "data-sidebar-scroll": "true", className: `flex flex-1 flex-col gap-3 overflow-y-auto scrollbar-hide pr-1 ${compact ? "-ml-2 pl-2" : "-ml-3 pl-3"}`, children: (() => {
934
634
  const isSettingsPath = (href) => {
935
635
  if (href === "/backend/settings") return true;
936
636
  return resolvedSettingsPathPrefixes.some((prefix) => href.startsWith(prefix));
@@ -960,42 +660,50 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
960
660
  ) : null,
961
661
  mainGroups.map((g, gi) => {
962
662
  const groupId = resolveGroupKey(g);
963
- const open = openGroups[groupId] !== false;
964
- const visibleItems = g.items.filter((item) => item.hidden !== true);
663
+ const open = navQueryActive ? true : openGroups[groupId] !== false;
664
+ const visibleItems = g.items.filter((item) => {
665
+ if (item.hidden === true) return false;
666
+ if (!navQueryActive) return true;
667
+ if (matchesQuery(item.title)) return true;
668
+ const itemChildren = (item.children ?? []).filter((c) => c.hidden !== true);
669
+ return itemChildren.some((c) => matchesQuery(c.title));
670
+ });
965
671
  if (visibleItems.length === 0) return null;
966
672
  return /* @__PURE__ */ jsxs("div", { children: [
967
- /* @__PURE__ */ jsxs(
673
+ !compact && /* @__PURE__ */ jsxs(
968
674
  Button,
969
675
  {
970
676
  variant: "muted",
971
677
  onClick: () => toggleGroup(groupId),
972
- className: `w-full ${compact ? "px-0 justify-center" : "px-2 justify-between"} flex text-xs uppercase text-muted-foreground/90 py-2`,
678
+ className: "w-full px-1 justify-between flex text-xs font-medium uppercase tracking-wider text-muted-foreground/70 py-1",
973
679
  "aria-expanded": open,
974
680
  children: [
975
- !compact && /* @__PURE__ */ jsx("span", { children: g.name }),
976
- !compact && /* @__PURE__ */ jsx(Chevron, { open })
681
+ /* @__PURE__ */ jsx("span", { children: g.name }),
682
+ /* @__PURE__ */ jsx(Chevron, { open })
977
683
  ]
978
684
  }
979
685
  ),
980
- open && /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1 ${!compact ? "pl-1" : ""}`, children: visibleItems.map((i) => {
981
- const childItems = (i.children ?? []).filter((child) => child.hidden !== true);
982
- const showChildren = !!pathname && childItems.length > 0 && pathname.startsWith(i.href);
983
- const hasActiveChild = !!(pathname && childItems.some((c) => pathname.startsWith(c.href)));
984
- const isParentActive = pathname === i.href || showChildren && !hasActiveChild;
985
- const base = compact ? "w-10 h-10 justify-center" : "px-2 py-1 gap-2";
686
+ (open || compact) && /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1`, children: visibleItems.map((i) => {
687
+ const allChildItems = (i.children ?? []).filter((child) => child.hidden !== true);
688
+ const matchingChildItems = navQueryActive ? allChildItems.filter((c) => matchesQuery(c.title)) : allChildItems;
689
+ const childItems = navQueryActive ? matchingChildItems : allChildItems;
690
+ const showChildren = navQueryActive ? matchingChildItems.length > 0 : !!pathname && allChildItems.length > 0 && pathname.startsWith(i.href);
691
+ const hasActiveChild = !!(pathname && allChildItems.some((c) => pathname.startsWith(c.href)));
692
+ const isParentActive = pathname === i.href || !navQueryActive && showChildren && !hasActiveChild;
693
+ const base = compact ? "w-10 h-10 justify-center" : "w-full px-3 py-2 gap-2";
986
694
  return /* @__PURE__ */ jsxs(React.Fragment, { children: [
987
695
  /* @__PURE__ */ jsxs(
988
696
  Link,
989
697
  {
990
698
  href: i.href,
991
- className: `relative text-sm rounded inline-flex items-center ${base} ${isParentActive ? "bg-background border shadow-sm" : "hover:bg-accent hover:text-accent-foreground"} ${i.enabled === false ? "pointer-events-none opacity-50" : ""}`,
699
+ className: `relative text-sm font-medium rounded-lg inline-flex items-center ${base} ${isParentActive ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted"} ${i.enabled === false ? "pointer-events-none opacity-50" : ""}`,
992
700
  "aria-disabled": i.enabled === false,
993
701
  title: compact ? i.title : void 0,
994
702
  "data-menu-item-id": i.id ?? i.href,
995
703
  onClick: () => setMobileOpen(false),
996
704
  children: [
997
- isParentActive ? /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }) : null,
998
- /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: renderIcon(
705
+ isParentActive ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: `absolute ${compact ? "left-[-20px]" : "left-[-12px]"} top-2 w-1 h-5 rounded-r bg-foreground` }) : null,
706
+ /* @__PURE__ */ jsx("span", { className: "flex items-center justify-center shrink-0", children: renderIcon(
999
707
  i.icon,
1000
708
  i.iconName,
1001
709
  i.iconMarkup,
@@ -1005,40 +713,43 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1005
713
  ]
1006
714
  }
1007
715
  ),
1008
- showChildren ? /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1 ${!compact ? "pl-4" : ""}`, children: childItems.map((c) => {
1009
- const childActive = pathname?.startsWith(c.href);
1010
- const childBase = compact ? "w-10 h-8 justify-center" : "px-2 py-1 gap-2";
1011
- return /* @__PURE__ */ jsxs(
1012
- Link,
1013
- {
1014
- href: c.href,
1015
- className: `relative text-sm rounded inline-flex items-center ${childBase} ${childActive ? "bg-background border shadow-sm" : "hover:bg-accent hover:text-accent-foreground"} ${c.enabled === false ? "pointer-events-none opacity-50" : ""}`,
1016
- "aria-disabled": c.enabled === false,
1017
- title: compact ? c.title : void 0,
1018
- "data-menu-item-id": c.id ?? c.href,
1019
- onClick: () => setMobileOpen(false),
1020
- children: [
1021
- childActive ? /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }) : null,
1022
- /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: renderIcon(
1023
- c.icon,
1024
- c.iconName,
1025
- c.iconMarkup,
1026
- c.href.includes("/backend/entities/user/") && c.href.endsWith("/records") ? DataTableIcon : DefaultIcon
1027
- ) }),
1028
- !compact && /* @__PURE__ */ jsx("span", { children: c.title })
1029
- ]
1030
- },
1031
- c.href
1032
- );
1033
- }) }) : null
716
+ showChildren ? /* @__PURE__ */ jsxs("div", { className: `relative flex flex-col ${compact ? "items-center" : ""} gap-1`, children: [
717
+ !compact && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute left-1.5 top-1 bottom-1 w-px bg-border" }),
718
+ childItems.map((c) => {
719
+ const childActive = pathname?.startsWith(c.href);
720
+ const childBase = compact ? "w-10 h-8 justify-center" : "w-full pl-5 pr-3 py-2 gap-2";
721
+ return /* @__PURE__ */ jsxs(
722
+ Link,
723
+ {
724
+ href: c.href,
725
+ className: `relative text-sm font-medium rounded-lg inline-flex items-center ${childBase} ${childActive ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted"} ${c.enabled === false ? "pointer-events-none opacity-50" : ""}`,
726
+ "aria-disabled": c.enabled === false,
727
+ title: compact ? c.title : void 0,
728
+ "data-menu-item-id": c.id ?? c.href,
729
+ onClick: () => setMobileOpen(false),
730
+ children: [
731
+ childActive ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: `absolute ${compact ? "left-[-20px]" : "left-[-12px]"} top-2 w-1 h-5 rounded-r bg-foreground` }) : null,
732
+ /* @__PURE__ */ jsx("span", { className: "flex items-center justify-center shrink-0", children: renderIcon(
733
+ c.icon,
734
+ c.iconName,
735
+ c.iconMarkup,
736
+ c.href.includes("/backend/entities/user/") && c.href.endsWith("/records") ? DataTableIcon : DefaultIcon
737
+ ) }),
738
+ !compact && /* @__PURE__ */ jsx("span", { children: c.title })
739
+ ]
740
+ },
741
+ c.href
742
+ );
743
+ })
744
+ ] }) : null
1034
745
  ] }, i.href);
1035
746
  }) }),
1036
- gi !== mainLastVisibleGroupIndex && /* @__PURE__ */ jsx("div", { className: "my-2 border-t border-dotted" })
747
+ gi !== mainLastVisibleGroupIndex && /* @__PURE__ */ jsx("div", { className: `my-2 border-t ${compact ? "-ml-2 -mr-3" : "-ml-3 -mr-4"}` })
1037
748
  ] }, groupId);
1038
749
  })
1039
750
  ] }) });
1040
751
  })() }),
1041
- /* @__PURE__ */ jsxs("div", { className: "sticky bottom-0 pt-4 border-t bg-background/80 backdrop-blur-sm pb-1", children: [
752
+ /* @__PURE__ */ jsxs("div", { className: "sticky bottom-0 bg-background pb-1", children: [
1042
753
  shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
1043
754
  InjectionSpot,
1044
755
  {
@@ -1046,56 +757,13 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1046
757
  context: injectionContext
1047
758
  }
1048
759
  ) : null,
1049
- /* @__PURE__ */ jsxs(
1050
- Link,
760
+ shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
761
+ StatusBadgeInjectionSpot,
1051
762
  {
1052
- href: "/backend/settings",
1053
- className: `relative text-sm rounded inline-flex items-center w-full ${compact ? "w-10 h-10 justify-center" : "px-2 py-1 gap-2"} ${pathname?.startsWith("/backend/settings") || pathname?.startsWith("/backend/config") || pathname?.startsWith("/backend/users") || pathname?.startsWith("/backend/roles") || pathname?.startsWith("/backend/api-keys") || pathname?.startsWith("/backend/entities") || pathname?.startsWith("/backend/query-indexes") || pathname?.startsWith("/backend/definitions") || pathname?.startsWith("/backend/instances") || pathname?.startsWith("/backend/tasks") || pathname?.startsWith("/backend/events") || pathname?.startsWith("/backend/rules") || pathname?.startsWith("/backend/sets") || pathname?.startsWith("/backend/logs") || pathname?.startsWith("/backend/directory") || pathname?.startsWith("/backend/feature-toggles") ? "bg-background border shadow-sm font-medium" : "hover:bg-accent hover:text-accent-foreground"}`,
1054
- title: compact ? t("backend.nav.settings", "Settings") : void 0,
1055
- onClick: () => setMobileOpen(false),
1056
- children: [
1057
- (pathname?.startsWith("/backend/settings") || pathname?.startsWith("/backend/config") || pathname?.startsWith("/backend/users") || pathname?.startsWith("/backend/roles") || pathname?.startsWith("/backend/api-keys") || pathname?.startsWith("/backend/entities") || pathname?.startsWith("/backend/query-indexes") || pathname?.startsWith("/backend/definitions") || pathname?.startsWith("/backend/instances") || pathname?.startsWith("/backend/tasks") || pathname?.startsWith("/backend/events") || pathname?.startsWith("/backend/rules") || pathname?.startsWith("/backend/sets") || pathname?.startsWith("/backend/logs") || pathname?.startsWith("/backend/directory") || pathname?.startsWith("/backend/feature-toggles")) && /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }),
1058
- /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1059
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
1060
- /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" })
1061
- ] }) }),
1062
- !compact && /* @__PURE__ */ jsx("span", { children: t("backend.nav.settings", "Settings") })
1063
- ]
763
+ spotId: GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID,
764
+ context: injectionContext
1064
765
  }
1065
- ),
1066
- !customizing && /* @__PURE__ */ jsxs("div", { className: "mt-2", children: [
1067
- shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
1068
- StatusBadgeInjectionSpot,
1069
- {
1070
- spotId: GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID,
1071
- context: injectionContext
1072
- }
1073
- ) : null,
1074
- compact || isMobileVariant ? /* @__PURE__ */ jsx(
1075
- IconButton,
1076
- {
1077
- variant: "outline",
1078
- size: "lg",
1079
- onClick: startCustomization,
1080
- disabled: loadingPreferences,
1081
- "aria-label": t("appShell.customizeSidebar"),
1082
- children: CustomizeIcon
1083
- }
1084
- ) : /* @__PURE__ */ jsxs(
1085
- Button,
1086
- {
1087
- variant: "outline",
1088
- size: "default",
1089
- onClick: startCustomization,
1090
- disabled: loadingPreferences,
1091
- "aria-label": t("appShell.customizeSidebar"),
1092
- children: [
1093
- CustomizeIcon,
1094
- loadingPreferences ? t("appShell.sidebarCustomizationLoading") : t("appShell.customizeSidebar")
1095
- ]
1096
- }
1097
- )
1098
- ] }),
766
+ ) : null,
1099
767
  shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
1100
768
  InjectionSpot,
1101
769
  {
@@ -1106,7 +774,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1106
774
  ] })
1107
775
  ] });
1108
776
  }
1109
- const gridColsClass = customizing ? "lg:grid-cols-[320px_1fr]" : effectiveCollapsed ? "lg:grid-cols-[72px_1fr]" : "lg:grid-cols-[240px_1fr]";
777
+ const gridColsClass = effectiveCollapsed ? "lg:grid-cols-[80px_1fr]" : "lg:grid-cols-[240px_1fr]";
1110
778
  const headerCtxValue = React.useMemo(() => ({
1111
779
  setBreadcrumb: setHeaderBreadcrumb,
1112
780
  setTitle: setHeaderTitle
@@ -1144,7 +812,23 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1144
812
  );
1145
813
  return /* @__PURE__ */ jsxs(HeaderContext.Provider, { value: headerCtxValue, children: [
1146
814
  /* @__PURE__ */ jsxs("div", { className: `min-h-svh lg:grid ${gridColsClass}`, children: [
1147
- /* @__PURE__ */ jsx("aside", { className: `${asideClassesBase} ${effectiveCollapsed ? "px-2" : "px-3"} hidden lg:block`, style: { width: asideWidth }, children: renderSidebar(effectiveCollapsed) }),
815
+ /* @__PURE__ */ jsxs("aside", { ref: sidebarAsideRef, className: `${asideClassesBase} ${effectiveCollapsed ? "px-2" : "px-3"} hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative`, style: { width: asideWidth }, children: [
816
+ renderSidebar(effectiveCollapsed),
817
+ sidebarScrollState !== "none" ? /* @__PURE__ */ jsx(
818
+ "div",
819
+ {
820
+ "aria-hidden": true,
821
+ className: "pointer-events-none absolute inset-x-0 bottom-0 flex h-10 items-end justify-center bg-gradient-to-t from-background via-background/80 to-transparent pb-1.5",
822
+ children: /* @__PURE__ */ jsx(
823
+ "span",
824
+ {
825
+ className: `inline-flex transition-transform duration-300 ${sidebarScrollState === "up" ? "rotate-180" : ""}`,
826
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-4 animate-bounce text-muted-foreground/70" })
827
+ }
828
+ )
829
+ }
830
+ ) : null
831
+ ] }),
1148
832
  /* @__PURE__ */ jsxs("div", { className: "flex min-h-svh flex-col min-w-0", children: [
1149
833
  /* @__PURE__ */ jsxs("header", { className: "border-b bg-background/80 px-3 lg:px-4 py-2 lg:py-3 flex items-center justify-between gap-2", children: [
1150
834
  /* @__PURE__ */ jsx(
@@ -1165,7 +849,6 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1165
849
  className: "hidden lg:inline-flex",
1166
850
  "aria-label": t("appShell.toggleSidebar"),
1167
851
  onClick: () => setCollapsed((c) => !c),
1168
- disabled: customizing,
1169
852
  children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1170
853
  /* @__PURE__ */ jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }),
1171
854
  /* @__PURE__ */ jsx("path", { d: "M9 4v16" })
@@ -1259,112 +942,6 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1259
942
  /* @__PURE__ */ jsx(UmesDevToolsPanel, {})
1260
943
  ] });
1261
944
  }
1262
- AppShell.cloneGroups = function cloneGroups(groups) {
1263
- const cloneItem = (item) => ({
1264
- id: item.id,
1265
- href: item.href,
1266
- title: item.title,
1267
- defaultTitle: item.defaultTitle,
1268
- icon: item.icon,
1269
- iconName: item.iconName,
1270
- iconMarkup: item.iconMarkup,
1271
- enabled: item.enabled,
1272
- hidden: item.hidden,
1273
- pageContext: item.pageContext,
1274
- children: item.children ? item.children.map((child) => cloneItem(child)) : void 0
1275
- });
1276
- return groups.map((group) => ({
1277
- id: group.id,
1278
- name: group.name,
1279
- defaultName: group.defaultName,
1280
- items: group.items.map((item) => cloneItem(item))
1281
- }));
1282
- };
1283
- function applyCustomizationDraft(baseGroups, draft) {
1284
- const clones = AppShell.cloneGroups(baseGroups);
1285
- const byId = /* @__PURE__ */ new Map();
1286
- for (const group of clones) {
1287
- byId.set(resolveGroupKey(group), group);
1288
- }
1289
- const orderedIds = mergeGroupOrder(draft.order, Array.from(byId.keys()));
1290
- const seen = /* @__PURE__ */ new Set();
1291
- const result = [];
1292
- for (const id of orderedIds) {
1293
- if (seen.has(id)) continue;
1294
- const group = byId.get(id);
1295
- if (!group) continue;
1296
- seen.add(id);
1297
- const baseName = group.defaultName ?? group.name;
1298
- const override = draft.groupLabels[id]?.trim();
1299
- result.push({
1300
- ...group,
1301
- name: override && override.length > 0 ? override : baseName,
1302
- items: group.items.map((item) => applyItemDraft(item, draft))
1303
- });
1304
- }
1305
- return result;
1306
- }
1307
- function applyItemDraft(item, draft) {
1308
- const itemKey = resolveItemKey(item);
1309
- const baseTitle = item.defaultTitle ?? item.title;
1310
- const override = draft.itemLabels[itemKey]?.trim();
1311
- const children = item.children ? item.children.map((child) => applyItemDraft(child, draft)) : void 0;
1312
- const hidden = draft.hiddenItemIds[itemKey] === true;
1313
- return {
1314
- ...item,
1315
- title: override && override.length > 0 ? override : baseTitle,
1316
- hidden,
1317
- children
1318
- };
1319
- }
1320
- function mergeGroupOrder(preferred, current) {
1321
- const seen = /* @__PURE__ */ new Set();
1322
- const merged = [];
1323
- for (const id of preferred) {
1324
- const trimmed = id.trim();
1325
- if (!trimmed || seen.has(trimmed) || !current.includes(trimmed)) continue;
1326
- seen.add(trimmed);
1327
- merged.push(trimmed);
1328
- }
1329
- for (const id of current) {
1330
- if (seen.has(id)) continue;
1331
- seen.add(id);
1332
- merged.push(id);
1333
- }
1334
- return merged;
1335
- }
1336
- function collectSidebarDefaults(groups) {
1337
- const groupDefaults = /* @__PURE__ */ new Map();
1338
- const itemDefaults = /* @__PURE__ */ new Map();
1339
- const visitItems = (items) => {
1340
- for (const item of items) {
1341
- const key = resolveItemKey(item);
1342
- const baseTitle = item.defaultTitle ?? item.title;
1343
- itemDefaults.set(key, baseTitle);
1344
- itemDefaults.set(item.href, baseTitle);
1345
- if (item.children && item.children.length > 0) visitItems(item.children);
1346
- }
1347
- };
1348
- for (const group of groups) {
1349
- const key = resolveGroupKey(group);
1350
- groupDefaults.set(key, group.defaultName ?? group.name);
1351
- visitItems(group.items);
1352
- }
1353
- return { groupDefaults, itemDefaults };
1354
- }
1355
- function filterMainSidebarGroups(groups) {
1356
- const isMainItem = (item) => {
1357
- if (item.pageContext && item.pageContext !== "main") return false;
1358
- return true;
1359
- };
1360
- return groups.map((group) => ({
1361
- ...group,
1362
- items: group.items.filter(isMainItem).map((item) => ({
1363
- ...item,
1364
- children: item.children?.filter(isMainItem)
1365
- }))
1366
- })).filter((group) => group.items.length > 0);
1367
- }
1368
945
  export {
1369
946
  AppShell,
1370
947
  ApplyBreadcrumb