keystone_ui 0.37.0 β†’ 0.38.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: ba03a37368cf5e1f7e0b18721a1eca9a04aae5dea7a5b3d8887e3db9956f9e8f
4
+ data.tar.gz: 6c0eb35d049ec308a68984f32cd787d8fa1c183057a975322844187f294c31fc
5
5
  SHA512:
6
- metadata.gz: 1869494c71af590cd1fa49fcd1062b024df02950f571e5dcbd7deaaa508574ad0f0aa2bfc8e23b0dc10d35ca440a50bc12e5d18063c62888de9eb29c42c09b45
7
- data.tar.gz: 45563016cb89280c2e97cca008236c9fc50ba4681486a53f0d82fc4066e7db68368ad412c43b512cb2ca4ccd4f359dd29339890e24854eae2ead04ed9db7f6db
6
+ metadata.gz: 36f95ca64410c3d39cae7355a0f1f7673a66e963ecf78a67d82a5559c74eef5ea02de3dc4668aef26fdab81b7b330d8d30dbb1a0aea658f13a188fdfdf296e27
7
+ data.tar.gz: 41c5aff5beccf97a8fac694a8cb6e4250e9b6c4456efb78dc1335df8c5a3be086bacae0b5a28292520666bfae44f7df5b8d6cc9c2fbe36f88f619c0a61e33cc1
@@ -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, tabs| %>
10
+ <div class="<%= SIDEBAR_GROUP_CLASSES %>">
11
+ <p class="<%= sidebar_group_label_classes(tabs) %>"><%= group.label %></p>
12
+ <% tabs.each do |tab| %>
13
+ <a class="<%= sidebar_tab_classes(tab) %>" href="<%= tab.href_for(helpers) %>"><%= 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, tabs| %>
35
+ <%= render Keystone::Ui::NavDropdownComponent.new(title: group.label, area: group.label, active: holds_current?(tabs)) do %>
36
+ <% tabs.each do |tab| %>
37
+ <%= render Keystone::Ui::NavItemComponent.new(label: tab.label, href: tab.href_for(helpers), active: current?(tab)) %>
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,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "keystone_ui/navigation_order"
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
+ permitted = KeystoneUi.configuration.navigation_groups
26
+ .map { |group| [ group, group.tabs_permitted_for(helpers) ] }
27
+ .reject { |_group, tabs| tabs.empty? }
28
+ @groups = KeystoneUi::NavigationOrder.new(saved_navigation["order"] || []).arrange(permitted)
29
+ @current_tab = KeystoneUi.configuration.supplied_current_tab(helpers)
30
+ end
31
+
32
+ def current?(tab)
33
+ tab.key == @current_tab
34
+ end
35
+
36
+ def holds_current?(tabs)
37
+ tabs.any? { |tab| current?(tab) }
38
+ end
39
+
40
+ def sidebar_group_label_classes(tabs)
41
+ [ SIDEBAR_GROUP_LABEL_CLASSES, (ACTIVE_CLASS if holds_current?(tabs)) ].compact.join(" ")
42
+ end
43
+
44
+ def sidebar_tab_classes(tab)
45
+ [ SIDEBAR_TAB_CLASSES, (ACTIVE_CLASS if current?(tab)) ].compact.join(" ")
46
+ end
47
+
48
+ def anything_to_show?
49
+ groups.any? || logo? || menus?
50
+ end
51
+
52
+ def sidebar?
53
+ SIDEBAR_PLACEMENTS.include?(placement)
54
+ end
55
+
56
+ def sidebar_first?
57
+ placement == "left"
58
+ end
59
+
60
+ private
61
+
62
+ def placement
63
+ saved_navigation["placement"]
64
+ end
65
+
66
+ def saved_navigation
67
+ value = KeystoneUi.configuration.supplied_preference(helpers, :navigation)&.dig(:value)
68
+ value.is_a?(Hash) ? value : {}
69
+ end
70
+ end
71
+ end
72
+ 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
@@ -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.38.0"
5
5
  end
@@ -112,6 +112,28 @@ outer element. See Conventions before using it.
112
112
  `mobile_center`, `mobile_right`. Desktop slots are hidden below `lg:` and the
113
113
  mobile slots above it. `desktop_right` renders only when `desktop_links` is
114
114
  also filled.
