Avo Documentation

repository·main·Indexed 23 days ago

https://github.com/avo-hq/avo

Avo is a highly customizable Admin Panel Framework, Content Management System, and Internal Tool Builder for Ruby on Rails. It enables developers to build administrative interfaces using pure Ruby code, featuring resource management for Active Record models, dashboards, global search, and ActiveStorage integration. The framework leverages Hotwire for its UI/UX and Pundit for authorization.

Tokens
20.5K
Snippets
13
Records
99
Agent score
82%

What's inside Avo

  1. Overview of Avo features

    main

    Avo is a Ruby on Rails Admin Panel Framework, CMS, and Internal Tool Builder. Key capabilities include:

    • Resource Management: CRUD interfaces for Active Record models via code-driven configuration.
    • Dashboarding: Metrics, charts, and custom cards.
    • Search: Global resource search and fuzzy-searchable associations.
    • Associations: Full support for belongs_to, has_many, and polymorphic associations.
    • File Handling: One-line integration with ActiveStorage.
    • Customization: Custom fields, custom tools (to break out of CRUD), and custom filters.
    • Security: Authorization leveraging Pundit policies.
    • UI/UX: Powered by Hotwire, supports grid views, tabbed interfaces, and mobile-friendly layouts.
  2. Understand the Resizable Sidebar implementation plan

    main

    The Resizable Sidebar feature is being implemented in three distinct phases to ensure a stable foundation before adding interactive elements:

    1. Phase 1 — Foundation (no interaction): Focuses on CSS architecture, server-side parsing of width preferences, and width synchronization via Turbo/cross-tab. This phase includes making .container-small max-width-based and implementing label ellipsis.
    2. Phase 2 — Handle and drag: Introduces the physical UI elements (handles), drag mechanics, and the persistence layer to commit width changes.
    3. Phase 3 — Keyboard and interlocks: Adds accessibility (ARIA, keyboard support) and lifecycle interlocks.

    This phased approach ensures that visual layout and server-side state are correct before user interaction is enabled.

  3. Understand the Resizable Sidebar behavior

    main

    Avo features a resizable sidebar that allows users to adjust the width of the navigation area.

    Key behaviors:

    • Continuous Dragging: Users can drag the sidebar handle to resize. The sidebar has a minimum width (200px) and a maximum width (1440px or 40% of the viewport width, whichever is smaller).
    • Persistence: Sidebar width is persisted via a cookie. The cookie key is determined by window.Avo.configuration.cookies_key.
    • Resetting: Double-clicking the resize handle resets the sidebar to its default width (256px). This action removes the persistence cookie and the --sidebar-width-stored CSS property.
    • Keyboard Control: Users can resize using arrow keys. ArrowLeft/ArrowRight steps by 16px, or 64px with a modifier. Home sets to minimum width, and End sets to maximum width.
    • RTL Support: In Right-to-Left (RTL) layouts, dragging away from the sidebar widens it, and ArrowLeft increases the width.
  4. Use dynamic blocks in `row_options` with `Avo::ExecutionContext`

    main

    The row_options configuration can accept a Hash or a block that returns a Hash. Furthermore, individual attribute values can be blocks that are evaluated per-record using Avo::ExecutionContext.

    When using blocks, you have access to the following local variables:

    • record: The current record being rendered.
    • resource: The resource class.
    • view: The context of the table, either :index or :has_many (derived automatically).

    Return Type Contract

    • nil or false: The attribute is omitted entirely.
    • String or Symbol: Converted to a string.
    • Array<String> or Hash<String, Boolean>: Only valid for the class key (processed via class_names).
    • Other types: Will raise an ArgumentError in development/test environments.
  5. Use blocks in `row_options` for dynamic attributes

    main

    To make row attributes dynamic based on the record being rendered, you can use blocks. Blocks are evaluated within an Avo::ExecutionContext and have access to the following local variables:

    • record: The current record instance.
    • resource: The resource class.
    • view: The render context. It is either :index (for the main index table) or :has_many (for association tables on parent show pages).

    Evaluation Rules

    • Performance: Blocks are re-evaluated on every render and are not memoized. Ensure blocks are efficient and preload any associations used within them via self.includes.
    • Return Types:
      • nil or false: The attribute is omitted.
      • String or Symbol: Coerced via .to_s.
      • Array<String> or Hash<String, Boolean>: Valid only for the class: key.
      • Other types: Raise an ArgumentError in development/test.
    class Avo::Resources::Message < Avo::BaseResource
      self.table_view = {
        row_options: -> {
          {
            class: record.role == "agent" ? "bg-blue-50" : "",
            data: { role: record.role },
            title: "Message from #{record.role}"
          }
        }
      }
    end
  6. Understand server-side tab selection parameters

    main

    When implementing manual triggers for tabs, ensure you provide the correct parameters to the request. Server-side tab selection relies on specific keys:

    1. tab-group_<id>: The group parameter used to identify the tab group.
    2. tab_turbo_frame: Used by TabGroupComponent#is_not_loaded? to manage state.

    Warning: The parameter active_tab_title is written into URLs by the component but is not consumed by the server. Including it in your manual trigger will not affect rendering.

  7. Difference between brand overrides and theme selection

    main

    It is important to distinguish between install-level branding and per-user theme selection:

    1. Install-level branding (neutral_colors:, accent_colors:): This defines the colors that represent your application's specific brand. These colors are applied to the :root and .dark CSS variables. When a user opens the theme picker, the "Brand" option will reflect these custom colors.

    2. Per-user theme selection (neutral:, accent:): This allows individual users to switch between pre-defined color palettes (like slate, stone, blue, etc.). These themes use specific CSS classes (e.g., .neutral-theme-slate) which have higher specificity than the :root brand colors.

    Note: The legacy Hash form of neutral: and accent: (used to pass color values) has been removed. These keys now only accept Symbols to represent the default selection.

  8. Understand the Resizable Sidebar implementation details

    main

    The resizable sidebar is implemented as a div with role="separator" and tabindex="0".

    Accessibility (A11y)

    • ARIA Attributes: The separator uses aria-orientation="vertical", aria-controls="main-sidebar", and static server-computed aria-valuenow, aria-valuemin, and aria-valuemax attributes.
    • Targeting: The aria-controls attribute points to the element with id="main-sidebar" (the wrapper containing .avo-sidebar).
    • Keyboard/Touch: The handle is hidden via display: none on small viewports (< lg) and for devices with coarse pointers (@media (pointer: coarse)) to ensure it is removed from the tab order and is not a pointer target.

    Visual and Interaction Behavior

    • Cursor: The handle unconditionally displays cursor: col-resize to signal discoverability.
    • Grip: The visual grip is a pseudo-element (::after) to avoid interfering with the role="separator" which forces role="presentation" on descendants.
    • Drag State: To prevent accidental triggers, the data-sidebar-resizing state is only applied after a pointermove event crosses a 3px travel threshold, rather than immediately on pointerdown.
    • Reduced Motion: Transitions for the handle are zeroed out when @media (prefers-reduced-motion: reduce) is active.
    • RTL Support: The implementation uses inset-inline-start and start/end properties to support Right-to-Left layouts.
  9. Override the sidebar width via CSS

    main

    The sidebar width is driven by the CSS custom property --sidebar-width on the .main element. If a host application redeclares this property, it will defeat the stored user preference.

    Additionally, the .container-small class is now subject to new clamping logic related to the sidebar width. If you override .container-small, be aware that it is now constrained by the sidebar's dimensions.

  10. Handle manual frame loading failures and retries

    main

    When a manual Turbo Frame load fails (e.g., a 500 error or a network timeout), Avo displays an inline error message and a Retry button within the frame itself. This prevents the application from redirecting to a global failed_to_load page.

    Error States

    • HTTP 500: The frame catches the error and shows the error state.
    • Network/4xx Errors: The manual-frame Stimulus controller handles turbo:fetch-request-error and non-2xx responses to ensure the frame doesn't simply go blank.
    • Retry Mechanism: Clicking the Retry button re-issues the request using the same deferred load URL.
  11. Understand the Resizable Sidebar implementation details

    main

    The Resizable Sidebar feature uses a combination of server-side rendering and client-side interaction to manage sidebar width.

    Key Technical Details:

    • Width Storage: The server-rendered width is carried by the <html> element via a nonce'd pre-paint script. This ensures the width is available before the body is parsed.
    • CSS Variables: The sidebar host uses --sidebar-width (declared in layout.css) which derives its value from the inherited stored value within the lg breakpoint.
    • The Resize Handle: The handle is a position: fixed sibling overlay. It is positioned at inset-inline-start: calc(var(--sidebar-offset-size) − 2px), with a top of var(--top-navbar-height) and a height of calc(100dvh − var(--top-navbar-height)). Its z-index is set between .avo-sidebar (50) and .top-navbar (400).
    • Grab Zone: The interactive grab zone is 15px wide, spanning from [offset − 2px, offset + 13px].
    • Interaction Model: Resizing is performed via a rAF-throttled (requestAnimationFrame) live write to the documentElement during dragging.
    • Turbo Integration: To ensure the sidebar width persists across Turbo navigations, the turbo:before-render hook is used for reconciliation.
  12. Understand the Resizable Sidebar behavior and constraints

    main

    Avo features a resizable main navigation sidebar for desktop users (≥ lg breakpoint). The sidebar width is controlled by a draggable divider between the sidebar and the .main-content area.

    Key Constraints

    • Width Bounds: The sidebar is clamped between a minimum of 200px and a maximum of min(480px, 40vw).
    • Responsive Behavior:
      • Desktop (≥ lg, 1024px): The sidebar is resizable. The stored width is applied. The resize handle is visible to fine-pointer devices (hover-reveal model).
      • Mobile (< lg): The sidebar is not resizable. It ignores any stored width and defaults to 256px. The resize handle is not rendered.
      • Sidebar Closed: When the sidebar is toggled off, the handle is removed from the DOM and the tab order.
    • Persistence: The chosen width is stored in a persistent cookie (with max_age, path: "/", and SameSite=Lax). This width survives browser restarts and is applied server-side on the first paint to prevent layout shifts.
    • Label Overflow: Sidebar link labels will automatically truncate with an ellipsis when they exceed the current width, with the full label available on hover.