Class: LcpRuby::Metadata::MenuItem

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/metadata/menu_item.rb

Constant Summary collapse

TYPES =
%i[view_group link group separator sidebar_toggle].freeze
POSITION_BOTTOM =
"bottom"
POSITION_RIGHT =
"right"
POSITION_TOP =
"top"
POSITION_LEFT =
"left"
POSITIONS =
[ POSITION_BOTTOM, POSITION_RIGHT, POSITION_TOP, POSITION_LEFT ].freeze
MAX_NESTING_DEPTH =
2
BUILTIN_ACTIONS =

Built-in CRUD actions implicit on every presenter. Used by ‘meets_visibility?` to pick `can?(action)` (built-in) vs `can_execute_action?(action)` (custom) for permission gating of `presenter:` items, and by `MenuItemResolver` to pick the correct route helper.

%w[show edit new destroy index].freeze
DESTINATION_KEYS =

Destination keys — exactly one must appear on every non-widget item. ‘widget:` (PR #2b) accepts `provider:` only; that mutex is layered on top in PR #2b’s MenuItem extensions.

%w[url presenter view_group provider children separator sidebar_toggle].freeze
KNOWN_KEYS =

Every key the MenuItem parser knows about. Anything outside this set is rejected at boot — the schema also has ‘additionalProperties: false` on menu_item but that runs only via `lcp_ruby:validate`, not at `rails s` time. Parse-time rejection turns silent typos (`section: “Marketplace”` instead of `label:`) into immediate boot errors rather than icon-only menu items the configurator hunts for an hour. Keys starting with `_` are reserved for internal DSL annotations (`_label_source_loc`, `_aria_label_source_loc`) and allowed without enumeration. NOTE: `type` is intentionally NOT here. It’s an internal kwarg (derived from the destination key, round-tripped via ‘to_kwargs` →`new` at the Ruby level, never read from author YAML). Listing it would let `type:` pass this parse-time check while the JSON schema (`additionalProperties: false`, no `type` property) rejects it under `lcp_ruby:validate` — two different verdicts for the same file.

%w[
  url presenter view_group provider children separator sidebar_toggle widget
  method alias action defaults
  render render_panel panel_provider options
  label label_key icon aria_label aria_label_key
  visible_when disable_when position badge
  responsive_priority pin hide_below show_below collapse_label_below
  _label_source_loc _aria_label_source_loc
].freeze
SEPARATOR_ALLOWED_KEYS =