115
+ - `ui_navigation` β€” no keywords, takes a block holding the page content. Placed
116
+ in the layout around `yield`, it draws the tabs the app declares with
117
+ `config.navigation_group` as a top bar of menus from `lg:` up, then the
118
+ content. It leaves out tabs whose permission check fails and groups with no
119
+ tab left, and below `lg:` it draws only the content. Inside the block, a
120
+ `navigation.with_logo do ... end` block puts the app's logo at the left end
121
+ of the top bar, and a `navigation.with_menus do ... end` block puts menus
122
+ such as the account menu and the user menu at its right end, so the layout
123
+ draws no second bar for them. When the preference supplied for
124
+ `:navigation` holds `{ "placement" => "left" }` or `"right"`, it draws a
125
+ sidebar on that side of the content from `lg:` up instead, with each group's
126
+ label above its tabs, the logo at its top and the menus at its bottom, and
127
+ below `lg:` the content takes the full width. Any other placement draws the
128
+ top bar. When that value holds an `"order"` list of
129
+ `{ "group" => label, "tabs" => [tab key strings] }` entries, both placements
130
+ draw the named groups and tabs first in that order, then the rest in
131
+ declared order, ignoring names no longer declared. When the app sets `config.current_tab_supplier`, the tab whose key
132
+ it returns for the page and that tab's group are shown as active in either
133
+ placement, and nothing is marked when it returns `nil`, a key no declared
134
+ tab has, or is not set. Check whether the app's Keystone UI
135
+ initializer declares navigation groups before adding desktop tabs by hand;
136
+ declaring them is `keystone_ui-install`'s job.
115
137
  - `ui_nav_item(label:, href:, active: false)` β€” one desktop navigation link.
116
138
  - `ui_nav_dropdown(title:, area:, active: false)` β€” takes a block. A navbar
117
139
  dropdown; the block holds the menu links.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: keystone_ui-install
3
- description: Use to hook Keystone UI into a project β€” adding the gem, running the install generator to wire Tailwind, the Stimulus controllers and the layout's theme attributes, and configuring the palette, the theme mode supplier, registered looks, the breadcrumb trail supplier, the saved table layout supplier and extra Tailwind imports and sources.
3
+ description: Use to hook Keystone UI into a project β€” adding the gem, running the install generator to wire Tailwind, the Stimulus controllers and the layout's theme attributes, and configuring the palette, the theme mode supplier, registered looks, the breadcrumb trail supplier, the saved table layout supplier, the navigation groups and current tab supplier, and extra Tailwind imports and sources.
4
4
  tools: Bash, Read, Edit
5
5
  scope: UI β€” pages, forms, tables, navigation, dashboards
6
6
  ---
@@ -28,7 +28,12 @@ built on ViewComponent; hook it in before building any screen with those helpers
28
28
  show pages that pass none, and their Back link when they pass none either),
29
29
  `preference_supplier` (a callable that supplies the saved hidden columns and
30
30
  column order of a data table given a `key:`, and the address its Columns menu
31
- saves to),
31
+ saves to, and the navigation's placement under the key `:navigation`),
32
+ `navigation_group` (a labelled group of desktop tabs that `ui_navigation`
33
+ draws, each tab declared with `tab key, label:, href:, permitted:`),
34
+ `current_tab_supplier` (a callable that supplies the key of the declared tab
35
+ the current page belongs to, so `ui_navigation` marks it and its group
36
+ active),
32
37
  and the `tailwind_imports` and `tailwind_sources` lists (extra CSS files and
33
38
  scan paths added to the Tailwind build).
34
39
  - `keystone_theme_attributes` β€” a layout helper placed inside the `<html>` tag.
@@ -183,6 +188,10 @@ built on ViewComponent; hook it in before building any screen with those helpers
183
188
  config.look_supplier = ->(view) { view.current_user&.look }
184
189
  config.trail_supplier = ->(view) { view.breadcrumb_trail }
185
190
  config.preference_supplier = ->(view, key) { TablePreferences.for(view.current_user, key) }
191
+ config.navigation_group "Sales" do |group|
192
+ group.tab :quotes, label: "Quotes", href: ->(view) { view.quotes_path }, permitted: ->(view) { view.policy(Quote).index? }
193
+ end
194
+ config.current_tab_supplier = ->(view) { view.controller.try(:navigation_tab) }
186
195
  config.tailwind_imports << "/absolute/path/to/extra.css"
187
196
  config.tailwind_sources << "/absolute/path/to/components/**/*.{erb,rb}"
188
197
  end
@@ -287,9 +296,40 @@ built on ViewComponent; hook it in before building any screen with those helpers
287
296
  page unreloaded. The table then shows "Your column changes were not
288
297
  saved." beside its Columns button, and the menu's boxes and order go back
289
298
  to what the table shows.
299
+ - For the key `:navigation` the supplier returns the person's navigation
300
+ placement as `{ value: { "placement" => "left" } }`, with `"left"`,
301
+ `"right"` or `"top"`. `ui_navigation` draws a sidebar on the left or the
302
+ right of the page content on desktop screens for the first two, and the
303
+ top bar for `"top"`, `nil`, a value with no `"placement"` string key, or
304
+ any other placement.
305
+ - The same `:navigation` value may hold the person's order of groups and
306
+ tabs under `"order"`, as
307
+ `[ { "group" => "Admin" }, { "group" => "Sales", "tabs" => [ "orders", "quotes" ] } ]`.
308
+ A group is named by its declared label and a tab by its key as a string.
309
+ `ui_navigation` draws the named groups and tabs first in that order, then
310
+ the rest in declared order, and ignores names no longer declared.
290
311
  - A companion preferences gem may set this supplier for the app. Ask the
