keystone_ui 0.37.0 → 0.39.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1534a3f721376c82b75b22a653e8bcee3a0509bcd3b09cd137c93c78e15110f
4
- data.tar.gz: 0df6e2e29387563688ba68bc53d8c29dc69aa322aa86c84b6416c0e3af36f703
3
+ metadata.gz: b11fa4104f5668547e8efeff23c90f69f4c3d1b7374103c33d7dc2c208363acb
4
+ data.tar.gz: 719716203afcd39869e402674d72a6bf8f1e5f4e7fce163a76c3429ae2bc77e2
5
5
  SHA512:
6
- metadata.gz: 1869494c71af590cd1fa49fcd1062b024df02950f571e5dcbd7deaaa508574ad0f0aa2bfc8e23b0dc10d35ca440a50bc12e5d18063c62888de9eb29c42c09b45
7
- data.tar.gz: 45563016cb89280c2e97cca008236c9fc50ba4681486a53f0d82fc4066e7db68368ad412c43b512cb2ca4ccd4f359dd29339890e24854eae2ead04ed9db7f6db
6
+ metadata.gz: b4849b26d1df9c8a45c7721fa966569e6d0f556780b5139a479a73e08c0602ce1db0eab5d4f8bfedcf5a30301cc4d1bba3cffdb0bf82e91a65a3d87c9fdb575c
7
+ data.tar.gz: 8fdb709bfbf187d4792a76c5057ebec69651fb5d4ba2e5b7a6b527b5e0ae68f437689896a492ec390f46ddc7d330fd3e00e2946588f41932d85bf82e790104e3
@@ -0,0 +1,48 @@
1
+ <% if !anything_to_show? %>
2
+ <%= content %>
3
+ <% elsif sidebar? %>
4
+ <% sidebar = capture do %>
5
+ <nav class="<%= SIDEBAR_CLASSES %>">
6
+ <% if logo? %>
7
+ <div><%= logo %></div>
8
+ <% end %>
9
+ <% groups.each do |group| %>
10
+ <div class="<%= SIDEBAR_GROUP_CLASSES %>">
11
+ <p class="<%= sidebar_group_label_classes(group) %>"><%= group.label %></p>
12
+ <% group.tabs.each do |tab| %>
13
+ <a class="<%= sidebar_tab_classes(tab) %>" href="<%= tab.href %>"><%= tab.label %></a>
14
+ <% end %>
15
+ </div>
16
+ <% end %>
17
+ <% if menus? %>
18
+ <div class="<%= SIDEBAR_MENUS_CLASSES %>"><%= menus %></div>
19
+ <% end %>
20
+ </nav>
21
+ <% end %>
22
+ <div class="<%= SIDEBAR_ROW_CLASSES %>">
23
+ <%= sidebar if sidebar_first? %>
24
+ <div class="<%= SIDEBAR_CONTENT_CLASSES %>"><%= content %></div>
25
+ <%= sidebar unless sidebar_first? %>
26
+ </div>
27
+ <% else %>
28
+ <div class="<%= TOP_BAR_CLASSES %>">
29
+ <%= render Keystone::Ui::NavbarComponent.new(sticky: false) do |navbar| %>
30
+ <% if logo? %>
31
+ <% navbar.with_logo do %><%= logo %><% end %>
32
+ <% end %>
33
+ <% navbar.with_desktop_links do %>
34
+ <% groups.each do |group| %>
35
+ <%= render Keystone::Ui::NavDropdownComponent.new(title: group.label, area: group.label, active: group.active) do %>
36
+ <% group.tabs.each do |tab| %>
37
+ <%= render Keystone::Ui::NavItemComponent.new(label: tab.label, href: tab.href, active: tab.active) %>
38
+ <% end %>
39
+ <% end %>
40
+ <% end %>
41
+ <% end %>
42
+ <% if menus? %>
43
+ <% navbar.with_desktop_right do %><%= menus %><% end %>
44
+ <% end %>
45
+ <% end %>
46
+ </div>
47
+ <%= content %>
48
+ <% end %>
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "keystone_ui/visible_navigation"
4
+
5
+ module Keystone
6
+ module Ui
7
+ class NavigationComponent < ViewComponent::Base
8
+ TOP_BAR_CLASSES = "hidden lg:block sticky top-0 z-40"
9
+ SIDEBAR_ROW_CLASSES = "lg:flex"
10
+ SIDEBAR_CLASSES = "ks-sidebar hidden lg:flex lg:sticky lg:top-0 lg:h-screen shrink-0 overflow-y-auto"
11
+ SIDEBAR_GROUP_CLASSES = "flex flex-col"
12
+ SIDEBAR_GROUP_LABEL_CLASSES = "ks-sidebar-group-label"
13
+ SIDEBAR_TAB_CLASSES = "ks-sidebar-tab"
14
+ ACTIVE_CLASS = "active"
15
+ SIDEBAR_MENUS_CLASSES = "mt-auto"
16
+ SIDEBAR_CONTENT_CLASSES = "min-w-0 flex-1"
17
+ SIDEBAR_PLACEMENTS = %w[left right].freeze
18
+
19
+ renders_one :logo
20
+ renders_one :menus
21
+
22
+ attr_reader :groups
23
+
24
+ def before_render
25
+ @groups = KeystoneUi::VisibleNavigation.new(
26
+ KeystoneUi.configuration.navigation_groups,
27
+ view: helpers,
28
+ saved_order: saved_navigation["order"] || [],
29
+ current_tab: KeystoneUi.configuration.supplied_current_tab(helpers)
30
+ ).groups
31
+ end
32
+
33
+ def sidebar_group_label_classes(group)
34
+ [ SIDEBAR_GROUP_LABEL_CLASSES, (ACTIVE_CLASS if group.active) ].compact.join(" ")
35
+ end
36
+
37
+ def sidebar_tab_classes(tab)
38
+ [ SIDEBAR_TAB_CLASSES, (ACTIVE_CLASS if tab.active) ].compact.join(" ")
39
+ end
40
+
41
+ def anything_to_show?
42
+ groups.any? || logo? || menus?
43
+ end
44
+
45
+ def sidebar?
46
+ SIDEBAR_PLACEMENTS.include?(placement)
47
+ end
48
+
49
+ def sidebar_first?
50
+ placement == "left"
51
+ end
52
+
53
+ private
54
+
55
+ def placement
56
+ saved_navigation["placement"]
57
+ end
58
+
59
+ def saved_navigation
60
+ value = KeystoneUi.configuration.supplied_preference(helpers, :navigation)&.dig(:value)
61
+ value.is_a?(Hash) ? value : {}
62
+ end
63
+ end
64
+ end
65
+ end
@@ -141,6 +141,10 @@ module KeystoneUiHelper
141
141
  render Keystone::Ui::NavbarComponent.new(**args), &block
142
142
  end
143
143
 
144
+ def ui_navigation(&block)
145
+ render Keystone::Ui::NavigationComponent.new, &block
146
+ end
147
+
144
148
  def ui_nav_item(**args)
145
149
  render Keystone::Ui::NavItemComponent.new(**args)
146
150
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "keystone_ui/navigation_group"
4
+
3
5
  module KeystoneUi
4
6
  class Configuration
5
- attr_accessor :accent, :surface, :theme_mode_supplier, :look_supplier, :trail_supplier, :preference_supplier
6
- attr_reader :tailwind_imports, :tailwind_sources, :looks, :default_look
7
+ attr_accessor :accent, :surface, :theme_mode_supplier, :look_supplier, :trail_supplier, :preference_supplier, :current_tab_supplier
8
+ attr_reader :tailwind_imports, :tailwind_sources, :looks, :default_look, :navigation_groups
7
9
 
8
10
  def initialize
9
11
  @accent = :blue
@@ -11,6 +13,7 @@ module KeystoneUi
11
13
  @tailwind_imports = []
12
14
  @tailwind_sources = []
13
15
  @looks = {}
16
+ @navigation_groups = []
14
17
  end
15
18
 
16
19
  def default_look=(name)
@@ -22,6 +25,12 @@ module KeystoneUi
22
25
  @tailwind_imports << path.to_s
23
26
  end
24
27
 
28
+ def navigation_group(label)
29
+ group = NavigationGroup.new(label)
30
+ yield(group)
31
+ @navigation_groups << group
32
+ end
33
+
25
34
  def supplied_look(view)
26
35
  look_supplier&.call(view)&.to_s
27
36
  end
@@ -37,6 +46,10 @@ module KeystoneUi
37
46
  def supplied_preference(view, key)
38
47
  preference_supplier&.call(view, key)
39
48
  end
49
+
50
+ def supplied_current_tab(view)
51
+ current_tab_supplier&.call(view)
52
+ end
40
53
  end
41
54
 
42
55
  def self.configuration
@@ -3,6 +3,7 @@
3
3
  require "keystone_ui/source_css"
4
4
  require "keystone_ui/leftover_stylesheet"
5
5
  require "keystone_ui/look_check"
6
+ require "keystone_ui/navigation_check"
6
7
 
7
8
  module KeystoneUi
8
9
  class Engine < ::Rails::Engine
@@ -32,8 +33,13 @@ module KeystoneUi
32
33
  KeystoneUi::LookCheck.new(looks: config.looks, default: config.default_look).call
33
34
  end
34
35
 
36
+ def self.check_navigation
37
+ KeystoneUi::NavigationCheck.new(groups: KeystoneUi.configuration.navigation_groups).call
38
+ end
39
+
35
40
  config.after_initialize do
36
41
  KeystoneUi::Engine.check_looks
42
+ KeystoneUi::Engine.check_navigation
37
43
 
38
44
  tailwind_dir = Rails.root.join("app/assets/tailwind")
