keystone_ui-app 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8baed1af3145b6225cba7256510492d03a2f6ef3945742053c54758ab8330730
4
+ data.tar.gz: 3c088d53b28241d1c74a75712dfaab074e4e695fe3dd848bb93e47cb975ff805
5
+ SHA512:
6
+ metadata.gz: 898e732c1403fe43b9ac93b380383987d1b235cac81f7ed846663b435f6de24052bd92c21503fc47299b014bc64053fa53262595716df9632962d8a61908b253
7
+ data.tar.gz: 458b9cd3f057d02ba88bafc92f6bf9bd131f39916de54d0e2e0e076e7d6fddd9326207376a05d1b7959d3e4b9c1ac09c2a28e876bd32f823e6ed7b045dca4c9d
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tyler Schneider
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # keystone_ui-app
2
+
3
+ The app shell for Rails apps built on keystone_ui. It draws the page frame for desktop browsers, phone browsers and the Hotwire Native app from one call in the layout.
4
+
5
+ ## Install
6
+
7
+ ```ruby
8
+ gem "keystone_ui"
9
+ gem "keystone_ui-app"
10
+ ```
11
+
12
+ The gem requires keystone_ui-styles 0.30.0 or later, which defines the `hotwire-native` and `not-hotwire-native` Tailwind variants, and turbo-rails, whose `hotwire_native_app?` tells the shell it is inside the native app.
13
+
14
+ ## Use
15
+
16
+ Replace the layout with one call to `ui_app_shell`, filling its slots and putting the page inside it:
17
+
18
+ ```erb
19
+ <%= ui_app_shell do |shell| %>
20
+ <% shell.with_head do %><%= render "head" %><% end %>
21
+ <% shell.with_flash do %><%= render "flash" %><% end %>
22
+ <% shell.with_logo do %><%= link_to "Shop", root_path %><% end %>
23
+ <% shell.with_menus do %><%= render "user_menu" %><% end %>
24
+ <% shell.with_footer do %><%= render "footer" %><% end %>
25
+ <%= yield %>
26
+ <% end %>
27
+ ```
28
+
29
+ Register the gem's Stimulus controllers beside keystone_ui's in `app/javascript/controllers/index.js`. The engine pins them in the importmap, along with its own copy of the Hotwire Native Bridge library:
30
+
31
+ ```js
32
+ import { registerControllers as registerKeystoneUiAppControllers } from "keystone_ui_app/index"
33
+ registerKeystoneUiAppControllers(application)
34
+ ```
35
+
36
+ Every slot is optional. The shell writes the `<!DOCTYPE html>`, the `html`, `head` and `body` elements and the viewport meta tag, so the app's head leaves out its own viewport tag.
37
+
38
+ ## What it draws
39
+
40
+ - The `html` element carries keystone_ui's theme attributes and the current locale.
41
+ - On a desktop browser, keystone_ui's navigation bar with the logo and the menus.
42
+ - 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.
43
+ - 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.
44
+ - Inside the native app, the `menu-button` bridge component sends the section's label and tabs to the native menu button, and choosing a tab opens its page in place of the current one. A page in no section sends an empty list.
45
+ - Inside the native app, no navigation bar, no phone header and no footer, the `hotwire-native` class on the `html` element, and a viewport that cannot be pinch-zoomed.
46
+ - In a browser, the footer, unless the page calls `content_for :hide_footer, true`.
47
+
48
+ ## Licence
49
+
50
+ MIT.
@@ -0,0 +1,4 @@
1
+ // @hotwired/hotwire-native-bridge@1.2.2 downloaded from https://ga.jspm.io/npm:@hotwired/hotwire-native-bridge@1.2.2/dist/hotwire-native-bridge.js
2
+
3
+ import{Controller as e}from"@hotwired/stimulus";var t=class{#e;#t;#s;#n;constructor(){this.#e=null;this.#t=0;this.#s=[];this.#n=new Map}start(){this.notifyApplicationAfterStart()}notifyApplicationAfterStart(){document.dispatchEvent(new Event("web-bridge:ready"))}supportsComponent(e){return!!this.#e&&this.#e.supportsComponent(e)}send({component:e,event:t,data:s,callback:n}){if(!this.#e){this.#i({component:e,event:t,data:s,callback:n});return null}if(!this.supportsComponent(e))return null;const i=this.generateMessageId();const r={id:i,component:e,event:t,data:s||{}};this.#e.receive(r);n&&this.#n.set(i,n);return i}receive(e){this.executeCallbackFor(e)}executeCallbackFor(e){const t=this.#n.get(e.id);t&&t(e)}removeCallbackFor(e){this.#n.has(e)&&this.#n.delete(e)}removePendingMessagesFor(e){this.#s=this.#s.filter((t=>t.component!=e))}generateMessageId(){const e=++this.#t;return e.toString()}setAdapter(e){this.#e=e;document.documentElement.dataset.bridgePlatform=this.#e.platform;this.adapterDidUpdateSupportedComponents();this.#r()}adapterDidUpdateSupportedComponents(){this.#e&&(document.documentElement.dataset.bridgeComponents=this.#e.supportedComponents.join(" "))}#i(e){this.#s.push(e)}#r(){this.#s.forEach((e=>this.send(e)));this.#s=[]}};var s=class{constructor(e){this.element=e}get title(){return(this.bridgeAttribute("title")||this.attribute("aria-label")||this.element.textContent||this.element.value).trim()}get enabled(){return!this.disabled}get disabled(){const e=this.bridgeAttribute("disabled");return e==="true"||e===this.platform}enableForComponent(e){e.enabled&&this.removeBridgeAttribute("disabled")}hasClass(e){return this.element.classList.contains(e)}attribute(e){return this.element.getAttribute(e)}bridgeAttribute(e){return this.attribute(`data-bridge-${e}`)}setBridgeAttribute(e,t){this.element.setAttribute(`data-bridge-${e}`,t)}removeBridgeAttribute(e){this.element.removeAttribute(`data-bridge-${e}`)}click(){this.platform=="android"&&this.element.removeAttribute("target");this.element.click()}get platform(){return document.documentElement.dataset.bridgePlatform}};var{userAgent:n}=window.navigator;function i(e){const t=n.match(/bridge-components: \[(.*?)\]/);return!!t&&t[1].split(" ").includes(e)}var r=class extends e{static component="";static get shouldLoad(){return i(this.component)}pendingMessageCallbacks=[];initialize(){this.pendingMessageCallbacks=[]}connect(){this.removeRestoreEventListener();this.addRestoreEventListener()}disconnect(){this.removePendingCallbacks();this.removePendingMessages();this.removeRestoreEventListener()}addRestoreEventListener(){this.restore=this.restore.bind(this);document.addEventListener("native:restore",this.restore)}removeRestoreEventListener(){document.removeEventListener("native:restore",this.restore)}restore(){this.connect()}get component(){return this.constructor.component}get platformOptingOut(){const{bridgePlatform:e}=document.documentElement.dataset;return this.identifier==this.element.getAttribute(`data-controller-optout-${e}`)}get enabled(){return!this.platformOptingOut&&this.bridge.supportsComponent(this.component)}send(e,t={},s){t.metadata={url:window.location.href};const n={component:this.component,event:e,data:t,callback:s};const i=this.bridge.send(n);s&&this.pendingMessageCallbacks.push(i)}removePendingCallbacks(){this.pendingMessageCallbacks.forEach((e=>this.bridge.removeCallbackFor(e)))}removePendingMessages(){this.bridge.removePendingMessagesFor(this.component)}get bridgeElement(){return new s(this.element)}get bridge(){return window.HotwireNative.web}};if(!window.HotwireNative){const e=new t;window.HotwireNative={web:e};a(e);e.start()}function a(e){window.Strada||(window.Strada={web:e});window.webBridge||(window.webBridge=e)}export{r as BridgeComponent,s as BridgeElement};
4
+
@@ -0,0 +1,7 @@
1
+ import MenuButtonController from "keystone_ui_app/menu_button_controller"
2
+
3
+ export function registerControllers(application) {
4
+ application.register("bridge--menu-button", MenuButtonController)
5
+ }
6
+
7
+ export { MenuButtonController }
@@ -0,0 +1,13 @@
1
+ import { BridgeComponent } from "keystone_ui_app/hotwire_native_bridge"
2
+
3
+ export default class extends BridgeComponent {
4
+ static component = "menu-button"
5
+ static values = { menu: Object }
6
+
7
+ connect() {
8
+ super.connect()
9
+ this.send("connect", this.menuValue, (message) => {
10
+ if (message.data.path) window.Turbo.visit(message.data.path, { action: "replace" })
11
+ })
12
+ }
13
+ }
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "keystone_ui/app/current_section"
4
+
5
+ module KeystoneUi
6
+ module App
7
+ class NativeMenuButtonComponent < ViewComponent::Base
8
+ CONTROLLER = "bridge--menu-button"
9
+
10
+ def call
11
+ tag.div(data: { controller: CONTROLLER, "#{CONTROLLER}-menu-value": menu.to_json })
12
+ end
13
+
14
+ def menu
15
+ { title: section&.label, items: tabs.map { |tab| { title: tab.label, path: tab.href } } }
16
+ end
17
+
18
+ def tabs
19
+ section ? section.tabs : []
20
+ end
21
+
22
+ def section
23
+ @section ||= KeystoneUi::App::CurrentSection.new(helpers).group
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ <div class="<%= WRAPPER_CLASSES %>">
2
+ <%= render Keystone::Ui::NavDropdownComponent.new(title: title, area: section.label) do %>
3
+ <% section.tabs.each do |tab| %>
4
+ <%= render Keystone::Ui::NavItemComponent.new(label: tab.label, href: tab.href, active: tab.active) %>
5
+ <% end %>
6
+ <% end %>
7
+ </div>
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "keystone_ui/app/current_section"
4
+
5
+ module KeystoneUi
6
+ module App
7
+ class SectionMenuComponent < ViewComponent::Base
8
+ WRAPPER_CLASSES = "lg:hidden"
9
+
10
+ def render?
11
+ section.present?
12
+ end
13
+
14
+ def title
15
+ helpers.content_for(:mobile_page_title).presence || section.label
16
+ end
17
+
18
+ def section
19
+ @section ||= KeystoneUi::App::CurrentSection.new(helpers).group
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html <%= helpers.keystone_theme_attributes %> class="<%= class_names("hotwire-native": native_app?) %>" lang="<%= I18n.locale %>">
3
+ <head>
4
+ <%= tag.meta name: "viewport", content: viewport %>
5
+ <%= head %>
6
+ </head>
7
+ <body>
8
+ <header>
9
+ <%= flash %>
10
+ <% if phone_header_shown? %>
11
+ <%= helpers.ui_mobile_header(title: page_signalled(:title), back_url: page_signalled(:back_url), subtitle: page_signalled(:subtitle)) %>
12
+ <% elsif !native_app? %>
13
+ <%= render KeystoneUi::App::SectionMenuComponent.new %>
14
+ <% end %>
15
+ </header>
16
+ <% page = tag.main(content) %>
17
+ <% if native_app? %>
18
+ <%= render KeystoneUi::App::NativeMenuButtonComponent.new %>
19
+ <%= page %>
20
+ <% else %>
21
+ <%= helpers.ui_navigation do |navigation| %>
22
+ <% navigation.with_logo { logo.to_s } if logo? %>
23
+ <% navigation.with_menus { menus.to_s } if menus? %>
24
+ <%= page %>
25
+ <% end %>
26
+ <% end %>
27
+ <%= footer if footer_shown? %>
28
+ </body>
29
+ </html>
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module App
5
+ class ShellComponent < ViewComponent::Base
6
+ renders_one :head
7
+ renders_one :flash
8
+ renders_one :logo
9
+ renders_one :menus
10
+ renders_one :footer
11
+
12
+ PAGE_SIGNALS = %i[show_page form_page].freeze
13
+ VIEWPORT = "width=device-width, initial-scale=1"
14
+ NATIVE_APP_VIEWPORT = "#{VIEWPORT}, maximum-scale=1.0, user-scalable=0"
15
+
16
+ def native_app?
17
+ return @native_app if defined?(@native_app)
18
+
19
+ @native_app = helpers.hotwire_native_app?
20
+ end
21
+
22
+ def viewport
23
+ native_app? ? NATIVE_APP_VIEWPORT : VIEWPORT
24
+ end
25
+
26
+ def phone_header_shown?
27
+ page_signal && !native_app?
28
+ end
29
+
30
+ def footer_shown?
31
+ !native_app? && !helpers.content_for?(:hide_footer)
32
+ end
33
+
34
+ def page_signal
35
+ PAGE_SIGNALS.find { |signal| helpers.content_for?(signal) }
36
+ end
37
+
38
+ def page_signalled(name)
39
+ helpers.content_for(:"#{page_signal}_#{name}")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUiAppHelper
4
+ def ui_app_shell(&block)
5
+ render KeystoneUi::App::ShellComponent.new, &block
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ pin "keystone_ui_app/index", to: "keystone_ui_app/index.js"
2
+ pin "keystone_ui_app/menu_button_controller", to: "keystone_ui_app/menu_button_controller.js"
3
+ pin "keystone_ui_app/hotwire_native_bridge", to: "keystone_ui_app/hotwire_native_bridge.js"
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "keystone_ui/visible_navigation"
4
+
5
+ module KeystoneUi
6
+ module App
7
+ class CurrentSection
8
+ def initialize(view)
9
+ @view = view
10
+ end
11
+
12
+ def group
13
+ KeystoneUi::VisibleNavigation.new(
14
+ KeystoneUi.configuration.navigation_groups,
15
+ view: @view,
16
+ saved_order: [],
17
+ current_tab: KeystoneUi.configuration.supplied_current_tab(@view)
18
+ ).groups.find(&:active)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module KeystoneUi
6
+ module App
7
+ class Engine < ::Rails::Engine
8
+ initializer "keystone_ui_app.importmap", before: "importmap" do |app|
9
+ app.config.importmap.paths << root.join("config/importmap.rb") if app.config.respond_to?(:importmap)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module App
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ require "keystone_ui"
2
+ require "view_component"
3
+ require "turbo-rails"
4
+ require "keystone_ui/app/version"
5
+ require "keystone_ui/app/engine"
6
+
7
+ module KeystoneUi
8
+ module App
9
+ end
10
+ end
@@ -0,0 +1 @@
1
+ require "keystone_ui/app"
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: keystone_ui-app
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tyler Schneider
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: keystone_ui
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.66.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.66.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: keystone_ui-styles
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.30.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.30.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: railties
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '7.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '7.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: turbo-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.20
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.0.20
69
+ description: A companion engine for keystone_ui that draws a layout's page frame for
70
+ desktop browsers, phone browsers and the Hotwire Native app from one call.
71
+ email:
72
+ - tylercschneider@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - MIT-LICENSE
78
+ - README.md
79
+ - app/assets/javascripts/keystone_ui_app/hotwire_native_bridge.js
80
+ - app/assets/javascripts/keystone_ui_app/index.js
81
+ - app/assets/javascripts/keystone_ui_app/menu_button_controller.js
82
+ - app/components/keystone_ui/app/native_menu_button_component.rb
83
+ - app/components/keystone_ui/app/section_menu_component.html.erb
84
+ - app/components/keystone_ui/app/section_menu_component.rb
85
+ - app/components/keystone_ui/app/shell_component.html.erb
86
+ - app/components/keystone_ui/app/shell_component.rb
87
+ - app/helpers/keystone_ui_app_helper.rb
88
+ - config/importmap.rb
89
+ - lib/keystone_ui-app.rb
90
+ - lib/keystone_ui/app.rb
91
+ - lib/keystone_ui/app/current_section.rb
92
+ - lib/keystone_ui/app/engine.rb
93
+ - lib/keystone_ui/app/version.rb
94
+ homepage: https://github.com/DYB-Development/keystone_ui-app
95
+ licenses:
96
+ - MIT
97
+ metadata:
98
+ homepage_uri: https://github.com/DYB-Development/keystone_ui-app
99
+ source_code_uri: https://github.com/DYB-Development/keystone_ui-app
100
+ changelog_uri: https://github.com/DYB-Development/keystone_ui-app/blob/main/CHANGELOG.md
101
+ bug_tracker_uri: https://github.com/DYB-Development/keystone_ui-app/issues
102
+ documentation_uri: https://github.com/DYB-Development/keystone_ui-app#readme
103
+ rubygems_mfa_required: 'true'
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 3.2.0
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubygems_version: 3.5.16
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: The page frame for a Rails app built on Keystone UI.
123
+ test_files: []