Keys allowed on a ‘:separator` item. Anything else is a configurator footgun — a separator with a `label:` is invisible (no element renders it); a separator with `disable_when:` makes no sense (nothing to disable). Validator rejects at boot.

%w[separator position visible_when].freeze
%w[
  sidebar_toggle label label_key icon aria_label aria_label_key
  position visible_when
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(type:, view_group_name: nil, label: nil, label_key: nil, icon: nil, url: nil, children: [], visible_when: {}, disable_when: {}, position: nil, badge: nil, aria_label: nil, aria_label_key: nil, http_method: nil, presenter_slug: nil, alias_name: nil, action_name: nil, defaults: nil, render_partial: nil, render_panel_partial: nil, widget: nil, provider_name: nil, provider_options: nil, panel_provider_name: nil, label_source_loc: nil, aria_label_source_loc: nil, responsive_priority: nil, pin: nil, hide_below: nil, show_below: nil, collapse_label_below: nil) ⇒ MenuItem

Returns a new instance of MenuItem.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 32

def initialize(type:, view_group_name: nil, label: nil, label_key: nil, icon: nil,
               url: nil, children: [], visible_when: {}, disable_when: {},
               position: nil, badge: nil, aria_label: nil, aria_label_key: nil,
               http_method: nil, presenter_slug: nil, alias_name: nil, action_name: nil,
               defaults: nil,
               render_partial: nil, render_panel_partial: nil, widget: nil,
               provider_name: nil, provider_options: nil, panel_provider_name: nil,
               label_source_loc: nil, aria_label_source_loc: nil,
               responsive_priority: nil, pin: nil, hide_below: nil,
               show_below: nil, collapse_label_below: nil)
  @type = type
  @view_group_name = view_group_name
  # `label` may be a String, `false` (explicit opt-out from any
  # resolved fallback — relevant for view_group items where the
  # presenter would otherwise supply a label), or nil/omitted.
  @label = label
  @label_key = label_key
  @icon = icon
  @url = url
  @children = children
  @visible_when = HashUtils.stringify_deep(visible_when || {})
  @disable_when = HashUtils.stringify_deep(disable_when || {})
  @position = position
  @badge = badge.is_a?(Hash) ? HashUtils.stringify_deep(badge) : nil
  @aria_label = aria_label
  @aria_label_key = aria_label_key
  # `method:` (YAML key) is stored as `http_method` to avoid
  # shadowing `Object#method(name)`. Normalized to a lowercase
  # symbol (`:get`, `:post`, `:put`, `:patch`, `:delete`); `nil`
  # means "unspecified" (renderer picks a default — `:get` for
  # url:/view_group:/presenter+show, etc.).
  @http_method = http_method&.to_s&.downcase&.to_sym
  @presenter_slug = presenter_slug&.to_s
  @alias_name = alias_name&.to_s
  @action_name = action_name&.to_s
  @defaults = defaults.is_a?(Hash) ? HashUtils.stringify_deep(defaults) : nil
  # PR #2b modifiers: `render:` partial path, `render_panel:`
  # partial path, `widget:` flag, `provider:` and `panel_provider:`
  # service names, and the shared `options:` hash both providers
  # consume (mutex with each other so no key collision).
  @render_partial = render_partial&.to_s
  @render_panel_partial = render_panel_partial&.to_s
  @widget = widget == true
  @provider_name = provider_name&.to_s
  @provider_options = provider_options.is_a?(Hash) ? HashUtils.stringify_deep(provider_options) : {}
  @panel_provider_name = panel_provider_name&.to_s
  # i18n_check Phase 3a — populated when a menu DSL builder is
  # added (none exists today; menu is YAML-only). YAML-loaded
  # menus carry nil and Pass 3 covers them.
  @label_source_loc = label_source_loc
  @aria_label_source_loc = aria_label_source_loc

  # Responsive per-item keys (all optional; nil = no effect at
  # runtime). Type coercion only — depth/layout-aware warnings
  # live in ConfigurationValidator.
  @responsive_priority = responsive_priority&.to_i
  @pin = pin&.to_s
  @hide_below = hide_below&.to_i
  @show_below = show_below&.to_i
  @collapse_label_below = collapse_label_below&.to_i

  validate!
end

Instance Attribute Details

#action_nameObject (readonly)

Returns the value of attribute action_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def action_name
  @action_name
end

#alias_nameObject (readonly)

Returns the value of attribute alias_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def alias_name
  @alias_name
end

#aria_labelObject (readonly)

Returns the value of attribute aria_label.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def aria_label
  @aria_label
end

#aria_label_keyObject (readonly)

Returns the value of attribute aria_label_key.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def aria_label_key
  @aria_label_key
end

#aria_label_source_locObject (readonly)

Returns the value of attribute aria_label_source_loc.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def aria_label_source_loc
  @aria_label_source_loc
end

#badgeObject (readonly)

Returns the value of attribute badge.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def badge
  @badge
end

#childrenObject (readonly)

Returns the value of attribute children.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def children
  @children
end

#collapse_label_belowObject (readonly)

Returns the value of attribute collapse_label_below.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def collapse_label_below
  @collapse_label_below
end

#defaultsObject (readonly)

Returns the value of attribute defaults.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def defaults
  @defaults
end

#disable_whenObject (readonly)

Returns the value of attribute disable_when.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def disable_when
  @disable_when
