feedback_engine 0.3.0 → 0.4.1

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: 156ebb1c4481df5db12c92701ca07f5199f033aa967f264aa855caf327f4542a
4
- data.tar.gz: f59ef8d27b7539d16f03eaef38abc4d6ce51ee55cdd630a738433ca7f3463ef7
3
+ metadata.gz: 0dbeb9cc3333acd2485439cb1896536b2617595f980da907d79ddf0cdb9ccb6c
4
+ data.tar.gz: a7f6907958921185994ea549d3a6ed987c567a4cc17019e281a8cf821fcbb89f
5
5
  SHA512:
6
- metadata.gz: 827983bd43f6d4b9af39b6d526f2d14755365ce561cfdbb338e240e97a2a54afa2173e0efb645846078d107c7ee234de27356e9814acbdbd891f8d7116e6a30b
7
- data.tar.gz: 3f311aeb99159a9d538ddd87e8d346d7b2bd7d10b4521fcec6e864945801eba2ccf9a5e58bf2ebd65034189379647dabf50d60bc5e421924cb2b012468953481
6
+ metadata.gz: 8f9bc37eeafccb24cac72fc0ce2099a604e66325e9b4e966cebe622688517575c68154b6b9dce8fdc9b1791b3ed9bba1118fccf0c9e11b1bf6b4c229c003cadf
7
+ data.tar.gz: 8ad4196a942eab2ce07a17a3fe9048b27015ca769251df5c777876914e7cbb93aa5e3f0168a13ac38ba920e55fd129d9c23d1064f3368eaadbacee25287da256
data/CHANGELOG.md CHANGED
@@ -1,11 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1 (2026-07-23)
4
+
5
+ - The widget JavaScript moved from `app/assets/` to `lib/` (it was always
6
+ inlined server-side, never served as an asset). Rails auto-registers every
7
+ engine's `app/assets/*` directory with the host's asset pipeline, so
8
+ Propshaft hosts were ingesting the file into their asset namespace under
9
+ the bare logical name `widget.js` — colliding with any other gem or host
10
+ file of the same name — and needlessly digesting a public copy at
11
+ precompile. The gem is now invisible to Sprockets/Propshaft entirely.
12
+ No behavior change.
13
+
14
+ ## 0.4.0 (2026-07-22)
15
+
16
+ - Removed the clipboard-paste / drag-and-drop / file-chips screenshot intake
17
+ added in 0.3.0 — pasted files all arrive as `image.png` and the extra UI
18
+ wasn't worth its complexity. Screenshots are back to a plain file input.
19
+
3
20
  ## 0.3.0 (2026-07-22)
4
21
 
5
- - Screenshots can now be pasted (Cmd/Ctrl+V) or dragged & dropped anywhere on
22
+ - Screenshots can be pasted (Cmd/Ctrl+V) or dragged & dropped anywhere on
6
23
  the feedback form, not just picked via the file dialog. Selected files show
7
24
  as removable chips; non-images are ignored and the configured maximum is
8
- enforced at intake.
25
+ enforced at intake. (Removed again in 0.4.0.)
9
26
  - The dialog traps Tab focus while open and is labelled for screen readers
10
27
  (`aria-labelledby`).