39
45
  css_path = tailwind_dir.join("application.css")
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ class NavigationCheck
5
+ class Error < StandardError; end
6
+
7
+ def initialize(groups:)
8
+ @groups = groups
9
+ end
10
+
11
+ def call
12
+ @groups.each do |group|
13
+ raise Error, "The #{group.label} navigation group has no tabs." if group.tabs.empty?
14
+ end
15
+ tabs.each do |tab|
16
+ raise Error, "The #{tab.key} tab has no label." if blank?(tab.label)
17
+ raise Error, "The #{tab.key} tab has no link." if blank?(tab.href)
18
+ end
19
+ raise Error, "More than one navigation tab has the key #{shared_key}." if shared_key
20
+ nil
21
+ end
22
+
23
+ private
24
+
25
+ def tabs
26
+ @groups.flat_map(&:tabs)
27
+ end
28
+
29
+ def shared_key
30
+ tabs.map(&:key).tally.find { |_key, count| count > 1 }&.first
31
+ end
32
+
33
+ def blank?(value)
34
+ value.nil? || value.to_s.strip.empty?
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ class NavigationGroup
5
+ Tab = Struct.new(:key, :label, :href, :permitted, keyword_init: true) do
6
+ def href_for(view)
7
+ href.respond_to?(:call) ? href.call(view) : href
8
+ end
9
+ end
10
+
11
+ attr_reader :label, :tabs
12
+
13
+ def initialize(label)
14
+ @label = label
15
+ @tabs = []
16
+ end
17
+
18
+ def tabs_permitted_for(view)
19
+ tabs.select { |tab| tab.permitted.call(view) }
20
+ end
21
+
22
+ def tab(key, label:, href:, permitted:)
23
+ @tabs << Tab.new(key: key, label: label, href: href, permitted: permitted)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ class NavigationOrder
5
+ def initialize(saved)
6
+ @saved = saved.is_a?(Array) ? saved.grep(Hash) : []
7
+ end
8
+
9
+ def arrange(groups)
10
+ in_saved_order(groups, group_labels) { |group, _tabs| group.label }
11
+ .map { |group, tabs| [ group, arrange_tabs(group, tabs) ] }
12
+ end
13
+
14
+ private
15
+
16
+ def in_saved_order(items, saved_names)
17
+ items.each_with_index
18
+ .sort_by { |item, declared| [ saved_names.index(yield(item)) || saved_names.size, declared ] }
19
+ .map(&:first)
20
+ end
21
+
22
+ def group_labels
23
+ @saved.map { |entry| entry["group"] }
24
+ end
25
+
26
+ def arrange_tabs(group, tabs)
27
+ in_saved_order(tabs, saved_tab_keys(group)) { |tab| tab.key.to_s }
28
+ end
29
+
30
+ def saved_tab_keys(group)
31
+ tabs = @saved.find { |entry| entry["group"] == group.label }&.fetch("tabs", nil)
32
+ tabs.is_a?(Array) ? tabs : []
33
+ end
34
+ end
35
+ end
@@ -37,6 +37,7 @@ module Keystone
37
37
  Keystone::Ui::NavItemComponent,
38
38
  Keystone::Ui::BottomNavItemComponent,
39
39
  Keystone::Ui::NavbarComponent,
40
+ Keystone::Ui::NavigationComponent,
40
41
  Keystone::Ui::BottomNavComponent,
41
42
  Keystone::Ui::SettingsLinkComponent,
42
43
  Keystone::Ui::FormPageComponent,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeystoneUi
4
- VERSION = "0.37.0"
4
+ VERSION = "0.39.0"
5
5
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "keystone_ui/navigation_order"
4
+
5
+ module KeystoneUi
6
+ class VisibleNavigation
7
+ Group = Struct.new(:label, :tabs, :active, keyword_init: true)
8
+ Tab = Struct.new(:label, :href, :active, keyword_init: true)
9
+
10
+ def initialize(declared_groups, view:, saved_order:, current_tab:)
11
+ @declared_groups = declared_groups
12
+ @view = view
13
+ @saved_order = saved_order
14
+ @current_tab = current_tab
15
+ end
16
+
17
+ def groups
18
+ NavigationOrder.new(@saved_order).arrange(permitted).map { |group, tabs| visible_group(group, tabs) }
19
+ end
20
+
21
+ private
22
+
23
+ def permitted
24
+ @declared_groups
25
+ .map { |group| [ group, group.tabs_permitted_for(@view) ] }
26
+ .reject { |_group, tabs| tabs.empty? }
27
+ end
28
+
29
+ def visible_group(group, tabs)
30
+ visible_tabs = tabs.map { |tab| visible_tab(tab) }
31
+ Group.new(label: group.label, tabs: visible_tabs, active: visible_tabs.any?(&:active))
32
+ end
33
+
34
+ def visible_tab(tab)
35
+ Tab.new(label: tab.label, href: tab.href_for(@view), active: tab.key == @current_tab)
36
+ end
37
+ end
38
+ end