291
312
  developer whether the app uses one, or which code stores each user's table
292
313
  layouts, before writing the callable.
314
+ - `navigation_group "Label" do |group| ... end` β€” declares one menu of
315
+ desktop tabs, in the order the groups are declared. Inside the block,
316
+ `group.tab key, label:, href:, permitted:` adds a tab in order. `href:` is
317
+ an address or a callable that receives the view and returns one.
318
+ `permitted:` is a callable that receives the view and returns whether the
319
+ current person may see the tab. The layout renders them with
320
+ `ui_navigation` around `yield`, setting the app's logo with
321
+ `navigation.with_logo` and its account and user menus with
322
+ `navigation.with_menus` inside the block, so the layout draws no other
323
+ top bar. Ask the developer which tabs and groups the app has, and which
324
+ permission check guards each, before declaring them.
325
+ - `current_tab_supplier` β€” a callable that receives the view and returns the
326
+ key of the declared tab the current page belongs to, or `nil`.
327
+ `ui_navigation` shows that tab and its group as active in the top bar and
328
+ gives that tab and its group's label the `active` class in the sidebar.
329
+ `nil`, a key no declared tab has, or no supplier marks nothing. The
330
+ callable must answer for every page the layout draws, so a page with no
331
+ tab returns `nil` rather than raising. Ask the developer how a page names
332
+ its tab, such as a method on each controller, before writing the callable.
293
333
  - `tailwind_imports` and `tailwind_sources` β€” lists to append to, never
294
334
  assign. Each import becomes an `@import` line and each source becomes an
295
335
  `@source` line in `keystone_source.css` on the next boot. They are for
@@ -39,6 +39,7 @@ develop:
39
39
  - ui_action_menu
40
40
  - ui_action_menu_item
41
41
  - ui_navbar
42
+ - ui_navigation
42
43
  - ui_nav_item
43
44
  - ui_nav_dropdown
44
45
  - ui_bottom_nav
@@ -71,6 +72,8 @@ sources:
71
72
  - app/components/keystone/ui/figure_component.rb
72
73
  - lib/generators/keystone/install_generator.rb
73
74
  - lib/keystone_ui/configuration.rb
75
+ - lib/keystone_ui/navigation_group.rb
76
+ - lib/keystone_ui/navigation_order.rb
74
77
  - lib/keystone_ui/missing_back_link.rb
75
78
  - lib/keystone_ui/incomplete_trail.rb
76
79
  - lib/keystone_ui/theme_choice.rb
@@ -131,6 +134,8 @@ sources:
131
134
  - app/components/keystone/ui/nav_dropdown_component.rb
132
135
  - app/components/keystone/ui/nav_item_component.rb
133
136
  - app/components/keystone/ui/navbar_component.rb
137
+ - app/components/keystone/ui/navigation_component.rb
138
+ - app/components/keystone/ui/navigation_component.html.erb
134
139
  - app/components/keystone/ui/option_card_component.rb
135
140
  - app/components/keystone/ui/page_component.rb
136
141
  - app/components/keystone/ui/page_component.html.erb
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keystone_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.37.0
4
+ version: 0.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.12.0
18
+ version: 0.14.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 0.12.0
25
+ version: 0.14.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: view_component
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -161,6 +161,8 @@ files:
161
161
  - app/components/keystone/ui/nav_item_component.rb
162
162
  - app/components/keystone/ui/navbar_component.html.erb
163
163
  - app/components/keystone/ui/navbar_component.rb
164
+ - app/components/keystone/ui/navigation_component.html.erb
165
+ - app/components/keystone/ui/navigation_component.rb
164
166
  - app/components/keystone/ui/option_card_component.html.erb
165
167
  - app/components/keystone/ui/option_card_component.rb
166
168
  - app/components/keystone/ui/page_component.html.erb
@@ -206,6 +208,8 @@ files:
206
208
  - lib/keystone_ui/look_check.rb
207
209
  - lib/keystone_ui/look_choice.rb
208
210
  - lib/keystone_ui/missing_back_link.rb
211
+ - lib/keystone_ui/navigation_group.rb
212
+ - lib/keystone_ui/navigation_order.rb
209
213
  - lib/keystone_ui/safelist.rb
210
214
  - lib/keystone_ui/source_css.rb
211
215
  - lib/keystone_ui/theme_choice.rb