lazy_hotkeys 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: 83d9da819a7a654425f85d5483735a7c3b39a9efe223ffeb745ff6cad2929237
4
+ data.tar.gz: a70db76d82d03af34cf84d8d98165a2106007ed27dc1d751e9258694f14af591
5
+ SHA512:
6
+ metadata.gz: 90938fc72535c798f603c5b412be32cd5fc49e73125969fa77969f1016253c94bdb6e759918bc11718a9ab4c252d72f8366a65dd8fafd7cdeeb6e3abd2fce3b1
7
+ data.tar.gz: c3a164be79bad857716d4dc19ceecb422f5e141ea1d407192443130d6f6b16f7fa14ccac51cfc9a06cd56c3b6d4e4aa64231d0a3b1b78f2a0b8f5d154339185b
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 QuarkOnRails
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,228 @@
1
+ # Lazy Hotkeys for Lazy People
2
+
3
+ **Stop writing JavaScript. Your keyboard already knows what to do.**
4
+
5
+ Declarative keyboard shortcuts for Rails. Define hotkeys in your views, watch them work. No Stimulus controllers. No event listeners. No `addEventListener` for the 10,000th time.
6
+
7
+ ## Install
8
+
9
+ **1. Add the gem:**
10
+
11
+ ```bash
12
+ bundle add lazy_hotkeys
13
+ ```
14
+
15
+ **2. Run the generator:**
16
+
17
+ ```bash
18
+ gem "lazy_hotkeys", "~> 0.1"
19
+ ```
20
+
21
+ This copies `lazy_hotkeys.js` to `app/javascript/`
22
+
23
+ **3. Load it:**
24
+
25
+ **Importmap:**
26
+ ```bash
27
+ pin "lazy_hotkeys", to: "lazy_hotkeys.js"
28
+ ```
29
+
30
+ Then import it:
31
+ ```javascript
32
+ // app/javascript/application.js
33
+ import "lazy_hotkeys"
34
+ ```
35
+
36
+ **esbuild/rollup/webpack or Vite:**
37
+
38
+ Import with relative path in your entry point:
39
+ ```javascript
40
+ // app/javascript/application.js (or entrypoints/application.js in Vite)
41
+ import "./lazy_hotkeys"
42
+ ```
43
+
44
+ Done. Hotkeys work now. (hopefully)
45
+
46
+ ---
47
+
48
+ ## 1. Hotkeys
49
+
50
+ Press keys. Things happen. Stop pretending you like writing JavaScript.
51
+
52
+ ### Navigate Somewhere
53
+
54
+ ```erb
55
+ <%= hotkey("g i", visit: "/inbox") %>
56
+ <%= hotkey("ctrl+n", visit: new_post_path) %>
57
+ ```
58
+
59
+ Press `g` then `i`. You're in your inbox. Magic? No. Just less suffering.
60
+
61
+ ### Send a Request
62
+
63
+ ```erb
64
+ <%= hotkey("ctrl+s", to: "/save", method: :post) %>
65
+ <%= hotkey("ctrl+d", to: post_path(@post), method: :delete) %>
66
+ ```
67
+
68
+ Ctrl+S sends a POST. Ctrl+D deletes. Your mouse is crying. Good.
69
+
70
+ ### Dispatch an Action
71
+
72
+ ```erb
73
+ <%= hotkey("ctrl+k", action: "open-command-palette") %>
74
+ ```
75
+
76
+ ```javascript
77
+ // Some Stimulus controller, somewhere
78
+ document.addEventListener('lazy-hotkeys:action', (e) => {
79
+ if (e.detail.action === 'open-command-palette') {
80
+ this.open();
81
+ }
82
+ });
83
+ ```
84
+
85
+ For when you absolutely must write JavaScript.
86
+
87
+ ### Chain Multiple Actions
88
+
89
+ ```erb
90
+ <%= hotkey("ctrl+s", chain: [
91
+ { type: "dom", target: "#status", set_text: "Saving..." },
92
+ { type: "request", to: "/save", method: "post" },
93
+ { type: "dom", target: "#status", set_text: "Saved!" }
94
+ ]) %>
95
+ ```
96
+
97
+ One key. Multiple actions. Sequential.
98
+
99
+ ---
100
+
101
+ ## 2. DOM Manipulation
102
+
103
+ ### Show/Hide Things
104
+
105
+ ```erb
106
+ <%= hotkey("?", dom: { target: "#help", toggle_class: "hidden" }) %>
107
+ <%= hotkey("esc", dom: { target: ".modal", add_class: "hidden" }) %>
108
+ <%= hotkey("ctrl+h", dom: { target: "#sidebar", remove_class: "collapsed" }) %>
109
+ ```
110
+
111
+ ### Change Text
112
+
113
+ ```erb
114
+ <%= hotkey("ctrl+shift+c", dom: {
115
+ target: "#counter",
116
+ set_text: "9999"
117
+ }) %>
118
+ ```
119
+
120
+ ### Click Things
121
+
122
+ ```erb
123
+ <%= hotkey("ctrl+enter", dom: { target: "form button", click: true }) %>
124
+ <%= hotkey("/", dom: { target: "#search-input", focus: true }) %>
125
+ ```
126
+
127
+ Your hands never leave the keyboard. Your mouse collects dust. Evolution.
128
+
129
+ ### Set Attributes
130
+
131
+ ```erb
132
+ <%= hotkey("ctrl+d", dom: { target: "#mode", set_attr: { "data-theme": "dark" }}) %>
133
+ ```
134
+
135
+ ### Use a Template (For more complex HTML)
136
+
137
+ ```erb
138
+ <%= hotkey("ctrl+p", dom: { target: "#preview", replace_with: "template" }) do %>
139
+ <div class="preview-panel">
140
+ <h3>Preview</h3>
141
+ <p>Your content here</p>
142
+ </div>
143
+ <% end %>
144
+ ```
145
+
146
+ Press the key. The template replaces the target. No `innerHTML`. No XSS. Just works.
147
+
148
+ ---
149
+
150
+ ## Options
151
+
152
+ | Option | Description |
153
+ |--------|-------------|
154
+ | `visit: "/path"` | Navigate to URL (Turbo or regular) |
155
+ | `to: "/endpoint"` | Send request (GET/POST/PATCH/DELETE) |
156
+ | `method: :post` | HTTP method (with `to:`) |
157
+ | `params: { ... }` | Request params (with `to:`) |
158
+ | `action: "name"` | Dispatch custom event |
159
+ | `dom: { ... }` | Manipulate DOM (see below) |
160
+ | `chain: [...]` | Multiple actions in sequence |
161
+ | `scope: "#form"` | Only works when element exists |
162
+ | `prevent_default: false` | Allow browser default (default: true) |
163
+ | `same_origin: false` | Allow cross-origin (default: true) |
164
+ | `hint: "Save"` | Tooltip/hint text |
165
+
166
+ ### DOM Options
167
+
168
+ | Option | Description | Example |
169
+ |--------|-------------|---------|
170
+ | `target: "#id"` | CSS selector | Required |
171
+ | `click: true` | Click the element | `{ target: "button", click: true }` |
172
+ | `focus: true` | Focus the element | `{ target: "input", focus: true }` |
173
+ | `set_text: "..."` | Change text (safe) | `{ target: "#status", set_text: "Done" }` |
174
+ | `add_class: "..."` | Add CSS class | `{ target: "#box", add_class: "active" }` |
175
+ | `remove_class: "..."` | Remove CSS class | `{ target: "#box", remove_class: "hidden" }` |
176
+ | `toggle_class: "..."` | Toggle CSS class | `{ target: "#menu", toggle_class: "open" }` |
177
+ | `set_attr: { k: v }` | Set attributes (whitelisted) | `{ target: "#mode", set_attr: { "data-theme": "dark" } }` |
178
+ | `remove_attr: "..."` | Remove attribute | `{ target: "#input", remove_attr: "disabled" }` |
179
+ | `replace_with: "template"` | Replace with template content | See template example above |
180
+
181
+ ### Cross-Origin
182
+
183
+ By default, requests to other domains are blocked:
184
+
185
+ ```erb
186
+ <%# This works %>
187
+ <%= hotkey("ctrl+s", to: "/save") %>
188
+
189
+ <%# This is blocked %>
190
+ <%= hotkey("ctrl+x", to: "https://github.com/Plan-Vert/open-letter") %>
191
+
192
+ <%# This works (you asked for it) %>
193
+ <%= hotkey("?", visit: "https://discord.gg/BUtwjJTwxt", same_origin: false) %>
194
+ ```
195
+
196
+ `javascript:`, `data:`, and `file:` URLs are always blocked, even with `same_origin: false`.
197
+
198
+ **Attribute whitelist:**
199
+ Check `lazy_hotkeys.js` and adjust attribute whitelist to your needs, define only what you need.
200
+ ---
201
+
202
+ ## Config
203
+
204
+ Minimal config at the top of `lazy_hotkeys.js`:
205
+
206
+ ```javascript
207
+ const CFG = {
208
+ normalizeCmdToCtrl: true, // Cmd on Mac = Ctrl on Windows
209
+ skipInInputs: true, // Don't fire when typing in inputs
210
+ sequenceTimeoutMs: 800 // How long to wait for sequences like "g i"
211
+ };
212
+ ```
213
+
214
+ Change these values directly in the file. That's it.
215
+
216
+ ---
217
+
218
+ ## Requirements
219
+
220
+ Rails 5.1+ (needs `tag.template` helper)
221
+
222
+ ---
223
+
224
+ ## License
225
+
226
+ MIT
227
+
228
+ ---
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ require "bundler/gem_tasks"
3
+ task default: :build
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ require "rails/generators"
3
+
4
+ module LazyHotkeys
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("../templates", __FILE__)
8
+
9
+ class_option :path, type: :string, default: "app/javascript/lazy_hotkeys.js",
10
+ desc: "Where to place the JS file (use app/assets/javascripts/... for sprockets)"
11
+
12
+ def copy_js_runtime
13
+ template "lazy_hotkeys.js", options[:path]
14
+ end
15
+
16
+ def say_next_steps
17
+ say <<~MSG
18
+ Added lazy_hotkeys JS runtime at #{options[:path]}.
19
+ Ensure it's loaded on the page (Importmap pin or pack include).
20
+ MSG
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,248 @@
1
+ // Scans <template data-hotkey ...> generated by "hotkey" helpers and installs key handlers.
2
+
3
+ (function () {
4
+ if (window.LazyHotkeys) return; // avoid double-boot
5
+
6
+ // Config,
7
+ const CFG = {
8
+ normalizeCmdToCtrl: true,
9
+ skipInInputs: true,
10
+ sequenceTimeoutMs: 800
11
+ };
12
+
13
+ // Utils
14
+ function isTyping(el) {
15
+ if (!CFG.skipInInputs) return false;
16
+ if (!el || !el.closest) return false;
17
+ return !!el.closest("input, textarea, [contenteditable]");
18
+ }
19
+
20
+ // Normalize printable vs named keys
21
+ function keyName(e) {
22
+ const k = e.key.length === 1 ? e.key.toLowerCase() : e.key.toLowerCase();
23
+ return k === " " ? "space" : k;
24
+ }
25
+
26
+ function metaOrCtrl(e) {
27
+ return (CFG.normalizeCmdToCtrl && (e.metaKey || e.ctrlKey)) || e.ctrlKey;
28
+ }
29
+
30
+ function eventToCombo(e) {
31
+ const parts = [];
32
+ if (metaOrCtrl(e)) parts.push("ctrl");
33
+ if (e.shiftKey) parts.push("shift");
34
+ if (e.altKey) parts.push("alt");
35
+ const k = keyName(e);
36
+ if (!["control", "shift", "alt", "meta"].includes(k)) parts.push(k);
37
+ return parts.join("+");
38
+ }
39
+
40
+ function parseJSON(s) {
41
+ try { return s ? JSON.parse(s) : null; } catch (_) { return null; }
42
+ }
43
+
44
+ function toArray(v) { return Array.isArray(v) ? v : [v]; }
45
+
46
+ function isSafeAttribute(name) {
47
+ const safePrefixes = ["data-", "aria-"];
48
+ const safeAttributes = [
49
+ "class", "id", "title", "lang", "dir",
50
+ "role", "tabindex", "alt",
51
+ "name", "value", "type", "for", "placeholder", "autocomplete", "autofocus",
52
+ "checked", "selected", "disabled", "readonly", "required", "multiple",
53
+ "maxlength", "minlength", "max", "min", "step", "pattern", "size",
54
+ "accept", "cols", "rows", "wrap", "spellcheck",
55
+ "hidden", "draggable"
56
+ ];
57
+
58
+ const lowerName = name.toLowerCase();
59
+
60
+ if (safeAttributes.includes(lowerName)) return true;
61
+ if (safePrefixes.some(prefix => lowerName.startsWith(prefix))) return true;
62
+
63
+ return false;
64
+ }
65
+
66
+ function isSafeUrl(url, allowCrossOrigin) {
67
+ try {
68
+ const u = new URL(url, location.href);
69
+ if (u.protocol !== "http:" && u.protocol !== "https:") { return false; }
70
+ if (!allowCrossOrigin && (u.origin !== location.origin)) { return false; }
71
+
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ // Combos like "g i"
79
+ let sequence = [];
80
+ let seqTimer = null;
81
+ function pushSequence(key) {
82
+ if (seqTimer) clearTimeout(seqTimer);
83
+ sequence.push(key);
84
+ seqTimer = setTimeout(() => { sequence = []; }, CFG.sequenceTimeoutMs);
85
+ return sequence.join(" ");
86
+ }
87
+
88
+ // Fetch helpers
89
+ function csrfToken() {
90
+ return document.querySelector("meta[name=csrf-token]")?.content || null;
91
+ }
92
+
93
+ async function request(def, allowCrossOrigin) {
94
+ if (!isSafeUrl(def.to, allowCrossOrigin)) return;
95
+
96
+ const method = (def.method || "GET").toUpperCase();
97
+ const headers = {
98
+ "Accept": "text/vnd.turbo-stream.html, text/html, application/json"
99
+ };
100
+
101
+ let body;
102
+ if (!["GET", "HEAD"].includes(method)) {
103
+ headers["Content-Type"] = "application/json";
104
+ const token = csrfToken();
105
+ if (token) headers["X-CSRF-Token"] = token;
106
+ body = JSON.stringify(def.params || {});
107
+ }
108
+
109
+ const res = await fetch(def.to, { method, headers, body });
110
+ const ct = (res.headers.get("Content-Type") || "").toLowerCase();
111
+ if (res.ok && ct.includes("turbo-stream")) {
112
+ const html = await res.text();
113
+ document.documentElement.insertAdjacentHTML("beforeend", html);
114
+ }
115
+ return res;
116
+ }
117
+
118
+ function visit(url, allowCrossOrigin) {
119
+ if (!isSafeUrl(url, allowCrossOrigin)) return;
120
+ if (window.Turbo && typeof Turbo.visit === "function") Turbo.visit(url);
121
+ else window.location.href = url;
122
+ }
123
+
124
+ // Sends a document-level event other controllers can catch.
125
+
126
+ function dispatchAction(action) {
127
+ const evt = new CustomEvent("lazy-hotkeys:action", { detail: { action } });
128
+ document.dispatchEvent(evt);
129
+ }
130
+
131
+ // DOM operations
132
+ function performDom(def, tplEl) {
133
+ const el = document.querySelector(def.target);
134
+ if (!el) return;
135
+
136
+ if (def.add_class) el.classList.add(...toArray(def.add_class));
137
+ if (def.remove_class) el.classList.remove(...toArray(def.remove_class));
138
+ if (def.toggle_class) el.classList.toggle(def.toggle_class);
139
+
140
+ if (def.set_attr) Object.entries(def.set_attr).forEach(([k, v]) => { if (isSafeAttribute(k)) el.setAttribute(k, String(v)); });
141
+ if (def.remove_attr) toArray(def.remove_attr).forEach((k) => el.removeAttribute(k));
142
+
143
+ if (def.set_text != null) el.textContent = String(def.set_text);
144
+
145
+ if (def.replace_with === "template" && tplEl) {
146
+ el.innerHTML = "";
147
+ el.append(tplEl.content.cloneNode(true));
148
+ }
149
+
150
+ if (def.focus) el.focus();
151
+ if (def.click) el.click();
152
+ }
153
+
154
+ // --- Def parsing ---
155
+ function parseDefs() {
156
+ const els = document.querySelectorAll("[data-hotkey='true']");
157
+ const defs = [];
158
+ els.forEach((el) => {
159
+ const type = el.dataset.hotkeyType; // "visit"|"request"|"action"|"dom"|"chain"
160
+ const opts = parseJSON(el.dataset.hotkeyOpts) || {};
161
+ defs.push({
162
+ el,
163
+ keys: (el.dataset.hotkeyKeys || "").toLowerCase(),
164
+ type,
165
+ prevent: el.dataset.hotkeyPrevent === "true",
166
+ sameOrigin: el.dataset.hotkeySameOrigin !== "false",
167
+ scope: el.dataset.hotkeyScope,
168
+ hint: el.dataset.hotkeyHint,
169
+ opts
170
+ });
171
+ });
172
+ return defs;
173
+ }
174
+
175
+ function inScope(def) {
176
+ if (!def.scope) return true;
177
+ return !!document.querySelector(def.scope);
178
+ }
179
+
180
+ // --- Registry & handlers ---
181
+ const registry = new Map(); // "ctrl+s" or "g i" -> def
182
+
183
+ function register(def) {
184
+ registry.set(def.keys, def);
185
+ }
186
+
187
+ function bootScan() {
188
+ registry.clear();
189
+ parseDefs().forEach(register);
190
+ }
191
+
192
+ async function handle(def) {
193
+ const opts = def.opts || {};
194
+ const allowCrossOrigin = !def.sameOrigin;
195
+ switch (def.type) {
196
+ case "visit":
197
+ visit(opts.to, allowCrossOrigin);
198
+ break;
199
+ case "request":
200
+ await request(opts, allowCrossOrigin);
201
+ break;
202
+ case "action":
203
+ dispatchAction(opts.action);
204
+ break;
205
+ case "dom":
206
+ performDom(opts, def.el);
207
+ break;
208
+ case "chain":
209
+ for (const step of opts) {
210
+ if (step.type === "dom") performDom(step, def.el);
211
+ else if (step.type === "visit") visit(step.to, allowCrossOrigin);
212
+ else if (step.type === "request") await request(step, allowCrossOrigin);
213
+ else if (step.type === "action") dispatchAction(step.action);
214
+ }
215
+ break;
216
+ }
217
+ }
218
+
219
+ function onKeydown(e) {
220
+ if (isTyping(e.target)) return;
221
+
222
+ const comboKey = eventToCombo(e); // e.g., "ctrl+s"
223
+ const seq = pushSequence(comboKey); // e.g., "g i" after two presses
224
+
225
+ const def = registry.get(seq) || registry.get(comboKey);
226
+ if (!def) return;
227
+
228
+ if (!inScope(def)) return;
229
+ if (def.prevent) e.preventDefault();
230
+
231
+ handle(def);
232
+ }
233
+
234
+ function boot() {
235
+ bootScan();
236
+ document.addEventListener("keydown", onKeydown);
237
+
238
+ // Re-scan after Turbo visits
239
+ document.addEventListener("turbo:load", bootScan);
240
+ }
241
+
242
+ window.LazyHotkeys = { boot };
243
+ if (document.readyState === "loading") {
244
+ document.addEventListener("DOMContentLoaded", boot);
245
+ } else {
246
+ boot();
247
+ }
248
+ })();
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LazyHotkeys
4
+ module HotkeysHelper
5
+ def hotkey(keys, **opts, &block)
6
+ type, payload =
7
+ if opts[:visit]
8
+ [:visit, { to: opts[:visit] }]
9
+ elsif opts[:to]
10
+ [:request, { to: opts[:to], method: (opts[:method] || :get).to_s.upcase, params: opts[:params] }]
11
+ elsif opts[:action]
12
+ [:action, { action: opts[:action].to_s }]
13
+ elsif opts[:dom]
14
+ [:dom, opts[:dom]]
15
+ elsif opts[:chain]
16
+ [:chain, opts[:chain]]
17
+ else
18
+ raise ArgumentError, "Specify one of: visit:, to:, action:, dom:, chain:"
19
+ end
20
+
21
+ data = {
22
+ "data-hotkey" => "true",
23
+ "data-hotkey-keys" => keys,
24
+ "data-hotkey-type" => type,
25
+ "data-hotkey-opts" => payload.to_json,
26
+ "data-hotkey-scope" => opts[:scope],
27
+ "data-hotkey-prevent" => (opts.key?(:prevent_default) ? opts[:prevent_default] : true).to_s,
28
+ "data-hotkey-same-origin" => (opts.key?(:same_origin) ? opts[:same_origin] : true).to_s,
29
+ "data-hotkey-hint" => opts[:hint]
30
+ }.compact
31
+
32
+ content = capture(&block) if block_given?
33
+ tag.template(content, **data)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ require "rails/railtie"
3
+
4
+ module LazyHotkeys
5
+ class Railtie < ::Rails::Railtie
6
+ initializer "lazy_hotkeys.view_helper" do
7
+ ActiveSupport.on_load(:action_view) do
8
+ require_relative "hotkeys_helper"
9
+ include LazyHotkeys::HotkeysHelper
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LazyHotkeys
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ require_relative "lazy_hotkeys/version"
3
+ require_relative "lazy_hotkeys/railtie" if defined?(Rails)
4
+
5
+ module LazyHotkeys
6
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lazy_hotkeys
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - QuarkXZ
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: railties
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '5.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '5.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: actionview
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '5.1'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9.0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '5.1'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9.0'
52
+ - !ruby/object:Gem::Dependency
53
+ name: activesupport
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '5.1'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '9.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '5.1'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '9.0'
72
+ - !ruby/object:Gem::Dependency
73
+ name: bundler
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '2.5'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '2.5'
86
+ - !ruby/object:Gem::Dependency
87
+ name: rake
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '13.0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '13.0'
100
+ description: Define hotkey events in views, trigger visit/request/client actions,
101
+ and simple DOM mutations without boilerplate JS.
102
+ email:
103
+ - quarkXZ@proton.me
104
+ executables: []
105
+ extensions: []
106
+ extra_rdoc_files: []
107
+ files:
108
+ - LICENSE
109
+ - README.md
110
+ - Rakefile
111
+ - lib/generators/lazy_hotkeys/install/install_generator.rb
112
+ - lib/generators/lazy_hotkeys/install/templates/lazy_hotkeys.js
113
+ - lib/lazy_hotkeys.rb
114
+ - lib/lazy_hotkeys/hotkeys_helper.rb
115
+ - lib/lazy_hotkeys/railtie.rb
116
+ - lib/lazy_hotkeys/version.rb
117
+ homepage: https://github.com/you/lazy_hotkeys
118
+ licenses:
119
+ - MIT
120
+ metadata:
121
+ source_code_uri: https://github.com/you/lazy_hotkeys
122
+ post_install_message: |2+
123
+
124
+ ⚡ LazyHotkeys installed!
125
+
126
+ Run: rails generate lazy_hotkeys:install
127
+
128
+ Then add to your importmap or JS bundler:
129
+ # Importmap
130
+ bin/importmap pin lazy_hotkeys
131
+
132
+ # Or require in your JavaScript entry point like application.js
133
+ import "lazy_hotkeys"
134
+
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubygems_version: 3.6.9
150
+ specification_version: 4
151
+ summary: Declarative keyboard shortcuts for Rails (Turbo/Stimulus-friendly).
152
+ test_files: []