keystone_ui-app 0.3.2 → 0.4.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 +4 -4
- data/README.md +86 -4
- data/app/components/keystone_ui/app/account_menu_component.html.erb +32 -0
- data/app/components/keystone_ui/app/account_menu_component.rb +38 -0
- data/app/components/keystone_ui/app/account_page_component.html.erb +60 -0
- data/app/components/keystone_ui/app/account_page_component.rb +37 -0
- data/app/components/keystone_ui/app/avatar_component.rb +27 -0
- data/app/components/keystone_ui/app/bottom_tabs_component.html.erb +1 -1
- data/app/components/keystone_ui/app/bottom_tabs_component.rb +5 -4
- data/app/components/keystone_ui/app/more_page_component.html.erb +7 -0
- data/app/components/keystone_ui/app/more_page_component.rb +33 -0
- data/app/components/keystone_ui/app/shell_component.html.erb +15 -2
- data/app/components/keystone_ui/app/shell_component.rb +13 -0
- data/app/controllers/keystone_ui/app/path_configurations_controller.rb +6 -4
- data/app/helpers/keystone_ui_app_helper.rb +8 -0
- data/lib/keystone_ui/app/bottom_bar.rb +90 -0
- data/lib/keystone_ui/app/configuration.rb +75 -1
- data/lib/keystone_ui/app/engine.rb +3 -1
- data/lib/keystone_ui/app/shell_check.rb +18 -1
- data/lib/keystone_ui/app/version.rb +1 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a6cdad5c44b2fa3fc519cc758c0b8b99641457035c225a84afe6cc4741bcb3d
|
|
4
|
+
data.tar.gz: cfa779ce21f5cf17a37240b9229ed6205db21938809944d733cf77a535af4c46
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 046a185c7e455766e39168ce20d65984addbb8a1c6b626da6abf37954366e067b023c03132fd9a16b59dc05831c7d41e411058704490b54dcbbeeb4112e8d72f
|
|
7
|
+
data.tar.gz: b9db43449c3593b9a260453d2f1646ede0a2cd90d9d87780dcb07dbae7f992e603547e71ded173b268c1f3282092740f14073bea9009ee5f78f8aacee6d21f7f
|
data/README.md
CHANGED
|
@@ -59,14 +59,95 @@ end
|
|
|
59
59
|
|
|
60
60
|
A page that should show no bottom tabs calls `content_for :hide_bottom_tabs, true`.
|
|
61
61
|
|
|
62
|
+
## App areas
|
|
63
|
+
|
|
64
|
+
An app area is a part of the app that covers one or more keystone_ui navigation groups and gets one bottom tab. Declare the areas and the More page in the same initializer as the bottom tabs:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
KeystoneUi::App.configure do |config|
|
|
68
|
+
config.bottom_tab(label: "Home", href: "/", icon: HOME_ICON_SVG, permitted: ->(view) { view.user_signed_in? }, section: nil, ios_system_image: "house", android_icon: "home")
|
|
69
|
+
config.app_area(name: "Selling", description: "Quotes and orders", groups: [ "Sales" ], on_by_default: true, opens: :quotes, icon: ->(view) { view.render("icons/sales") }, ios_system_image: "cart", android_icon: "shopping_cart")
|
|
70
|
+
config.more_page(href: ->(view) { view.main_app.more_path }, icon: MORE_ICON_SVG, ios_system_image: "ellipsis", android_icon: "more_horiz")
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `name:` is the area's name, and its tab is labelled with it.
|
|
75
|
+
- `description:` says what the area holds.
|
|
76
|
+
- `groups:` lists the labels of the keystone_ui navigation groups the area covers.
|
|
77
|
+
- `on_by_default:` says whether the area is on for an account, and until accounts can save a choice it decides for every account.
|
|
78
|
+
- `opens:` is the key of the navigation page the area's tab opens, and is optional.
|
|
79
|
+
- `icon:`, `ios_system_image:` and `android_icon:` are the tab's icons, given as a bottom tab's are.
|
|
80
|
+
- `more_page` declares the address of the app's More page and its tab's icons, and `href:` and `icon:` take a string or a callable given the view.
|
|
81
|
+
|
|
82
|
+
The bar on a phone shows, in this order:
|
|
83
|
+
|
|
84
|
+
1. The bottom tabs the person may see.
|
|
85
|
+
2. One tab for each area that is on and holds a page the person may open. The tabs follow the order the person saved for their navigation groups, each area in the place of its first group, or the order the areas are declared in when the person saved none.
|
|
86
|
+
3. A More tab, when the app declares a More page and the person may open a page that no tab in the bar opens.
|
|
87
|
+
|
|
88
|
+
The bar holds no more than five tabs, More included, and leaves out the areas that come last first. An area's tab opens the page `opens:` names when the person may open it, and otherwise the first page in the area the person may open, in their saved order. An area's tab is marked current on any page in one of its groups, and the More tab is marked current on the More page.
|
|
89
|
+
|
|
90
|
+
An app that declares no areas and no More page keeps the bar its bottom tabs give it.
|
|
91
|
+
|
|
92
|
+
### The More page
|
|
93
|
+
|
|
94
|
+
The app adds the route its `more_page` declaration points at, and draws the page in its own view with one call:
|
|
95
|
+
|
|
96
|
+
```erb
|
|
97
|
+
<%= ui_more_page %>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
It lists every navigation group the person may see, each under a heading with the group's label and with the pages they may open under it. Groups and pages follow the person's desktop menu, including an order they saved, and a page declared with `navigation_tab` outside any group is a single row with no heading. Each row opens its page.
|
|
101
|
+
|
|
102
|
+
## The account menu
|
|
103
|
+
|
|
104
|
+
The person's account links, accounts and notifications are declared once and drawn the same way on desktop and on a phone:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
KeystoneUi::App.configure do |config|
|
|
108
|
+
config.person_supplier = ->(view) { { name: view.current_user.name, email: view.current_user.email, avatar_url: view.current_user.avatar_url } }
|
|
109
|
+
config.accounts_supplier = ->(view) { view.current_user.accounts.map { |account| { name: account.name, href: view.main_app.switch_account_path(account), current: account == view.current_account } } }
|
|
110
|
+
config.notifications(href: ->(view) { view.main_app.notifications_path }, unread_count: ->(view) { view.current_user.notifications.unread.count })
|
|
111
|
+
config.account_page(href: ->(view) { view.main_app.account_path })
|
|
112
|
+
config.account_link(label: "Settings", href: ->(view) { view.main_app.edit_user_registration_path }, permitted: ->(view) { view.user_signed_in? })
|
|
113
|
+
config.account_link(label: "Sign out", href: ->(view) { view.main_app.destroy_user_session_path }, permitted: ->(view) { view.user_signed_in? }, sign_out: true)
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `account_link` declares one link, and the links are listed in the order they are declared.
|
|
118
|
+
- `label:` is the link's label, `href:` takes a string or a callable given the view, and `permitted:` is a callable given the view that says whether the person sees the link.
|
|
119
|
+
- `sign_out: true` draws the link as a button that sends DELETE to its address.
|
|
120
|
+
- `account_page` declares the address of the app's Account page, and `href:` takes a string or a callable given the view.
|
|
121
|
+
- `person_supplier` returns the person's `name`, `email` and an optional `avatar_url`. The avatar shows the picture when `avatar_url` is given, and otherwise the first letter of the name.
|
|
122
|
+
- `accounts_supplier` returns the person's accounts, each with a `name`, the `href` that switches to it and whether it is `current`. Choosing an account sends PATCH to its `href`.
|
|
123
|
+
- `notifications` declares the notifications address and the unread count, and each takes a value or a callable given the view.
|
|
124
|
+
|
|
125
|
+
Once the app declares an account link, the right of the desktop top bar shows the bell with the unread count, the account switcher named after the current account, and the person's avatar, which opens a dropdown of the account links they may see. These take the place of the menus the layout passes to the frame. An app that declares no account links keeps the menus it passes.
|
|
126
|
+
|
|
127
|
+
When the app declares an Account page, the top of every screen on a phone browser shows the person's avatar as a link to it, beside the phone menus. Inside the native app the avatar is not drawn.
|
|
128
|
+
|
|
129
|
+
### The Account page
|
|
130
|
+
|
|
131
|
+
The app adds the route its `account_page` declaration points at, and draws the page in its own view with one call:
|
|
132
|
+
|
|
133
|
+
```erb
|
|
134
|
+
<%= ui_account_page %>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
It shows the person's avatar, name and email, then every account with the current one marked, then a Notifications row with the unread count, then the account links the person may see in the same order as the desktop dropdown. A long name wraps and a long email is cut short. Inside the native app it is drawn as an ordinary web page.
|
|
138
|
+
|
|
62
139
|
## Boot check
|
|
63
140
|
|
|
64
|
-
The app refuses to boot when its bottom tabs or screen rules are declared wrong, and the error says which one:
|
|
141
|
+
The app refuses to boot when its bottom tabs, areas, More page or screen rules are declared wrong, and the error says which one:
|
|
65
142
|
|
|
66
143
|
- A bottom tab with no label, address, icon, iOS image or Android icon.
|
|
67
144
|
- A bottom tab whose `section:` is not the label of a declared keystone_ui navigation group.
|
|
68
145
|
- Two bottom tabs with the same label.
|
|
69
146
|
- More than five bottom tabs.
|
|
147
|
+
- A More page with more than four bottom tabs.
|
|
148
|
+
- An area with no tab icon, iOS image or Android icon.
|
|
149
|
+
- An area that names a group that is not a declared keystone_ui navigation group, named with the area.
|
|
150
|
+
- Two areas that cover the same group, named with the group.
|
|
70
151
|
- A screen rule with no pattern.
|
|
71
152
|
|
|
72
153
|
A tab with no label and a screen rule with no pattern are named by their position in the order they were declared, counting from 1.
|
|
@@ -124,7 +205,7 @@ That adds two addresses:
|
|
|
124
205
|
|
|
125
206
|
The controller behind them inherits from the host's `ApplicationController`, so a bottom tab's `permitted:` and `href:` callables are given a view with the host's helpers. The host keeps both addresses open to a person who is not signed in, since the native app reads them before sign-in.
|
|
126
207
|
|
|
127
|
-
Each configuration's `settings.tabs` holds
|
|
208
|
+
Each configuration's `settings.tabs` holds the same tabs, in the same order, as the person's bar on a phone browser, with each tab's label as the title and its address as the path. The iOS app gets a list with each tab's `ios_system_image_name`. The Android app gets the same tabs with each tab's `icon`, encoded as one JSON string.
|
|
128
209
|
|
|
129
210
|
Both configurations ship a rule that opens a page whose address ends in `/new` or `/edit` as a modal. The Android configuration also ships a rule before it that opens every page in the `hotwire://fragment/web` fragment with pull to refresh.
|
|
130
211
|
|
|
@@ -142,7 +223,8 @@ end
|
|
|
142
223
|
- The `html` element carries keystone_ui's theme attributes and the current locale.
|
|
143
224
|
- The `html` element carries the `dark` class when the theme mode keystone_ui chooses for the person is dark, and leaves it off for any other mode.
|
|
144
225
|
- The `body` element carries the attributes the layout passes as `body:`.
|
|
145
|
-
- On a desktop browser, keystone_ui's navigation bar with the logo and the menus.
|
|
226
|
+
- On a desktop browser, keystone_ui's navigation bar with the logo and the menus, or the account menu once the app declares account links.
|
|
227
|
+
- On a phone browser, the person's avatar at the top of the header, linking to the Account page, when the app declares one.
|
|
146
228
|
- On a phone browser, the phone menus at the top of the header on every page, including form and show pages, inside an element hidden from the `lg` breakpoint up.
|
|
147
229
|
- On a phone browser, keystone_ui's phone header for a page drawn with `ui_form_page` or `ui_show_page`, with its back link, title and subtitle.
|
|
148
230
|
- On a phone browser, a page that is not a form or show page shows a menu of the current section's tabs, with the tab the page belongs to marked as active. The section is the navigation group holding the tab keystone_ui's current tab supplier names, and the menu leaves out tabs the person is not permitted to see. A page in no section shows no menu.
|
|
@@ -151,7 +233,7 @@ end
|
|
|
151
233
|
- Inside the native app, the `add-button` bridge component sends a page header's add action to the native plus button, and a page with no add action sends none.
|
|
152
234
|
- Inside the native app, a `style` element in the head that hides the web submit button of a form marked with `ui_native_form_data` once the native app has connected its `form` bridge component.
|
|
153
235
|
- Inside the native app, no navigation bar, no phone menus, no phone header and no footer, the `hotwire-native` class on the `html` element, and a viewport that cannot be pinch-zoomed.
|
|
154
|
-
- On a phone browser, keystone_ui's bottom nav at the foot of every page with the tabs the
|
|
236
|
+
- On a phone browser, keystone_ui's bottom nav at the foot of every page with the bottom tabs the person is permitted to see, then the tabs of the app's areas and the More tab as the App areas section says. The tab whose section, or whose area's groups, hold the current tab is marked as active. The bar is hidden from the `lg` breakpoint up, and is not drawn inside the native app, on a page that calls `content_for :hide_bottom_tabs, true`, or when the person may see none of the declared tabs.
|
|
155
237
|
- In a browser, the footer, unless the page calls `content_for :hide_footer, true`.
|
|
156
238
|
|
|
157
239
|
## Licence
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<div class="<%= WRAPPER_CLASSES %>">
|
|
2
|
+
<% if notifications %>
|
|
3
|
+
<%= link_to notifications.href_for(helpers), class: BELL_CLASSES, aria: { label: "Notifications" } do %>
|
|
4
|
+
<%= BELL_ICON.html_safe %>
|
|
5
|
+
<% if unread_count.positive? %>
|
|
6
|
+
<%= render Keystone::Ui::BadgeComponent.new(label: unread_count.to_s, variant: :info) %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% if current_account %>
|
|
11
|
+
<div class="<%= SWITCHER_CLASSES %>">
|
|
12
|
+
<%= render Keystone::Ui::NavDropdownComponent.new(title: current_account[:name], area: "accounts") do %>
|
|
13
|
+
<% accounts.each do |account| %>
|
|
14
|
+
<% if account[:current] %>
|
|
15
|
+
<span class="ks-nav-item active" aria-current="true"><%= account[:name] %></span>
|
|
16
|
+
<% else %>
|
|
17
|
+
<%= button_to account[:name], account[:href], method: :patch, class: BUTTON_CLASSES %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
<%= render Keystone::Ui::NavDropdownComponent.new(title: render(KeystoneUi::App::AvatarComponent.new), area: "account") do %>
|
|
24
|
+
<% links.each do |link| %>
|
|
25
|
+
<% if link.sign_out %>
|
|
26
|
+
<%= button_to link.label, link.href_for(helpers), method: :delete, class: BUTTON_CLASSES %>
|
|
27
|
+
<% else %>
|
|
28
|
+
<%= render Keystone::Ui::NavItemComponent.new(label: link.label, href: link.href_for(helpers)) %>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% end %>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeystoneUi
|
|
4
|
+
module App
|
|
5
|
+
class AccountMenuComponent < ViewComponent::Base
|
|
6
|
+
WRAPPER_CLASSES = "ks-app-account-menu flex items-center gap-2"
|
|
7
|
+
BUTTON_CLASSES = "ks-nav-item w-full text-left cursor-pointer"
|
|
8
|
+
|
|
9
|
+
SWITCHER_CLASSES = "ks-app-account-switcher"
|
|
10
|
+
BELL_CLASSES = "ks-app-notifications-bell ks-nav-item inline-flex items-center gap-1"
|
|
11
|
+
BELL_ICON = <<~SVG.freeze
|
|
12
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="size-5" aria-hidden="true">
|
|
13
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
|
|
14
|
+
</svg>
|
|
15
|
+
SVG
|
|
16
|
+
|
|
17
|
+
def notifications
|
|
18
|
+
KeystoneUi::App.configuration.notifications_link
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def unread_count
|
|
22
|
+
notifications.unread_count_for(helpers).to_i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def accounts
|
|
26
|
+
KeystoneUi::App.configuration.accounts(helpers)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def current_account
|
|
30
|
+
accounts.find { |account| account[:current] }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def links
|
|
34
|
+
KeystoneUi::App.configuration.visible_account_links(helpers)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<section>
|
|
2
|
+
<%= render Keystone::Ui::SectionComponent.new do %>
|
|
3
|
+
<div class="<%= PERSON_CLASSES %>">
|
|
4
|
+
<%= render KeystoneUi::App::AvatarComponent.new %>
|
|
5
|
+
<div class="min-w-0 flex-1">
|
|
6
|
+
<span class="<%= NAME_CLASSES %>"><%= person[:name] %></span>
|
|
7
|
+
<span class="<%= EMAIL_CLASSES %>"><%= person[:email] %></span>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
</section>
|
|
12
|
+
<% if accounts.any? %>
|
|
13
|
+
<section class="ks-app-account-accounts">
|
|
14
|
+
<%= render Keystone::Ui::SectionComponent.new(title: "Accounts") do %>
|
|
15
|
+
<div class="ks-value-list">
|
|
16
|
+
<% accounts.each do |account| %>
|
|
17
|
+
<% if account[:current] %>
|
|
18
|
+
<div class="<%= ROW_CLASSES %>" aria-current="true">
|
|
19
|
+
<span class="<%= LABEL_CLASSES %>"><%= account[:name] %></span>
|
|
20
|
+
<%= render Keystone::Ui::BadgeComponent.new(label: "Current", variant: :success, class: "shrink-0") %>
|
|
21
|
+
</div>
|
|
22
|
+
<% else %>
|
|
23
|
+
<%= button_to account[:href], method: :patch, class: BUTTON_ROW_CLASSES do %>
|
|
24
|
+
<span class="<%= LABEL_CLASSES %>"><%= account[:name] %></span>
|
|
25
|
+
<%= CHEVRON_ICON.html_safe %>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
31
|
+
</section>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% if notifications %>
|
|
34
|
+
<section>
|
|
35
|
+
<%= render Keystone::Ui::SectionComponent.new do %>
|
|
36
|
+
<%= render Keystone::Ui::LinkListComponent.new(rows: [ notifications_row ]) %>
|
|
37
|
+
<% end %>
|
|
38
|
+
</section>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% if links.any? %>
|
|
41
|
+
<section class="ks-app-account-links">
|
|
42
|
+
<%= render Keystone::Ui::SectionComponent.new do %>
|
|
43
|
+
<div class="ks-value-list">
|
|
44
|
+
<% links.each do |link| %>
|
|
45
|
+
<% if link.sign_out %>
|
|
46
|
+
<%= button_to link.href_for(helpers), method: :delete, class: BUTTON_ROW_CLASSES do %>
|
|
47
|
+
<span class="<%= LABEL_CLASSES %>"><%= link.label %></span>
|
|
48
|
+
<%= CHEVRON_ICON.html_safe %>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% else %>
|
|
51
|
+
<%= link_to link.href_for(helpers), class: ROW_CLASSES do %>
|
|
52
|
+
<span class="<%= LABEL_CLASSES %>"><%= link.label %></span>
|
|
53
|
+
<%= CHEVRON_ICON.html_safe %>
|
|
54
|
+
<% end %>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
58
|
+
<% end %>
|
|
59
|
+
</section>
|
|
60
|
+
<% end %>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeystoneUi
|
|
4
|
+
module App
|
|
5
|
+
class AccountPageComponent < ViewComponent::Base
|
|
6
|
+
PERSON_CLASSES = "ks-app-account-person flex min-w-0 items-center gap-3"
|
|
7
|
+
NAME_CLASSES = "ks-paragraph block font-semibold break-words"
|
|
8
|
+
EMAIL_CLASSES = "ks-value-list-note block truncate"
|
|
9
|
+
ROW_CLASSES = "ks-value-list-row items-center no-underline"
|
|
10
|
+
BUTTON_ROW_CLASSES = "#{ROW_CLASSES} w-full text-left cursor-pointer"
|
|
11
|
+
CHEVRON_ICON = Keystone::Ui::SettingsLinkComponent::CHEVRON_ICON
|
|
12
|
+
LABEL_CLASSES = "ks-paragraph ks-settings-link-label block min-w-0 flex-1 break-words"
|
|
13
|
+
|
|
14
|
+
def accounts
|
|
15
|
+
KeystoneUi::App.configuration.accounts(helpers)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def notifications
|
|
19
|
+
KeystoneUi::App.configuration.notifications_link
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def notifications_row
|
|
23
|
+
count = notifications.unread_count_for(helpers).to_i
|
|
24
|
+
row = { label: "Notifications", href: notifications.href_for(helpers) }
|
|
25
|
+
count.positive? ? row.merge(badge: { label: count.to_s, variant: :info }) : row
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def links
|
|
29
|
+
KeystoneUi::App.configuration.visible_account_links(helpers)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def person
|
|
33
|
+
KeystoneUi::App.configuration.person(helpers)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeystoneUi
|
|
4
|
+
module App
|
|
5
|
+
class AvatarComponent < ViewComponent::Base
|
|
6
|
+
CLASSES = "ks-app-avatar inline-flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-gray-200 text-sm font-semibold text-gray-700 dark:bg-gray-700 dark:text-gray-100"
|
|
7
|
+
|
|
8
|
+
def call
|
|
9
|
+
tag.span(picture || initial, class: CLASSES)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def person
|
|
15
|
+
KeystoneUi::App.configuration.person(helpers)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def picture
|
|
19
|
+
tag.img(src: person[:avatar_url], alt: person[:name].to_s, class: "size-full object-cover") if person[:avatar_url].present?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initial
|
|
23
|
+
person[:name].to_s.strip[0].to_s.upcase
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%= render Keystone::Ui::BottomNavComponent.new do %>
|
|
2
2
|
<% tabs.each do |tab| %>
|
|
3
|
-
<%= render Keystone::Ui::BottomNavItemComponent.new(label: tab.label, href: tab.
|
|
3
|
+
<%= render Keystone::Ui::BottomNavItemComponent.new(label: tab.label, href: tab.href, icon: tab.icon, active: active?(tab)) %>
|
|
4
4
|
<% end %>
|
|
5
5
|
<% end %>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "rack/utils"
|
|
5
|
+
require "keystone_ui/app/bottom_bar"
|
|
5
6
|
require "keystone_ui/app/current_section"
|
|
6
7
|
|
|
7
8
|
module KeystoneUi
|
|
@@ -12,21 +13,21 @@ module KeystoneUi
|
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def tabs
|
|
15
|
-
@tabs ||= KeystoneUi::App.
|
|
16
|
+
@tabs ||= KeystoneUi::App::BottomBar.new(helpers).tabs
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def active?(tab)
|
|
19
|
-
return in_section?(tab) if tab.
|
|
20
|
+
return in_section?(tab) if tab.sections.any?
|
|
20
21
|
|
|
21
22
|
tabs.none? { |other| in_section?(other) } && on_page?(tab)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def in_section?(tab)
|
|
25
|
-
section.present? && tab.section
|
|
26
|
+
section.present? && tab.sections.include?(section.label)
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def on_page?(tab)
|
|
29
|
-
address = URI.parse(tab.
|
|
30
|
+
address = URI.parse(tab.href)
|
|
30
31
|
address.path == request.path &&
|
|
31
32
|
Rack::Utils.parse_query(address.query).all? { |name, value| request.query_parameters[name] == value }
|
|
32
33
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "keystone_ui/visible_navigation"
|
|
4
|
+
|
|
5
|
+
module KeystoneUi
|
|
6
|
+
module App
|
|
7
|
+
class MorePageComponent < ViewComponent::Base
|
|
8
|
+
Group = Struct.new(:label, :rows, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def groups
|
|
11
|
+
visible_groups.map do |group|
|
|
12
|
+
Group.new(label: (group.label unless group.standalone), rows: group.tabs.map { |page| { label: page.label, href: page.href } })
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def visible_groups
|
|
19
|
+
KeystoneUi::VisibleNavigation.new(
|
|
20
|
+
KeystoneUi.configuration.navigation_groups,
|
|
21
|
+
view: helpers,
|
|
22
|
+
saved_order: saved_order,
|
|
23
|
+
current_tab: nil
|
|
24
|
+
).groups
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def saved_order
|
|
28
|
+
value = KeystoneUi.configuration.supplied_preference(helpers, :navigation)&.dig(:value)
|
|
29
|
+
value.is_a?(Hash) && value["order"].is_a?(Array) ? value["order"] : []
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
<%= tag.body(**body) do %>
|
|
9
9
|
<header>
|
|
10
10
|
<%= flash %>
|
|
11
|
-
|
|
11
|
+
<% if phone_avatar_shown? %>
|
|
12
|
+
<%= tag.div(class: PHONE_AVATAR_ROW) do %>
|
|
13
|
+
<%= tag.div(phone_menus, class: "min-w-0 flex-1") %>
|
|
14
|
+
<%= link_to account_page.href_for(helpers), class: "shrink-0", aria: { label: "Account" } do %>
|
|
15
|
+
<%= render KeystoneUi::App::AvatarComponent.new %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% elsif phone_menus? && !native_app? %>
|
|
19
|
+
<%= tag.div(phone_menus, class: PHONE_ONLY) %>
|
|
20
|
+
<% end %>
|
|
12
21
|
<% if phone_header_shown? %>
|
|
13
22
|
<%= helpers.ui_mobile_header(title: page_signalled(:title), back_url: page_signalled(:back_url), subtitle: page_signalled(:subtitle)) %>
|
|
14
23
|
<% elsif !native_app? %>
|
|
@@ -24,7 +33,11 @@
|
|
|
24
33
|
<% else %>
|
|
25
34
|
<%= helpers.ui_navigation do |navigation| %>
|
|
26
35
|
<% navigation.with_logo { logo.to_s } if logo? %>
|
|
27
|
-
<%
|
|
36
|
+
<% if account_menu? %>
|
|
37
|
+
<% navigation.with_menus { render KeystoneUi::App::AccountMenuComponent.new } %>
|
|
38
|
+
<% elsif menus? %>
|
|
39
|
+
<% navigation.with_menus { menus.to_s } %>
|
|
40
|
+
<% end %>
|
|
28
41
|
<%= page %>
|
|
29
42
|
<% end %>
|
|
30
43
|
<% end %>
|
|
@@ -13,6 +13,7 @@ module KeystoneUi
|
|
|
13
13
|
renders_one :footer
|
|
14
14
|
|
|
15
15
|
PHONE_ONLY = "lg:hidden"
|
|
16
|
+
PHONE_AVATAR_ROW = "#{PHONE_ONLY} flex items-center gap-2 pr-4"
|
|
16
17
|
PAGE_SIGNALS = %i[show_page form_page].freeze
|
|
17
18
|
VIEWPORT = "width=device-width, initial-scale=1"
|
|
18
19
|
NATIVE_APP_VIEWPORT = "#{VIEWPORT}, maximum-scale=1.0, user-scalable=0"
|
|
@@ -46,6 +47,18 @@ module KeystoneUi
|
|
|
46
47
|
page_signal && !native_app?
|
|
47
48
|
end
|
|
48
49
|
|
|
50
|
+
def account_page
|
|
51
|
+
KeystoneUi::App.configuration.account_page_link
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def phone_avatar_shown?
|
|
55
|
+
account_page && !native_app?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def account_menu?
|
|
59
|
+
KeystoneUi::App.configuration.account_links.any?
|
|
60
|
+
end
|
|
61
|
+
|
|
49
62
|
def footer_shown?
|
|
50
63
|
!native_app? && !helpers.content_for?(:hide_footer)
|
|
51
64
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "keystone_ui/app/bottom_bar"
|
|
4
|
+
|
|
3
5
|
module KeystoneUi
|
|
4
6
|
module App
|
|
5
7
|
class PathConfigurationsController < ::ApplicationController
|
|
@@ -21,11 +23,11 @@ module KeystoneUi
|
|
|
21
23
|
private
|
|
22
24
|
|
|
23
25
|
def ios_tabs
|
|
24
|
-
|
|
26
|
+
bar_tabs.map { |tab| { title: tab.label, path: tab.href, ios_system_image_name: tab.ios_system_image } }
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def android_tabs
|
|
28
|
-
|
|
30
|
+
bar_tabs.map { |tab| { title: tab.label, path: tab.href, icon: tab.android_icon } }
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
def app_settings
|
|
@@ -36,8 +38,8 @@ module KeystoneUi
|
|
|
36
38
|
KeystoneUi::App.configuration.native_rules
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
def
|
|
40
|
-
KeystoneUi::App.
|
|
41
|
+
def bar_tabs
|
|
42
|
+
@bar_tabs ||= KeystoneUi::App::BottomBar.new(view).tabs
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
def view
|
|
@@ -5,6 +5,14 @@ module KeystoneUiAppHelper
|
|
|
5
5
|
render KeystoneUi::App::ShellComponent.new(body: body), &block
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
def ui_more_page
|
|
9
|
+
render KeystoneUi::App::MorePageComponent.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def ui_account_page
|
|
13
|
+
render KeystoneUi::App::AccountPageComponent.new
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
def ui_native_form_data
|
|
9
17
|
{ controller: "bridge--form", action: "turbo:submit-start->bridge--form#submitStart turbo:submit-end->bridge--form#submitEnd" }
|
|
10
18
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "keystone_ui/navigation_order"
|
|
4
|
+
require "keystone_ui/app/shell_check"
|
|
5
|
+
|
|
6
|
+
module KeystoneUi
|
|
7
|
+
module App
|
|
8
|
+
class BottomBar
|
|
9
|
+
Tab = Struct.new(:label, :href, :icon, :ios_system_image, :android_icon, :sections, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
MORE_LABEL = "More"
|
|
12
|
+
MOST_TABS = ShellCheck::MOST_BOTTOM_TABS
|
|
13
|
+
|
|
14
|
+
def initialize(view)
|
|
15
|
+
@view = view
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def tabs
|
|
19
|
+
shown = fixed_tabs + area_tabs
|
|
20
|
+
full = shown.first(MOST_TABS)
|
|
21
|
+
more_tab_needed?(full) ? shown.first(MOST_TABS - 1) + [ more_tab ] : full
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def fixed_tabs
|
|
27
|
+
configuration.bottom_tabs.select { |tab| tab.permitted.call(@view) }.map do |tab|
|
|
28
|
+
Tab.new(label: tab.label, href: tab.href_for(@view), icon: tab.icon_for(@view), ios_system_image: tab.ios_system_image, android_icon: tab.android_icon, sections: [ tab.section ].compact)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def area_tabs
|
|
33
|
+
shown = configuration.app_areas.select { |area| area.on_by_default && area_pages(area).any? }
|
|
34
|
+
in_person_order(shown).map { |area| area_tab(area) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def area_tab(area)
|
|
38
|
+
Tab.new(label: area.name, href: opened_page(area).href_for(@view), icon: area.icon_for(@view), ios_system_image: area.ios_system_image, android_icon: area.android_icon, sections: area.groups)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def more_tab_needed?(shown)
|
|
42
|
+
configuration.more_page_tab && (permitted_groups.flat_map(&:last).map { |page| page.href_for(@view) } - shown.map(&:href)).any?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def more_tab
|
|
46
|
+
more = configuration.more_page_tab
|
|
47
|
+
Tab.new(label: MORE_LABEL, href: more.href_for(@view), icon: more.icon_for(@view), ios_system_image: more.ios_system_image, android_icon: more.android_icon, sections: [])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def opened_page(area)
|
|
51
|
+
pages = area_pages(area)
|
|
52
|
+
pages.find { |page| page.key == area.opens } || pages.first
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def in_person_order(areas)
|
|
56
|
+
return areas if saved_order.empty?
|
|
57
|
+
|
|
58
|
+
labels = arranged_groups.map { |group, _pages| group.label }
|
|
59
|
+
areas.each_with_index
|
|
60
|
+
.sort_by { |area, declared| [ area.groups.filter_map { |label| labels.index(label) }.min, declared ] }
|
|
61
|
+
.map(&:first)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def area_pages(area)
|
|
65
|
+
arranged_groups.select { |group, _pages| area.groups.include?(group.label) }.flat_map(&:last)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def arranged_groups
|
|
69
|
+
@arranged_groups ||= KeystoneUi::NavigationOrder.new(saved_order).arrange(permitted_groups)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def permitted_groups
|
|
73
|
+
@permitted_groups ||= KeystoneUi.configuration.navigation_groups
|
|
74
|
+
.map { |group| [ group, group.tabs_permitted_for(@view) ] }
|
|
75
|
+
.reject { |_group, pages| pages.empty? }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def saved_order
|
|
79
|
+
@saved_order ||= begin
|
|
80
|
+
value = KeystoneUi.configuration.supplied_preference(@view, :navigation)&.dig(:value)
|
|
81
|
+
value.is_a?(Hash) && value["order"].is_a?(Array) ? value["order"] : []
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def configuration
|
|
86
|
+
KeystoneUi::App.configuration
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -13,10 +13,52 @@ module KeystoneUi
|
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
AppArea = Struct.new(:name, :description, :groups, :on_by_default, :opens, :icon, :ios_system_image, :android_icon, keyword_init: true) do
|
|
17
|
+
def icon_for(view)
|
|
18
|
+
icon.respond_to?(:call) ? icon.call(view) : icon
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
MorePage = Struct.new(:href, :icon, :ios_system_image, :android_icon, keyword_init: true) do
|
|
23
|
+
def href_for(view)
|
|
24
|
+
href.respond_to?(:call) ? href.call(view) : href
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def icon_for(view)
|
|
28
|
+
icon.respond_to?(:call) ? icon.call(view) : icon
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
AccountLink = Struct.new(:label, :href, :permitted, :sign_out, keyword_init: true) do
|
|
33
|
+
def href_for(view)
|
|
34
|
+
href.respond_to?(:call) ? href.call(view) : href
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
AccountPage = Struct.new(:href, keyword_init: true) do
|
|
39
|
+
def href_for(view)
|
|
40
|
+
href.respond_to?(:call) ? href.call(view) : href
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Notifications = Struct.new(:href, :unread_count, keyword_init: true) do
|
|
45
|
+
def href_for(view)
|
|
46
|
+
href.respond_to?(:call) ? href.call(view) : href
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unread_count_for(view)
|
|
50
|
+
unread_count.respond_to?(:call) ? unread_count.call(view) : unread_count
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attr_accessor :person_supplier, :accounts_supplier
|
|
55
|
+
|
|
56
|
+
attr_reader :bottom_tabs, :app_areas, :more_page_tab, :account_links, :account_page_link, :notifications_link, :native_rules, :native_settings
|
|
17
57
|
|
|
18
58
|
def initialize
|
|
19
59
|
@bottom_tabs = []
|
|
60
|
+
@app_areas = []
|
|
61
|
+
@account_links = []
|
|
20
62
|
@native_rules = []
|
|
21
63
|
@native_settings = {}
|
|
22
64
|
end
|
|
@@ -25,6 +67,38 @@ module KeystoneUi
|
|
|
25
67
|
@bottom_tabs << BottomTab.new(label: label, href: href, icon: icon, permitted: permitted, section: section, ios_system_image: ios_system_image, android_icon: android_icon)
|
|
26
68
|
end
|
|
27
69
|
|
|
70
|
+
def app_area(name:, description:, groups:, on_by_default:, icon:, ios_system_image:, android_icon:, opens: nil)
|
|
71
|
+
@app_areas << AppArea.new(name: name, description: description, groups: groups, on_by_default: on_by_default, opens: opens, icon: icon, ios_system_image: ios_system_image, android_icon: android_icon)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def more_page(href:, icon:, ios_system_image:, android_icon:)
|
|
75
|
+
@more_page_tab = MorePage.new(href: href, icon: icon, ios_system_image: ios_system_image, android_icon: android_icon)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def account_link(label:, href:, permitted:, sign_out: false)
|
|
79
|
+
@account_links << AccountLink.new(label: label, href: href, permitted: permitted, sign_out: sign_out)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def visible_account_links(view)
|
|
83
|
+
account_links.select { |link| link.permitted.call(view) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def account_page(href:)
|
|
87
|
+
@account_page_link = AccountPage.new(href: href)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def notifications(href:, unread_count:)
|
|
91
|
+
@notifications_link = Notifications.new(href: href, unread_count: unread_count)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def person(view)
|
|
95
|
+
person_supplier ? person_supplier.call(view) : {}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def accounts(view)
|
|
99
|
+
accounts_supplier ? accounts_supplier.call(view) : []
|
|
100
|
+
end
|
|
101
|
+
|
|
28
102
|
def native_rule(patterns:, properties:)
|
|
29
103
|
@native_rules << { patterns: patterns, properties: properties }
|
|
30
104
|
end
|
|
@@ -11,7 +11,9 @@ module KeystoneUi
|
|
|
11
11
|
KeystoneUi::App::ShellCheck.new(
|
|
12
12
|
bottom_tabs: configuration.bottom_tabs,
|
|
13
13
|
native_rules: configuration.native_rules,
|
|
14
|
-
sections: KeystoneUi.configuration.navigation_groups.map(&:label)
|
|
14
|
+
sections: KeystoneUi.configuration.navigation_groups.map(&:label),
|
|
15
|
+
app_areas: configuration.app_areas,
|
|
16
|
+
more_page: configuration.more_page_tab
|
|
15
17
|
).call
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -7,8 +7,10 @@ module KeystoneUi
|
|
|
7
7
|
|
|
8
8
|
MOST_BOTTOM_TABS = 5
|
|
9
9
|
|
|
10
|
-
def initialize(bottom_tabs:, native_rules:, sections:)
|
|
10
|
+
def initialize(bottom_tabs:, native_rules:, sections:, app_areas: [], more_page: nil)
|
|
11
11
|
@bottom_tabs = bottom_tabs
|
|
12
|
+
@more_page = more_page
|
|
13
|
+
@app_areas = app_areas
|
|
12
14
|
@native_rules = native_rules
|
|
13
15
|
@sections = sections
|
|
14
16
|
end
|
|
@@ -24,6 +26,9 @@ module KeystoneUi
|
|
|
24
26
|
end
|
|
25
27
|
raise Error, "More than one bottom tab has the label #{shared_label}." if shared_label
|
|
26
28
|
raise Error, "#{@bottom_tabs.size} bottom tabs are declared, and no more than #{MOST_BOTTOM_TABS} are allowed." if @bottom_tabs.size > MOST_BOTTOM_TABS
|
|
29
|
+
raise Error, "#{@bottom_tabs.size} bottom tabs are declared with a More page, and no more than #{MOST_BOTTOM_TABS - 1} are allowed." if @more_page && @bottom_tabs.size > MOST_BOTTOM_TABS - 1
|
|
30
|
+
@app_areas.each { |area| check_area(area) }
|
|
31
|
+
raise Error, "More than one area covers the #{shared_group} group." if shared_group
|
|
27
32
|
@native_rules.each.with_index(1) do |rule, position|
|
|
28
33
|
raise Error, "Screen rule #{position} has no pattern." if Array(rule[:patterns]).all? { |pattern| blank?(pattern) }
|
|
29
34
|
end
|
|
@@ -32,6 +37,18 @@ module KeystoneUi
|
|
|
32
37
|
|
|
33
38
|
private
|
|
34
39
|
|
|
40
|
+
def check_area(area)
|
|
41
|
+
raise Error, "The #{area.name} area's tab has no icon." if blank?(area.icon)
|
|
42
|
+
raise Error, "The #{area.name} area's tab has no iOS image." if blank?(area.ios_system_image)
|
|
43
|
+
raise Error, "The #{area.name} area's tab has no Android icon." if blank?(area.android_icon)
|
|
44
|
+
undeclared = area.groups.find { |group| !@sections.include?(group) }
|
|
45
|
+
raise Error, "The #{area.name} area names the #{undeclared} group, which is not a declared navigation group." if undeclared
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def shared_group
|
|
49
|
+
@app_areas.flat_map { |area| area.groups.uniq }.tally.find { |_group, count| count > 1 }&.first
|
|
50
|
+
end
|
|
51
|
+
|
|
35
52
|
def shared_label
|
|
36
53
|
@bottom_tabs.map(&:label).tally.find { |_label, count| count > 1 }&.first
|
|
37
54
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keystone_ui-app
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tyler Schneider
|
|
@@ -80,8 +80,15 @@ files:
|
|
|
80
80
|
- app/assets/javascripts/keystone_ui_app/hotwire_native_bridge.js
|
|
81
81
|
- app/assets/javascripts/keystone_ui_app/index.js
|
|
82
82
|
- app/assets/javascripts/keystone_ui_app/menu_button_controller.js
|
|
83
|
+
- app/components/keystone_ui/app/account_menu_component.html.erb
|
|
84
|
+
- app/components/keystone_ui/app/account_menu_component.rb
|
|
85
|
+
- app/components/keystone_ui/app/account_page_component.html.erb
|
|
86
|
+
- app/components/keystone_ui/app/account_page_component.rb
|
|
87
|
+
- app/components/keystone_ui/app/avatar_component.rb
|
|
83
88
|
- app/components/keystone_ui/app/bottom_tabs_component.html.erb
|
|
84
89
|
- app/components/keystone_ui/app/bottom_tabs_component.rb
|
|
90
|
+
- app/components/keystone_ui/app/more_page_component.html.erb
|
|
91
|
+
- app/components/keystone_ui/app/more_page_component.rb
|
|
85
92
|
- app/components/keystone_ui/app/native_add_button_component.rb
|
|
86
93
|
- app/components/keystone_ui/app/native_menu_button_component.rb
|
|
87
94
|
- app/components/keystone_ui/app/page_actions_component.html.erb
|
|
@@ -96,6 +103,7 @@ files:
|
|
|
96
103
|
- config/routes.rb
|
|
97
104
|
- lib/keystone_ui-app.rb
|
|
98
105
|
- lib/keystone_ui/app.rb
|
|
106
|
+
- lib/keystone_ui/app/bottom_bar.rb
|
|
99
107
|
- lib/keystone_ui/app/configuration.rb
|
|
100
108
|
- lib/keystone_ui/app/current_section.rb
|
|
101
109
|
- lib/keystone_ui/app/engine.rb
|