end

#hide_belowObject (readonly)

Returns the value of attribute hide_below.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def hide_below
  @hide_below
end

#http_methodObject (readonly)

Returns the value of attribute http_method.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def http_method
  @http_method
end

#iconObject (readonly)

Returns the value of attribute icon.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def icon
  @icon
end

#labelObject (readonly)

Returns the value of attribute label.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def label
  @label
end

#label_keyObject (readonly)

Returns the value of attribute label_key.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def label_key
  @label_key
end

#label_source_locObject (readonly)

Returns the value of attribute label_source_loc.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def label_source_loc
  @label_source_loc
end

#panel_provider_nameObject (readonly)

Returns the value of attribute panel_provider_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def panel_provider_name
  @panel_provider_name
end

#pinObject (readonly)

Returns the value of attribute pin.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def pin
  @pin
end

#positionObject (readonly)

Returns the value of attribute position.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def position
  @position
end

#presenter_slugObject (readonly)

Returns the value of attribute presenter_slug.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def presenter_slug
  @presenter_slug
end

#provider_nameObject (readonly)

Returns the value of attribute provider_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def provider_name
  @provider_name
end

#provider_optionsObject (readonly)

Returns the value of attribute provider_options.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def provider_options
  @provider_options
end

#render_panel_partialObject (readonly)

Returns the value of attribute render_panel_partial.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def render_panel_partial
  @render_panel_partial
end

#render_partialObject (readonly)

Returns the value of attribute render_partial.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def render_partial
  @render_partial
end

#responsive_priorityObject (readonly)

Returns the value of attribute responsive_priority.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def responsive_priority
  @responsive_priority
end

#show_belowObject (readonly)

Returns the value of attribute show_below.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def show_below
  @show_below
end

#typeObject (readonly)

Returns the value of attribute type.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def type
  @type
end

#urlObject (readonly)

Returns the value of attribute url.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def url
  @url
end

#view_group_nameObject (readonly)

Returns the value of attribute view_group_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def view_group_name
  @view_group_name
end

#visible_whenObject (readonly)

Returns the value of attribute visible_when.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def visible_when
  @visible_when
end

#widgetObject (readonly)

Returns the value of attribute widget.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def widget
  @widget
end

Class Method Details

.from_hash(hash) ⇒ Object

Build a MenuItem from a parsed YAML hash. Detection priority: separator > view_group > children > url|presenter Existing call style preserved: ‘from_hash(“view_group” => “x”, …)` works because the method takes a single positional Hash. `type` is NOT an accepted key here — reject_unknown_keys! (in the shared build_from_hash) rejects it, matching the JSON schema, which has no `type` property. Type is always re-derived from the destination keys present. Provider returns that echo a `type:` reach build_from_hash via `from_hash_at_depth` (not this method), but LayoutHelper#filter_provider_keys strips it upstream (PROVIDER_STRIP_KEYS) so the round-trip still works.



114
115
116
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 114

def self.from_hash(hash)
  build_from_hash(hash, 0)
end

.from_hash_at_depth(hash, depth) ⇒ Object

Variant that threads an explicit depth budget through static ‘:children:` recursion AND provider expansion (PR #2b). Public so `LayoutHelper#expand_provider` can pass the running depth without re-implementing the construction logic. The leading positional `hash` keeps Ruby 3 from misinterpreting string-key hash literals as kwargs.



124
125
126
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 124

def self.from_hash_at_depth(hash, depth)
  build_from_hash(hash, depth)
end

Instance Method Details

#badge_formObject



578
579
580
581
582
583
584
585
586
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 578

def badge_form
  if badge&.dig("renderer")
    :renderer
  elsif badge&.dig("partial")
    :partial
  elsif badge&.dig("template")
    :template
  end
end

#badge_optionsObject



600
601
602
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 600

def badge_options
  badge&.dig("options") || {}
end

#badge_partialObject



592
593
594
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 592

def badge_partial
  badge&.dig("partial")
end

#badge_providerObject



570
571
572
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 570

def badge_provider
  badge&.dig("provider")
end

#badge_rendererObject



588
589
590
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 588

def badge_renderer
  badge&.dig("renderer")
end

#badge_templateObject



596
597
598
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 596

def badge_template
  badge&.dig("template")
end

#bottom?Boolean

Returns:

  • (Boolean)


604
605
606
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 604

def bottom?
  position.to_s == POSITION_BOTTOM
end

#contains_slug?(slug, loader) ⇒ Boolean

Recursively check if this item or any descendant contains the given slug

Returns:

  • (Boolean)


463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 463

def contains_slug?(slug, loader)
  if view_group?
    vg = loader.view_group_definitions[view_group_name]
    return false unless vg

    all_slugs = vg.page_names.filter_map do |name|
      loader.page_definitions[name]&.slug
    end
    return true if all_slugs.include?(slug)
  end

  children.any? { |child| child.contains_slug?(slug, loader) }
end

#group?Boolean

Returns:

  • (Boolean)


631
632
633
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 631

def group?
  type == :group
end

#has_badge?Boolean

Returns:

  • (Boolean)


574
575
576
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 574

def has_badge?
  badge_provider.present?
end

#left?Boolean

Returns:

  • (Boolean)


619
620
621
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 619

def left?
  position.to_s == POSITION_LEFT
end

#link?Boolean

Returns:

  • (Boolean)


627
628
629
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 627

def link?
  type == :link
end

#meets_enabled?(user, _helper = nil) ⇒ Boolean

Mirror of ‘meets_visibility?` but for `disable_when:`. Returns true when the item should render in its enabled (clickable) state. Returns false when `disable_when:` evaluates true →rendering applies `aria-disabled` / `<button disabled>` plus the `lcp-menu-item–disabled` CSS class.

On condition errors: dev/test re-raises, production records and returns true (item enabled). The “enabled by default on error” bias mirrors today’s failure-modes table — disable_when is a UI affordance, not a security boundary, so its failure should not silently block users from a working endpoint.

Returns:

  • (Boolean)


508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 508

def meets_enabled?(user, _helper = nil)
  return true if disable_when.blank?

  !LcpRuby::ConditionEvaluator.evaluate_any(
    nil,
    disable_when,
    context: { current_user: user }
  )
rescue LcpRuby::ConditionError => e
  raise unless defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
  LcpRuby.record_error(e, subsystem: "menu_disable", item: label || view_group_name || url)
  true
end

#meets_visibility?(user, helper = nil) ⇒ Boolean

Menu items evaluate visibility from two stacks: the ‘visible_when:` condition tree (record-free; supports service: and compound all/any/not — `field:`/`collection:` are rejected by the validator) AND, for `presenter:` items, the same permission gate the presenter toolbar runs (`can_access_presenter?` plus `can?(action)` for built-in CRUD or `can_execute_action?` for custom actions).

The optional ‘helper` arg lets the layout helper share its per-render `@permission_cache` so N siblings with the same presenter pay the evaluator construction cost once. When `helper` is nil (specs and any non-helper context) the method falls back to constructing a fresh evaluator per call —functionally identical, just slower.

Returns:

  • (Boolean)


491
492
493
494
495
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 491

def meets_visibility?(user, helper = nil)
  return false unless meets_condition_visibility?(user)
  return true unless presenter_slug.present?
  meets_presenter_permission?(user, helper)
end

#render_panel_partial_pathObject

‘<namespace>/menu_renderers/<name>` — Rails partial path for the panel renderer. Same naming convention as `render_partial_path`; the role distinguishing `render:` from `render_panel:` lives at the YAML key level.



738
739
740
741
742
743
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 738

def render_panel_partial_path
  return nil if render_panel_partial.blank?
  ns, name = render_panel_partial.split("/", 2)
  return nil if name.blank?
  "#{ns}/menu_renderers/#{name}"
end

#render_partial_pathObject

‘<namespace>/menu_renderers/<name>` — Rails partial path for the trigger renderer. Returns nil when `render:` is unset so callers can guard with `if render_partial.present?`.



727
728
729
730
731
732
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 727

def render_partial_path
  return nil if render_partial.blank?
  ns, name = render_partial.split("/", 2)
  return nil if name.blank?
  "#{ns}/menu_renderers/#{name}"
end

#resolved_aria_label(_loader = nil) ⇒ Object

Resolve the accessible name for the link/button. Required when no visible label is rendered (icon-only items). Mirrors ‘resolved_label` priority: explicit aria_label_key > literal aria_label with i18n auto-lookup. Namespace `lcp_ruby.menu.aria.<slug>` keeps it separate from labels so the same literal used in both roles does not collide.



431
432
433
434
435
436
437
438
439
440
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 431

def resolved_aria_label(_loader = nil)
  if @aria_label_key.present?
    fallback = @aria_label.is_a?(String) && @aria_label.present? ? @aria_label : @aria_label_key.split(".").last.humanize
    return I18n.t(@aria_label_key, default: fallback)
  end

  return nil unless @aria_label.is_a?(String) && @aria_label.present?

  I18n.t("lcp_ruby.menu.aria.#{@aria_label.parameterize(separator: '_')}", default: nil) || @aria_label
end

#resolved_icon(loader) ⇒ Object

Resolve icon from view group’s primary presenter when not explicitly set



443
444
445
446
447
448
449
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 443

def resolved_icon(loader)
  return @icon if @icon.present?
  return nil unless view_group?

  presenter = primary_presenter(loader)
  presenter&.icon
end

#resolved_label(loader) ⇒ Object

Resolve label with i18n support. Priority: explicit ‘label: false` short-circuit > label_key > explicit label > view_group’s primary presenter resolved_label > ‘presenter:` referenced presenter resolved_label. Returns nil when the item should render no visible label —icon-only items, separator items, or items whose underlying presenter exposes no resolvable label.



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 397

def resolved_label(loader)
  return nil if @label == false

  if @label_key.present?
    fallback = @label.is_a?(String) && @label.present? ? @label : @label_key.split(".").last.humanize
    return I18n.t(@label_key, default: fallback)
  end

  if @label.is_a?(String) && @label.present?
    result = I18n.t("lcp_ruby.menu.#{@label.parameterize(separator: '_')}", default: nil)
    return result || @label
  end

  # No `view_group_name.humanize` fallback — a view_group whose
  # presenter has no resolvable label is a misconfiguration that
  # the validator surfaces, not something we silently humanize.
  return primary_presenter(loader)&.resolved_label if view_group?

  # `presenter:` items inherit the referenced presenter's resolved
  # label as the fallback — keeps `validate_resolved_content!`
  # happy without forcing the configurator to repeat the label.
  if presenter_slug.present?
    return loader.presenter_definitions[presenter_slug]&.resolved_label
  end

  nil
end

#resolved_slug(loader) ⇒ Object

Resolve slug from view group’s primary page



452
453
454
455
456
457
458
459
460
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 452

def resolved_slug(loader)
  return nil unless view_group?

  vg = loader.view_group_definitions[view_group_name]
  return nil unless vg

  page = loader.page_definitions[vg.primary_page]
  page&.slug
end

#right?Boolean

Returns:

  • (Boolean)


608
609
610
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 608

def right?
  position.to_s == POSITION_RIGHT
end

#separator?Boolean

Returns:

  • (Boolean)


635
636
637
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 635

def separator?
  type == :separator
end

Returns:

  • (Boolean)


639
640
641
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 639

def sidebar_toggle?
  type == :sidebar_toggle
end

#to_kwargsObject

Snapshot of the keyword arguments needed to reconstruct this item via ‘MenuItem.new(**kwargs)`. Powers `#with` and keeps the rebuild path in `LayoutHelper#visible_menu_items` from drifting away from `initialize` whenever a new optional key is added. Public so provider expansion (PR #2b) can reconstruct items via `to_kwargs.merge(provider_returned_keys)`.



688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 688

def to_kwargs
  {
    type: type,
    view_group_name: view_group_name,
    label: label,
    label_key: label_key,
    icon: icon,
    url: url,
    children: children,
    visible_when: visible_when,
    disable_when: disable_when,
    position: position,
    badge: badge,
    aria_label: aria_label,
    aria_label_key: aria_label_key,
    http_method: http_method,
    presenter_slug: presenter_slug,
    alias_name: alias_name,
    action_name: action_name,
    defaults: defaults,
    render_partial: render_partial,
    render_panel_partial: render_panel_partial,
    widget: widget,
    provider_name: provider_name,
    provider_options: provider_options,
    panel_provider_name: panel_provider_name,
    label_source_loc: label_source_loc,
    aria_label_source_loc: aria_label_source_loc,
    responsive_priority: responsive_priority,
    pin: pin,
    hide_below: hide_below,
    show_below: show_below,
    collapse_label_below: collapse_label_below
  }
end

#top?Boolean

Forward-compat predicates for ‘position: top` / `position: left`. Reserved for future sidebar alignments — no consumer paths exist today, so these always return false unless the YAML opts in.

Returns:

  • (Boolean)


615
616
617
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 615

def top?
  position.to_s == POSITION_TOP
end

#validate_resolved_content!(loader) ⇒ Object

Loader-aware boot-time validation. Called by ‘ConfigurationValidator#validate_menu_items` after the loader is fully populated so view_group fallbacks resolve. Raises with a descriptor that points at the offending entry.



647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 647

def validate_resolved_content!(loader)
  return if separator?
  # Platform-owned UI chrome — no destination, no privilege
  # escalation surface, stable aria_label.
  return if sidebar_toggle?
  # widget: items drop the wrapper entirely — the render: partial
  # owns all visible content + accessibility semantics. The
  # validator can't introspect the partial's HTML.
  return if widget?
  # provider: items are computed at render time — the provider
  # supplies label/icon/url in its return hash. Static YAML
  # content is optional (and usually empty); the validator
  # cannot introspect provider returns at boot.
  return if provider_name.present?

  content = visible_content(loader)

  # render: items provide visible content via the renderer
  # partial; label/icon are not required. aria_label/aria_label_key
  # is still strongly recommended (the renderer's HTML may not
  # include accessible text); enforce when neither label nor
  # aria_label is set.
  if content.empty? && render_partial.blank?
    raise MetadataError, "Menu item #{descriptor} has no visible content (label or icon)"
  end

  # Items with no visible label (icon-only OR render: producing
  # opaque HTML) need an explicit aria_label/aria_label_key. We
  # cannot introspect a render: partial's accessible text, so we
  # require the aria attribute regardless when no YAML label resolves.
  if !content.include?(:label) && @aria_label.blank? && @aria_label_key.blank?
    raise MetadataError, "Icon-only menu item #{descriptor} requires `aria_label` or `aria_label_key`"
  end
end

#view_group?Boolean

Returns:

  • (Boolean)


623
624
625
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 623

def view_group?
  type == :view_group
end

#widget?Boolean

Predicate sugar — clearer at call sites than ‘widget == true`.

Returns:

  • (Boolean)


746
747
748
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 746

def widget?
  @widget == true
end

#with(**overrides) ⇒ Object

Returns a copy of this item with selected attributes overridden. Used by ‘LayoutHelper#visible_menu_items` to rebuild a `:group` item with filtered children without re-enumerating every attribute by hand.



100
101
102
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 100

def with(**overrides)
  self.class.new(**to_kwargs.merge(overrides))
end