11
28
  - Dashboard search across message, author, and section (case-insensitive,
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # feedback_engine
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/feedback_engine)](https://rubygems.org/gems/feedback_engine)
4
+ [![Downloads](https://img.shields.io/gem/dt/feedback_engine)](https://rubygems.org/gems/feedback_engine)
4
5
  [![CI](https://github.com/yshmarov/feedback-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/yshmarov/feedback-engine/actions/workflows/ci.yml)
5
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](MIT-LICENSE)
6
7
 
@@ -12,6 +13,30 @@ every submission in your own database. A minimal built-in dashboard lets you
12
13
  browse and triage what users send. No third-party service, no data leaves your
13
14
  app.
14
15
 
16
+ <picture>
17
+ <source media="(prefers-color-scheme: dark)" srcset="demo-widget-dark.png">
18
+ <img alt="The feedback widget: type, section, message, and screenshots" src="demo-widget-light.png" width="420">
19
+ </picture>
20
+
21
+ <picture>
22
+ <source media="(prefers-color-scheme: dark)" srcset="demo-dashboard-dark.png">
23
+ <img alt="The built-in triage dashboard: status tabs, filters, search" src="demo-dashboard-light.png" width="820">
24
+ </picture>
25
+
26
+ Both the widget and the dashboard follow the viewer's system appearance —
27
+ these screenshots are the same pages in light and dark mode.
28
+
29
+ ## Why not Canny (or another feedback SaaS)?
30
+
31
+ Hosted feedback tools are great until you notice the trade: your users'
32
+ feedback lives in someone else's database, behind someone else's login, at a
33
+ per-seat price. `feedback_engine` keeps the core loop — collect, triage,
34
+ resolve — inside your app: **your database, your users, your auth, one gem.**
35
+ Attribution plugs into whatever authentication you already have, gating is a
36
+ lambda, and submissions are ordinary ActiveRecord rows you can query, export,
37
+ or wire into Slack. (A public voting board à la Canny is on the
38
+ [roadmap](https://github.com/yshmarov/feedback-engine/issues/1).)
39
+
15
40
  - **Zero UI dependencies.** The widget is plain JavaScript and styles itself.
16
41
  No Tailwind, no Stimulus, no importmap, no build step. The dashboard renders
17
42
  its own styles too.
@@ -20,8 +45,7 @@ app.
20
45
  - **Trigger it your way.** Use the built-in floating button, or hide it and
21
46
  open the form from any element with a `data-feedback-engine-open` attribute.
22
47
  - **Screenshots included.** Users can attach up to 3 images (via Active
23
- Storage) — picked, pasted (Cmd/Ctrl+V), or dragged onto the form. Limits are
24
- configurable and enforced server-side.
48
+ Storage) — limits are configurable and enforced server-side.
25
49
  - **Pluggable gating and attribution.** You decide who can send feedback, who
26
50
  can read it, and how a submission is attributed to a user.
27
51
  - **Localized.** The widget follows your app's `I18n.locale`; translations ship
@@ -177,11 +201,9 @@ end
177
201
  ### Screenshots
178
202
 
179
203
  Uploads use Active Storage: `bin/rails active_storage:install` if you haven't
180
- already. Users can pick files, paste an image from the clipboard, or drag &
181
- drop onto the form; selected files show as removable chips. Limits
182
- (`max_screenshots`, `max_screenshot_size`) are enforced server-side and shown
183
- as a hint in the widget. If Active Storage isn't loaded, the upload control
184
- simply doesn't render and uploads are rejected.
204
+ already. Limits (`max_screenshots`, `max_screenshot_size`) are enforced
205
+ server-side and shown as a hint in the widget. If Active Storage isn't loaded,
206
+ the upload control simply doesn't render and uploads are rejected.
185
207
 
186
208
  ### Notifications
187
209
 
@@ -32,7 +32,7 @@
32
32
  <thead>
33
33
  <tr>
34
34
  <th><%= t('feedback_engine.kind', default: 'Type') %></th>
35
- <th><%= t('feedback_engine.message', default: 'Message') %></th>
35
+ <th><%= t('feedback_engine.dashboard.message', default: 'Message') %></th>
36
36
  <th><%= t('feedback_engine.section', default: 'Section') %></th>
37
37
  <th><%= t('feedback_engine.dashboard.from', default: 'From') %></th>
38
38
  <th><%= t('feedback_engine.dashboard.filed', default: 'Filed') %></th>
@@ -33,8 +33,9 @@
33
33
  .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
34
34
  background: var(--border); font-size: 12px; text-align: center; }
35
35
  .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
36
- .filters select { padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px;
37
- background: var(--surface); color: var(--text); }
36
+ .filters select, .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--border);
37
+ border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
38
+ .filters input[type=search] { flex: 1; max-width: 320px; }
38
39
  .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
39
40
  table { width: 100%; border-collapse: collapse; }
40
41
  th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
@@ -30,6 +30,7 @@ en:
30
30
  title: "Feedback"
31
31
  all_kinds: "All types"
32
32
  filter: "Filter"
33
+ message: "Message"
33
34
  search: "Search"
34
35
  search_placeholder: "Search message, author, section…"
35
36
  from: "From"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FeedbackEngine
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.1'
5
5
  end
@@ -20,8 +20,6 @@
20
20
  var Z = 2147483000;
21
21
  var overlay = null;
22
22
  var lastFocused = null;
23
- var fileInput = null;
24
- var fileChips = null;
25
23
 
26
24
  function ready(fn) {
27
25
  if (document.readyState === "loading") {
@@ -114,25 +112,6 @@
114
112
  // Keep Tab (and Shift+Tab) cycling inside the dialog while it is open.
115
113
  overlay.addEventListener("keydown", trapFocus);
116
114
 
117
- // Screenshots can be pasted (Cmd/Ctrl+V) or dropped anywhere on the form —
118
- // the file picker is just one way in.
119
- if (config.screenshots.enabled) {
120
- dialog.addEventListener("paste", function (event) {
121
- var files = event.clipboardData && event.clipboardData.files;
122
- if (files && files.length) {
123
- event.preventDefault();
124
- addFiles(files);
125
- }
126
- });
127
- dialog.addEventListener("dragover", function (event) {
128
- event.preventDefault();
129
- });
130
- dialog.addEventListener("drop", function (event) {
131
- event.preventDefault();
132
- if (event.dataTransfer) addFiles(event.dataTransfer.files);
133
- });
134
- }
135
-
136
115
  var first = dialog.querySelector("select, textarea, input");
137
116
  if (first) first.focus();
138
117
  }
@@ -141,8 +120,6 @@
141
120
  if (!overlay) return;
142
121
  overlay.remove();
143
122
  overlay = null;
144
- fileInput = null;
145
- fileChips = null;
146
123
  if (lastFocused && lastFocused.focus) lastFocused.focus();
147
124
  }
148
125
 
@@ -265,69 +242,19 @@
265
242
  }
266
243
 
267
244
  function screenshotsField() {
268
- fileInput = document.createElement("input");
269
- fileInput.type = "file";
270
- fileInput.name = "screenshots";
271
- fileInput.multiple = true;
272
- fileInput.accept = "image/*";
273
- fileInput.addEventListener("change", renderFileChips);
274
- var wrap = field(config.labels.screenshots, fileInput);
245
+ var input = document.createElement("input");
246
+ input.type = "file";
247
+ input.name = "screenshots";
248
+ input.multiple = true;
249
+ input.accept = "image/*";
250
+ var wrap = field(config.labels.screenshots, input);
275
251
  var hint = document.createElement("span");
276
252
  hint.className = "fbe-hint";
277
253
  hint.textContent = config.labels.screenshotsHint;
278
254
  wrap.appendChild(hint);
279
- fileChips = document.createElement("ul");
280
- fileChips.className = "fbe-chips";
281
- wrap.appendChild(fileChips);
282
255
  return wrap;
283
256
  }
284
257
 
285
- // Merge pasted/dropped images into the file input (the single source of
286
- // truth for what gets uploaded), capped at the configured maximum.
287
- function addFiles(files) {
288
- if (!fileInput) return;
289
- var transfer = new DataTransfer();
290
- var current = Array.prototype.slice.call(fileInput.files);
291
- var incoming = Array.prototype.slice.call(files).filter(function (file) {
292
- return /^image\//.test(file.type);
293
- });
294
- current.concat(incoming).slice(0, config.screenshots.max).forEach(function (file) {
295
- transfer.items.add(file);
296
- });
297
- fileInput.files = transfer.files;
298
- renderFileChips();
299
- }
300
-
301
- function removeFile(index) {
302
- if (!fileInput) return;
303
- var transfer = new DataTransfer();
304
- Array.prototype.slice.call(fileInput.files).forEach(function (file, i) {
305
- if (i !== index) transfer.items.add(file);
306
- });
307
- fileInput.files = transfer.files;
308
- renderFileChips();
309
- }
310
-
311
- function renderFileChips() {
312
- if (!fileChips) return;
313
- fileChips.textContent = "";
314
- Array.prototype.slice.call(fileInput.files).forEach(function (file, index) {
315
- var chip = document.createElement("li");
316
- var name = document.createElement("span");
317
- name.textContent = file.name;
318
- var remove = document.createElement("button");
319
- remove.type = "button";
320
- remove.setAttribute("aria-label", config.labels.close + " " + file.name);
321
- remove.textContent = "×";
322
- remove.addEventListener("click", function () {
323
- removeFile(index);
324
- });
325
- chip.appendChild(name);
326
- chip.appendChild(remove);
327
- fileChips.appendChild(chip);
328
- });
329
- }
330
-
331
258
  // --- submit -------------------------------------------------------------------
332
259
 
333
260
  function submit(form) {
@@ -436,12 +363,6 @@
436
363
  "border:1px solid #d1d5db;border-radius:6px;background:none;color:#1c2024;font:inherit;cursor:pointer}",
437
364
  "#fbe-dialog textarea{resize:vertical}",
438
365
  "#fbe-dialog .fbe-hint{display:block;margin-top:4px;font-size:12px;color:#6b7280;font-weight:400}",
439
- "#fbe-dialog .fbe-chips{list-style:none;margin:6px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:6px}",
440
- "#fbe-dialog .fbe-chips li{display:flex;align-items:center;gap:4px;max-width:100%;",
441
- "padding:2px 4px 2px 10px;border:1px solid #d1d5db;border-radius:999px;font-size:12px;font-weight:400}",
442
- "#fbe-dialog .fbe-chips span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}",
443
- "#fbe-dialog .fbe-chips button{border:0;background:none;color:inherit;cursor:pointer;",
444
- "font-size:14px;line-height:1;padding:2px 6px}",
445
366
  "#fbe-dialog .fbe-error{color:#dc2626;margin:0 0 12px}",
446
367
  "#fbe-dialog .fbe-actions{display:flex;justify-content:flex-end;gap:8px}",
447
368
  "#fbe-dialog button{padding:8px 14px;border-radius:8px;cursor:pointer;font:inherit}",
@@ -454,7 +375,6 @@
454
375
  "#fbe-dialog select,#fbe-dialog textarea,#fbe-dialog input[type=file]{border-color:#2a313a}",
455
376
  "#fbe-dialog input[type=file]{color:#9aa2ab}",
456
377
  "#fbe-dialog input[type=file]::file-selector-button{border-color:#2a313a;color:#e6e8ea}",
457
- "#fbe-dialog .fbe-chips li{border-color:#2a313a}",
458
378
  "#fbe-dialog .fbe-secondary{border-color:#2a313a}",
459
379
  "#fbe-dialog .fbe-hint{color:#9aa2ab}",
460
380
  "}"
@@ -7,9 +7,11 @@ module FeedbackEngine
7
7
  # framework, no build step) and styles itself inline, so it drops into any
8
8
  # Rails app regardless of its CSS or JS setup. It is inlined into the page
9
9
  # rather than served as a separate asset to avoid any dependency on the
10
- # host's asset pipeline.
10
+ # host's asset pipeline — and it lives under lib/ (not app/assets/) so a
11
+ # host that *does* run a pipeline never ingests it either: nothing lands in
12
+ # the host's asset namespace or precompiled output.
11
13
  module Widget
12
- SOURCE = File.expand_path('../../app/assets/feedback_engine/widget.js', __dir__)
14
+ SOURCE = File.expand_path('widget.js', __dir__)
13
15
 
14
16
  # Right-to-left scripts, so the form renders mirrored for those locales.
15
17
  # Matched on the language subtag, so region variants ("ar-EG") count too.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feedback_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -38,7 +38,6 @@ files:
38
38
  - MIT-LICENSE
39
39
  - README.md
40
40
  - Rakefile
41
- - app/assets/feedback_engine/widget.js
42
41
  - app/controllers/feedback_engine/application_controller.rb
43
42
  - app/controllers/feedback_engine/feedbacks_controller.rb
44
43
  - app/controllers/feedback_engine/screenshots_controller.rb
@@ -79,6 +78,7 @@ files:
79
78
  - lib/feedback_engine/configuration.rb
80
79
  - lib/feedback_engine/engine.rb
81
80
  - lib/feedback_engine/version.rb
81
+ - lib/feedback_engine/widget.js
82
82
  - lib/feedback_engine/widget.rb
83
83
  - lib/generators/feedback_engine/install/install_generator.rb
84
84
  - lib/generators/feedback_engine/install/templates/create_feedback_engine_feedbacks.rb.tt
@@ -87,8 +87,10 @@ homepage: https://github.com/yshmarov/feedback-engine
87
87
  licenses:
88
88
  - MIT
89
89
  metadata:
90
+ homepage_uri: https://github.com/yshmarov/feedback-engine
90
91
  source_code_uri: https://github.com/yshmarov/feedback-engine
91
92
  changelog_uri: https://github.com/yshmarov/feedback-engine/blob/main/CHANGELOG.md
93
+ bug_tracker_uri: https://github.com/yshmarov/feedback-engine/issues
92
94
  rubygems_mfa_required: 'true'
93
95
  rdoc_options: []
94
96
  require_paths: