Просмотр исходного кода

Categories next to the tabs in NEO list pages until the side panel shows (#5878)

* Categories next to the tabs in NEO list pages until the side panel shows

NEO's Conductor showed left-column menu blocks (`use_as_menu`) as dropdowns
next to the tabs only below its desktop switch (lg), but hides the left panel
until its own breakpoint (xl by default). In between, the block dropped into
the list with no padding. The Filters trigger had the same gap and sat on a
row of its own.

- The tab row's dropdowns, the Filters trigger and the list's exclusions now
  follow whether the left panel is on screen.
- The Filters trigger sits at the end of the tab row with the dropdowns.
- On the web both triggers are NeoButtons like the tab pills (glass, small
  capsule): the menu with a chevron, Filters with a sliders icon. They follow
  the header band's colour scheme too. Native is unchanged.
- The forum home's categories block is `use_as_menu` in a fresh install
  (bx_forum enable-app.sql), so default NEO shows a Categories dropdown in
  the tab row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Line the left panel's menu blocks up with its own links

The Conductor's left panel pulls its section links out with -mx-1, so their
icons align with the section title's icon. A menu block below them (forum
categories) kept the block's edges, so its links sat 4 px further in on each
side. The panel now passes `itemsClassName="-mx-1"` to its menu blocks
(ElementMenu uses it in place of `w-full`, which would cancel the right-hand
margin). The Filters sheet and other sidebars keep the old layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko 5 дней назад
Родитель
Сommit
b976b9fc2f

+ 5 - 0
modules/boonex/forum/install/sql/enable-app.sql

@@ -10,6 +10,11 @@ UPDATE `sys_objects_page` SET `config_api`='{\r\n    layout: \'navigator\',\r\n
 UPDATE `sys_objects_page` SET `config_api`='{\r\n    layout: \'navigator\',\r\n    blocks: {\r\n        browse: {\r\n            name: \'bx_forum:browse_new\',\r\n            showTitle: false,\r\n            showBg: false,\r\n            perLine: 1,\r\n        },\r\n        browse_sidebar: {\r\n            name: \'bx_forum:browse_popular\',\r\n            showTitle: true,\r\n            showBg: false,\r\n            sidebar: true,\r\n            unitType: \'small\',\r\n        },\r\n        categories: {\r\n            name: \'system:categories_list\',\r\n            showTitle: false,\r\n            showBg: true,\r\n            sidebar: false,\r\n            hidden: true,\r\n        },\r\n    },\r\n}' WHERE `object`='bx_forum_home';
 
 
+-- PAGES: blocks config_api
+-- Categories on the forum home: a dropdown next to the tabs while NEO's left panel is off screen.
+UPDATE `sys_pages_blocks` SET `config_api`='{"use_as_menu": true}' WHERE `object`='bx_forum_home' AND `module`='bx_forum' AND `title_system`='' AND `title`='_bx_forum_page_block_title_cats';
+
+
 -- PAGES: active_api
 UPDATE `sys_pages_blocks` SET `active_api`=1 WHERE `object`='bx_forum_create_entry' AND `module`='bx_forum' AND `title_system`='' AND `title`='_bx_forum_page_block_title_create_entry';
 UPDATE `sys_pages_blocks` SET `active_api`=1 WHERE `object`='bx_forum_edit_entry' AND `module`='bx_forum' AND `title_system`='' AND `title`='_bx_forum_page_block_title_edit_entry';

+ 4 - 2
neo/packages/app/components/elements/menu.js

@@ -31,7 +31,9 @@ function MenuChoiceRow({ item, href }) {
     )
 }
 
-export default function ElementMenu({ data, blockWrapperProps, url }) {
+// `itemsClassName`: classes for the list of links in place of `w-full`, e.g. the Conductor's left
+// panel lines them up with its own links (`-mx-1`: a fixed `w-full` would cancel the right margin).
+export default function ElementMenu({ data, blockWrapperProps, url, itemsClassName = '' }) {
     const MenuItemSidebarWithWrapper = components['menu-item']['sidebar_with_wrapper'];
     const currentPath = String(url || '');
     const currentPathComparable = currentPath.replace(/^\/+/, '');
@@ -137,7 +139,7 @@ export default function ElementMenu({ data, blockWrapperProps, url }) {
 
     return (
         <BlockWrapper {...blockWrapperProps}>
-            <View className="w-full gap-0.5">
+            <View className={`gap-0.5 ${itemsClassName || 'w-full'}`}>
                 {renderItems(topLevelItems)}
             </View>
         </BlockWrapper>

+ 1 - 1
neo/packages/app/ui/molecules/sections/conductor/helpers.js

@@ -976,7 +976,7 @@ export function partitionLeftColumn(leftColumnContent = [], layoutName) {
 
 /**
  * Ids that should not also render in the main list.
- * Desktop web: nothing excluded (right sidebar owns the left column).
+ * Left panel on screen (web, `isDesktop`): nothing excluded, the panel shows its blocks.
  * Navigator: all left-column ids. Else: menu + filter blocks only.
  */
 export function getLeftColumnExcludedFromMainIds({

+ 24 - 18
neo/packages/app/ui/molecules/sections/conductor/index.web.js

@@ -233,20 +233,6 @@ export function Conductor({
         ensureRouteInited(routes, index, setRoutes)
     }, [index])
 
-    // UNA's left column carries two different things: dropdown menus (shown next
-    // to the tabs) and filter blocks (sidebar on desktop, sheet on mobile).
-    const leftColumnContent = activeRoute?.leftbar?.content ?? EMPTY_CONTENT
-
-    const {
-        leftColumnMenuBlocks,
-        leftColumnFilterBlocks,
-        leftColumnExcludedFromMainIds,
-    } = useLeftColumn({
-        leftColumnContent,
-        layoutName,
-        isDesktop,
-    })
-
     /**
      * Route the *columns* render from. While a newly selected tab is still
      * loading, keep the previous one so the side columns do not collapse and
@@ -265,6 +251,22 @@ export function Conductor({
     ) || {}
     const isLeftColumnShown = isConductorColumnShown(columnCells.left?.breakpoint, breakpointName)
     const isRightColumnShown = isConductorColumnShown(columnCells.right?.breakpoint, breakpointName)
+
+    // UNA's left column carries two different things: dropdown menus and filter blocks. While
+    // the left panel is on screen they stay in it; below its breakpoint (phones, tablets and
+    // narrow desktops alike) the menus become dropdowns at the end of the tab row, the filters a
+    // "Filters" trigger beside them, and neither repeats in the list.
+    const leftColumnContent = activeRoute?.leftbar?.content ?? EMPTY_CONTENT
+
+    const {
+        leftColumnMenuBlocks,
+        leftColumnFilterBlocks,
+        leftColumnExcludedFromMainIds,
+    } = useLeftColumn({
+        leftColumnContent,
+        layoutName,
+        isDesktop: isLeftColumnShown,
+    })
     // /tasks-home only: pin the shell to the leftover viewport and scroll the
     // list inside it so the site header + task toolbar stay put.
     const isTasksHome = isTasksHomeUri(activeRoute?.pageData?.uri) || isTasksHomeUri(activeRoute?.key)
@@ -280,9 +282,9 @@ export function Conductor({
         data?.cover_block?.profile?.id === data?.context?.current?.id &&
         isDesktop
 
-    // Mobile: Filters sheet for use_as_filter (any layout) + navigator non-menu left column.
-    // On desktop the same blocks live in the left column, so no button is needed.
-    const showFiltersBtn = !isDesktop && leftColumnFilterBlocks.length > 0
+    // Filters sheet for use_as_filter (any layout) + navigator non-menu left column, while the
+    // left panel that otherwise holds those blocks is off screen.
+    const showFiltersBtn = !isLeftColumnShown && leftColumnFilterBlocks.length > 0
     /** Profile chrome is rendered inline here instead of in the page header. */
     const isUseCurrentHeader = layoutName === 'profile' && (!isCoverDisabled || !isDesktop)
 
@@ -351,6 +353,7 @@ export function Conductor({
                 setIndex={setIndex}
                 onChangeRoute={onChangeRoute}
                 leftColumnMenus={leftColumnMenuBlocks}
+                showLeftColumnMenus={!isLeftColumnShown}
                 showFilters={showFiltersBtn}
                 onShowFilters={showFilters}
             />
@@ -373,6 +376,7 @@ export function Conductor({
             setIndex,
             onChangeRoute,
             leftColumnMenuBlocks,
+            isLeftColumnShown,
             showFiltersBtn,
             showFilters,
         ]
@@ -393,7 +397,9 @@ export function Conductor({
             routes={routes}
             stickyColumnStyle={stickyColumnStyle}
         >
-            <LeftColumnContent route={tabRoute} onChange={onFormChangedValues} />
+            {/* Menu blocks' links line up with the panel's own (LeftColumnContainer pulls those
+                out with -mx-1, so their icons align with the section title's). */}
+            <LeftColumnContent route={tabRoute} onChange={onFormChangedValues} menuItemsClassName="-mx-1" />
         </LeftColumnContainer>
     ) : null
 

+ 36 - 3
neo/packages/app/ui/molecules/sections/conductor/shared-ui.js

@@ -5,7 +5,7 @@ import { Icon } from 'app/ui/atoms/icon'
 import DropdownMenu, { DropdownMenuOpenContext } from 'app/ui/atoms/dropdown-menu'
 import UniList from 'app/ui/atoms/unilist'
 import { BlockByName, BlockByData } from 'app/components/block'
-import { findElementById, hasNonEmptyContent } from 'app/lib/util'
+import { findElementById, hasNonEmptyContent, isWeb } from 'app/lib/util'
 import { ItemRenderer } from 'app/components/item-renderer'
 import { NeoButton } from 'app/design/controls'
 import { cueAttributes, PressCueContext } from 'app/lib/sound'
@@ -79,6 +79,7 @@ export const LeftColumnContent = memo(function LeftColumnContent({
     saveOnChanges = false,
     restoreFormValues = false,
     className = 'gap-y-4',
+    menuItemsClassName,
 }) {
     const items = itemsProp ?? route?.leftbar?.content ?? []
     if (!items.length) return null
@@ -90,7 +91,13 @@ export const LeftColumnContent = memo(function LeftColumnContent({
           )
         : (data ?? route?.pageData)
 
-    const blockProps = { onChange, onFormSubmit, saveOnChanges, sidebar: true }
+    const blockProps = {
+        onChange,
+        onFormSubmit,
+        saveOnChanges,
+        sidebar: true,
+        ...(menuItemsClassName ? { itemsClassName: menuItemsClassName } : null),
+    }
 
     return (
         <View className={className}>
@@ -153,11 +160,16 @@ export function openFiltersSheet(
     })
 }
 
-/** Filters trigger under the tab bar (native / mobile web). */
+/**
+ * Filters trigger: under the tab bar on native; in the tab row on the web, where it takes the
+ * tab pills' look (`style` / `image`, see TabBar in web-ui.js).
+ */
 export function FiltersButton({
     onPress,
     visible = true,
     className = 'items-start px-3 py-2',
+    style,
+    image,
 }) {
     const { t } = useTranslation()
     if (!visible) return null
@@ -167,6 +179,8 @@ export function FiltersButton({
                 label={t('Filters')}
                 controlSize="small"
                 borderShape="capsule"
+                {...(style ? { style } : null)}
+                {...(image ? { image } : null)}
                 onPress={onPress}
             />
         </View>
@@ -230,6 +244,25 @@ export function LeftColumnMenuDropdown({ blockItem, t }) {
     const title = t(blockItem?.data?.title || 'Menu')
     const icon = blockItem?.data?.config_api?.icon || 'LayoutGrid'
 
+    // Web: a NeoButton like the tab pills beside it (glass, small capsule), so it follows the
+    // header band's colour scheme too; the chevron says it opens a menu.
+    if (isWeb)
+        return (
+            <DropdownMenu
+                items={items}
+                mode="popup"
+                triggerAccessibilityLabel={title}
+                buttonProps={{
+                    label: title,
+                    image: 'ChevronDown',
+                    imagePlacement: 'trailing',
+                    style: 'glass',
+                    controlSize: 'small',
+                    borderShape: 'capsule',
+                }}
+            />
+        )
+
     return (
         <DropdownMenu
             items={items}

+ 2 - 1
neo/packages/app/ui/molecules/sections/conductor/use-conductor.js

@@ -24,7 +24,8 @@ import {
  * @param {object} opts
  * @param {array}  opts.leftColumnContent
  * @param {string} opts.layoutName
- * @param {boolean} [opts.isDesktop=false] — web only; native always false
+ * @param {boolean} [opts.isDesktop=false] — web: the left panel is on screen (its blocks stay in it);
+ *   native always false
  */
 export function useLeftColumn({
     leftColumnContent = [],

+ 35 - 24
neo/packages/app/ui/molecules/sections/conductor/web-ui.js

@@ -1075,7 +1075,8 @@ export const RightColumnContainer = ({ route, stickyColumnStyle }) => {
  * @param {Function} props.setIndex
  * @param {Function} [props.onChangeRoute]
  * @param {array}    [props.leftColumnMenus] Left-column blocks shown as dropdowns.
- * @param {boolean}  [props.showFilters]     Render the "Filters" button.
+ * @param {boolean}  [props.showLeftColumnMenus] Show them: the left panel is off screen.
+ * @param {boolean}  [props.showFilters]     Render the "Filters" button (in the tab row).
  * @param {Function} [props.onShowFilters]
  */
 export const HeaderContainer = ({
@@ -1092,6 +1093,7 @@ export const HeaderContainer = ({
     setIndex,
     onChangeRoute,
     leftColumnMenus = [],
+    showLeftColumnMenus,
     showFilters = false,
     onShowFilters,
 }) => {
@@ -1123,24 +1125,20 @@ export const HeaderContainer = ({
     }, [])
 
     const tabBarContent = (
-        <>
-            <TabBar
-                menu={menu}
-                routes={routes}
-                layoutName={layoutName}
-                index={index}
-                setIndex={setIndex}
-                onChangeRoute={onChangeRoute}
-                pageData={pageData}
-                leftColumnMenus={leftColumnMenus}
-                contentWidth={contentWidth}
-            />
-            <FiltersButton
-                visible={showFilters}
-                onPress={onShowFilters}
-                className="items-start px-3 lg:px-4 py-2"
-            />
-        </>
+        <TabBar
+            menu={menu}
+            routes={routes}
+            layoutName={layoutName}
+            index={index}
+            setIndex={setIndex}
+            onChangeRoute={onChangeRoute}
+            pageData={pageData}
+            leftColumnMenus={leftColumnMenus}
+            showLeftColumnMenus={showLeftColumnMenus}
+            showFilters={showFilters}
+            onShowFilters={onShowFilters}
+            contentWidth={contentWidth}
+        />
     )
 
     if (useMorph) {
@@ -1253,13 +1251,19 @@ export const TabBar = ({
     onChangeRoute,
     omitDefaultBackground = false,
     leftColumnMenus = [],
+    showLeftColumnMenus,
+    showFilters = false,
+    onShowFilters,
     contentWidth,
 }) => {
     const { t } = useTranslation()
     const { layoutName: layout } = useLayoutSettings()
     const menuSettings = getMenuSettings(menu.object, menu.config, menu)
     const isDesktop = useIsDesktop()
-    const hasLeftColumnMenus = !isDesktop && leftColumnMenus.length > 0
+    // Left-column menus and the Filters trigger sit at the end of the tab row while the left
+    // panel is off screen (below its breakpoint, which can be wider than `isDesktop`).
+    const hasLeftColumnMenus = (showLeftColumnMenus ?? !isDesktop) && leftColumnMenus.length > 0
+    const hasRowEnd = hasLeftColumnMenus || showFilters
 
     if (!routes.length) return null
 
@@ -1276,7 +1280,7 @@ export const TabBar = ({
     const isShowSecondLine = (
         routes.length > 1 ||
         !!pageData.cover_block?.actions_menu ||
-        hasLeftColumnMenus
+        hasRowEnd
     )
     return (
         <TopSidebar
@@ -1297,15 +1301,22 @@ export const TabBar = ({
                         onChangeRoute={onChangeRoute}
                     />}
                 </View>
-                    {hasLeftColumnMenus && (
-                        <Row className={`items-center gap-1 shrink-0 pl-1${showActionsInTabBar ? '' : ' pr-3 lg:pr-4'}`}>
-                            {leftColumnMenus.map((blockItem) => (
+                    {hasRowEnd && (
+                        <Row className={`items-center gap-2 shrink-0 pl-1${showActionsInTabBar ? '' : ' pr-3 lg:pr-4'}`}>
+                            {hasLeftColumnMenus && leftColumnMenus.map((blockItem) => (
                                 <LeftColumnMenuDropdown
                                     key={blockItem.id ?? blockItem.block?.name}
                                     blockItem={blockItem}
                                     t={t}
                                 />
                             ))}
+                            <FiltersButton
+                                visible={showFilters}
+                                onPress={onShowFilters}
+                                className=""
+                                style="glass"
+                                image="SlidersHorizontal"
+                            />
                         </Row>
                     )}
                     {!!pageData.cover_block?.actions_menu && showActionsInTabBar ? (