lsa_tdx_feedback 1.0.4 → 2.0.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: 9ba11981b7bf60500f49c19285a8e70e401e0b1901fd81daec64b9838deba7fa
4
- data.tar.gz: c1a0858eabc19efa362d67283db2b10b2899b2fdf8d2cd1110727f574a92e143
3
+ metadata.gz: 69d0dae8aaebab74ea26a463495be295ec35369e9ec579cdfe7ee008837fcbb3
4
+ data.tar.gz: de81da8ce4c00ed435be260b49d7cd0bd447d963dae02c1d8094ece55fa9bd2d
5
5
  SHA512:
6
- metadata.gz: 31ba0604bf35ee495ecb501da72a4bac4c0c8ad3101427d001fcf2156bc90cdc4ff8e49e0f994cd8c162210048db55873b83f1149dce2d20dd081cfbee6a9906
7
- data.tar.gz: f7896b3c35231a37dcdf798d9ed1a6745c3415e8ba7973a8ef5bd4513c615745bb721941a895dd1f7d9936b016304ce265a89824906864c912de9d1e85d4eaf8
6
+ metadata.gz: 5b2dd1661fc85b467a0c656ac2a6024a2735fbb81c8c2c7df90c33579e43d8b1da3595417ea52b1c86b8ac7bf96adcfc8762db8fe7b6b41cc3894b5b3dff0210
7
+ data.tar.gz: d6bdfeb8dc93fc60fc41c03895303bef1a906a839508154e41d9bd910911bbe386b2f958c611adb00a91bb4d787a390df9fcaafc4fea5963eb0fce7aa645299a
data/CHANGELOG.md CHANGED
@@ -7,13 +7,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.1] - 2026-07-21
11
+
12
+ ### Fixed
13
+ - **Duplicate floating trigger button**: the modal partial rendered two
14
+ `#lsa-tdx-feedback-trigger` buttons (an i18n button outside the `trigger:`
15
+ guard, plus the old hardcoded button inside it). The JS click handler bound
16
+ only to the first; the second sat on top and swallowed clicks, so the modal
17
+ never opened and produced no console or server errors. Also meant
18
+ `trigger: false` still left a button on the page. There is now a single
19
+ i18n button inside the `trigger:` guard.
20
+
21
+ ## [2.0.0] - 2026-07-20
22
+
23
+ ### Fixed
24
+ - **`NameError` on controllers that don't define `current_user`**: `set_lsa_tdx_feedback_data`
25
+ referenced `current_user` unconditionally inside a debug log line, so it raised on any
26
+ controller without that method — ViewComponent's preview controller,
27
+ `Rails::HealthController`, ActiveStorage's controllers, and so on. Because the extension is
28
+ mixed into `ActionController::Base` via `on_load :action_controller`, this broke those
29
+ requests in every host application.
30
+ - Removed the leftover per-request debug logging, which also drops six `Rails.logger.info`
31
+ calls emitted on every request of every controller.
32
+ - The documented `current_user_email_for_feedback` override point is unchanged, and it was
33
+ already guarded — email prefill still works exactly as before.
34
+ - Added specs covering both cases (controller with and without `current_user`).
35
+
10
36
  ### Added
37
+ - **I18n for the modal**: every visible string in the modal now comes from I18n,
38
+ scoped under `lsa_tdx_feedback.*` (English defaults ship in
39
+ `config/locales/en.yml`, auto-loaded by the engine). Host apps can translate to
40
+ other locales — or reword any string — by defining the same scoped keys in
41
+ their own locale files, which load after the gem's and win. Keys are namespaced
42
+ so they never collide with a host app's own I18n. (The JavaScript's runtime
43
+ status/validation messages are not yet localized — a follow-up.)
44
+ - **Opt-out for the floating trigger button**: `lsa_tdx_feedback_modal(trigger: false)`
45
+ (and `lsa_tdx_feedback(trigger: false)`) render the modal WITHOUT the built-in
46
+ fixed bottom-right button, so a host can open it from its own control via
47
+ `window.LsaTdxFeedback.showModal()` — e.g. when the fixed button collides with
48
+ other fixed page chrome (cookie banners, toasts). Defaults to `true`; callers
49
+ that don't pass `trigger:` are unaffected.
50
+ - **Delivery fallback (`config.fallback`)**: an optional callable invoked with the
51
+ feedback data when a ticket can't be filed — TDX not configured, or the API call
52
+ raises — so feedback is never lost. The controller files a TDX ticket when TDX is
53
+ configured and the call succeeds, otherwise hands the data to your fallback (e.g.
54
+ emailing an admin, enqueuing a job). Defaults to `nil`, which preserves the
55
+ original behavior exactly: no fallback, error response on failure.
11
56
  - **Documentation**: Added Rails 8 authentication setup instructions to README
12
57
  - Step-by-step guide for generating Rails 8.1.1's built-in authentication system
13
58
  - Instructions for creating `current_user` helper method to match Devise pattern
14
59
  - Examples for protecting actions and excluding authentication for specific controllers
15
60
  - Documentation on using `allow_unauthenticated_access` for public pages
16
61
 
62
+ ### Changed
63
+ - **Modal is now a native `<dialog>`**: it renders in the browser's top layer
64
+ (above all page content, no z-index needed) with a built-in focus trap and
65
+ native Escape handling, so it no longer competes with host page chrome or
66
+ relies on a hand-rolled backdrop/focus/escape. The JS now saves and restores
67
+ the host's `document.body` overflow on open/close instead of clearing it
68
+ (which could clobber another component's scroll lock).
69
+ - **WCAG 2.2 AAA defaults + themeable tokens**: the stylesheet now targets AAA
70
+ (7:1 text contrast, 44px minimum touch targets) in light and dark, and its
71
+ colors/sizes are exposed as `--lsa-tdx-feedback-*` CSS custom properties for
72
+ rebranding without copying the stylesheet. A `prefers-color-scheme: dark`
73
+ variant is included, and the modal's field colors are pinned so a theme-aware
74
+ host app can't bleed low-contrast text into them. This changes the modal's
75
+ default appearance (notably a darker, AAA-contrast accent).
76
+
77
+ ### Breaking
78
+ - The modal's outer element changed from `<div>` to `<dialog>`, and the
79
+ `.lsa-tdx-feedback-modal-backdrop` div was removed (the backdrop is now the
80
+ `::backdrop` pseudo-element). If you override the modal partial, or style the
81
+ old backdrop div, update accordingly. Custom colors/sizes previously set by
82
+ editing the stylesheet should move to the `--lsa-tdx-feedback-*` properties.
83
+
17
84
  ## [1.0.4] - 2026-02-09
18
85
 
19
86
  ### Changed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- lsa_tdx_feedback (1.0.3)
4
+ lsa_tdx_feedback (2.0.1)
5
5
  httparty (~> 0.24.0)
6
6
  rails (>= 6.0)
7
7
  redis (>= 4.0)
@@ -87,7 +87,7 @@ GEM
87
87
  benchmark (0.4.1)
88
88
  bigdecimal (4.0.1)
89
89
  builder (3.3.0)
90
- concurrent-ruby (1.3.5)
90
+ concurrent-ruby (1.3.7)
91
91
  connection_pool (2.5.4)
92
92
  crack (1.0.0)
93
93
  bigdecimal
data/README.md CHANGED
@@ -148,6 +148,26 @@ This avoids FOUC (flash of unstyled content) and ensures the script loads after
148
148
  - **Optimal loading (recommended)**: Use the separate helpers so CSS loads in `<head>` and JS loads just before `</body>`. This avoids FOUC and keeps asset order predictable.
149
149
  - **Convenience**: Use `lsa_tdx_feedback` (all‑in‑one) for quick setup. It injects CSS/JS plus the modal where it’s called, which can delay CSS and is less ideal for performance.
150
150
 
151
+ #### Bring your own trigger
152
+
153
+ By default the modal ships a fixed bottom‑right floating button. To render the
154
+ modal **without** it — and open it from your own control (a nav link, a footer
155
+ button) — pass `trigger: false` and call `window.LsaTdxFeedback.showModal()`:
156
+
157
+ ```erb
158
+ <%= lsa_tdx_feedback_modal(trigger: false) %>
159
+
160
+ <button type="button" id="my-feedback-link">Send feedback</button>
161
+ <script>
162
+ document.getElementById('my-feedback-link')
163
+ .addEventListener('click', () => window.LsaTdxFeedback.showModal());
164
+ </script>
165
+ ```
166
+
167
+ This is handy when the fixed button overlaps other fixed page chrome (a cookie
168
+ banner, toasts), or when you want feedback reachable from a specific place in
169
+ your own UI. `lsa_tdx_feedback(trigger: false)` works the same way.
170
+
151
171
  #### Recommended placement (optimal)
152
172
 
153
173
  ```erb
@@ -238,6 +258,73 @@ class ApplicationController < ActionController::Base
238
258
  end
239
259
  ```
240
260
 
261
+ ### Translating or customizing the modal text
262
+
263
+ The modal's visible strings come from I18n, scoped under `lsa_tdx_feedback.*`
264
+ (English defaults ship with the gem and are auto-loaded). To translate to another
265
+ locale — or to reword any string — define the same keys in your app's locale
266
+ files; they load after the gem's, so they win:
267
+
268
+ ```yaml
269
+ # config/locales/fr.yml
270
+ fr:
271
+ lsa_tdx_feedback:
272
+ modal:
273
+ title: "Envoyer un commentaire"
274
+ submit: "Envoyer"
275
+ # ...override only what you need
276
+ ```
277
+
278
+ See `config/locales/en.yml` in the gem for the full list of keys. (The
279
+ JavaScript's runtime messages — submission success and client-side validation
280
+ text — are not yet localized.)
281
+ ### Delivery fallback (never lose feedback)
282
+
283
+ By default, if TDX isn't configured — or a ticket can't be created — the modal
284
+ shows an error. Set `config.fallback` to a callable and the gem hands it the
285
+ feedback instead, so you can email an admin, enqueue a job, or log it, and the
286
+ user still gets a success response:
287
+
288
+ ```ruby
289
+ # config/initializers/lsa_tdx_feedback.rb
290
+ LsaTdxFeedback.configure do |config|
291
+ # ...your TDX config...
292
+
293
+ config.fallback = ->(feedback_data) do
294
+ FeedbackMailer.submission(feedback_data).deliver_later
295
+ end
296
+ end
297
+ ```
298
+
299
+ The callable receives the same `feedback_data` hash the ticket would have used
300
+ (`:feedback`, `:email`, `:category`, `:url`, `:user_agent`, `:additional_info`,
301
+ `:title`, `:priority_id`). When `config.fallback` is `nil` (the default), behavior
302
+ is unchanged. A TDX ticket is still filed when TDX is configured and the call
303
+ succeeds — the fallback runs only when it can't.
304
+
305
+ ### Theming (colors, dark mode, accessibility)
306
+
307
+ The modal is a native `<dialog>` — it renders in the browser's top layer (above
308
+ your page chrome, with a built-in focus trap and Escape handling), so it needs
309
+ no z-index coordination with the rest of your app.
310
+
311
+ Its styling ships **WCAG 2.2 AAA** defaults (7:1 text contrast, 44px touch
312
+ targets) in both light and dark, and exposes colors and sizes as CSS custom
313
+ properties. To rebrand, override any of them in a stylesheet loaded after the
314
+ gem's:
315
+
316
+ ```css
317
+ :root {
318
+ --lsa-tdx-feedback-accent: #6d28d9; /* your brand color */
319
+ --lsa-tdx-feedback-accent-hover: #5b21b6;
320
+ --lsa-tdx-feedback-surface: #ffffff;
321
+ /* full list in app/assets/stylesheets/lsa_tdx_feedback.css */
322
+ }
323
+ ```
324
+
325
+ A `prefers-color-scheme: dark` variant is built in, and the modal's field colors
326
+ are pinned so a theme-aware host app can't bleed low-contrast text into them.
327
+
241
328
  ## Rails 8 Authentication Setup
242
329
 
243
330
  If you're using Rails 8.1.1's built-in authentication system, here's how to set it up and create a `current_user` helper method similar to Devise.
@@ -304,22 +391,19 @@ This makes `current_user` available in all controllers and views, including Rail
304
391
 
305
392
  ### Step 3A: Create an initializer (config/initializers/lsa_tdx_feedback.rb) that skips authentication for the feedback controller, since feedback forms should be public
306
393
 
307
- ```ruby
308
- # Configure lsa_tdx_feedback gem to allow unauthenticated access
309
- # Feedback forms should be accessible without authentication
310
- Rails.application.config.to_prepare do
311
- if defined?(LsaTdxFeedback::FeedbackController)
312
- LsaTdxFeedback::FeedbackController.class_eval do
313
- # Skip authentication for feedback submissions
314
- # Use raise: false to prevent errors if the before_action doesn't exist
315
- skip_before_action :require_authentication, raise: false
316
- end
317
- end
318
- rescue => e
319
- Rails.logger.warn("Could not configure LsaTdxFeedback: #{e.message}") if defined?(Rails.logger)
320
- end
394
+ `LsaTdxFeedback::FeedbackController` inherits from the host app's `ApplicationController`. If that controller runs a global authentication `before_action`, anonymous submissions are rejected before the action executes, producing a `401 Unauthorized` on submit:
395
+
396
+ ```text
397
+ Processing by LsaTdxFeedback::FeedbackController#create as */*
398
+ Completed 401 Unauthorized in 8ms (ActiveRecord: 0.0ms)
321
399
  ```
322
400
 
401
+ The 0‑query, sub‑10ms completion is the tell: the request was blocked by an authentication callback, not by the TDX API. Skip that callback for the feedback controller only.
402
+
403
+ #### Rails 8 built-in authentication
404
+
405
+ The Rails 8 generator names the callback `:require_authentication`:
406
+
323
407
  and update request_authentication in the Authentication concern ( app/controllers/concerns/authentication.rb ) to use main_app.new_session_path so it works when called from engine controllers.
324
408
 
325
409
  ```ruby
@@ -329,6 +413,32 @@ and update request_authentication in the Authentication concern ( app/controller
329
413
  ...
330
414
  ```
331
415
 
416
+ #### Devise
417
+
418
+ Devise apps use `:authenticate_user!`. Skip that callback instead:
419
+
420
+ ```ruby
421
+ # config/initializers/lsa_tdx_feedback.rb
422
+ # Allow anonymous (signed-out) visitors to submit feedback.
423
+ # The feedback modal is rendered on public pages, so skipping the host app's
424
+ # Devise authentication callback prevents 401 Unauthorized responses on submit.
425
+ Rails.application.config.to_prepare do
426
+ LsaTdxFeedback::FeedbackController.class_eval do
427
+ skip_before_action :authenticate_user!, raise: false
428
+ end
429
+ end
430
+ ```
431
+
432
+ For both setups, `to_prepare` re‑applies the change across code reloads in development, and `raise: false` keeps the app booting even if the callback name differs or changes in a future release.
433
+
434
+ #### The email field is informational, not an identity
435
+
436
+ The form requires an email **client‑side** before it allows submission, so the controller always receives a contact address. That value is **not authoritative** — any email can be entered, and it is recorded on the TDX ticket as‑is. For signed‑in users the email is auto‑populated for convenience (via `current_user_email_for_feedback`), but the authorization decision does **not** depend on it.
437
+
438
+ #### What still protects the endpoint
439
+
440
+ - **CSRF protection remains in effect.** The controller keeps `protect_from_forgery with: :exception`, so submissions must originate from your rendered pages with a valid CSRF token. This is the primary guard against scripted/anonymous abuse now that authentication is skipped.
441
+ - **No server‑side email validation.** The controller permits `:email` and passes it straight through to the TDX ticket; it does **not** validate presence or format (that enforcement lives only in the browser). If you want a backend safety net so a crafted request can't create a ticket with a blank or malformed email, add that validation in the gem or in a controller override.
332
442
 
333
443
  ### Step 4: Protect Actions (Optional)
334
444
 
@@ -13,9 +13,9 @@
13
13
  triggerClick: null,
14
14
  closeClick: null,
15
15
  cancelClick: null,
16
- backdropClick: null,
16
+ dialogClick: null,
17
17
  formSubmit: null,
18
- escapeKey: null
18
+ dialogClose: null
19
19
  },
20
20
 
21
21
  init: function() {
@@ -53,7 +53,6 @@
53
53
  if (this.modal) {
54
54
  var closeBtn = this.modal.querySelector('.lsa-tdx-feedback-close-btn');
55
55
  var cancelBtn = this.modal.querySelector('.lsa-tdx-feedback-cancel-btn');
56
- var backdrop = this.modal.querySelector('.lsa-tdx-feedback-modal-backdrop');
57
56
 
58
57
  if (closeBtn && this.handlers.closeClick) {
59
58
  closeBtn.removeEventListener('click', this.handlers.closeClick);
@@ -61,8 +60,11 @@
61
60
  if (cancelBtn && this.handlers.cancelClick) {
62
61
  cancelBtn.removeEventListener('click', this.handlers.cancelClick);
63
62
  }
64
- if (backdrop && this.handlers.backdropClick) {
65
- backdrop.removeEventListener('click', this.handlers.backdropClick);
63
+ if (this.handlers.dialogClick) {
64
+ this.modal.removeEventListener('click', this.handlers.dialogClick);
65
+ }
66
+ if (this.handlers.dialogClose) {
67
+ this.modal.removeEventListener('close', this.handlers.dialogClose);
66
68
  }
67
69
  }
68
70
 
@@ -70,18 +72,14 @@
70
72
  this.form.removeEventListener('submit', this.handlers.formSubmit);
71
73
  }
72
74
 
73
- if (this.handlers.escapeKey) {
74
- document.removeEventListener('keydown', this.handlers.escapeKey);
75
- }
76
-
77
75
  // Clear handler references
78
76
  this.handlers = {
79
77
  triggerClick: null,
80
78
  closeClick: null,
81
79
  cancelClick: null,
82
- backdropClick: null,
80
+ dialogClick: null,
83
81
  formSubmit: null,
84
- escapeKey: null
82
+ dialogClose: null
85
83
  };
86
84
  },
87
85
 
@@ -110,7 +108,6 @@
110
108
  // Close buttons
111
109
  var closeBtn = this.modal.querySelector('.lsa-tdx-feedback-close-btn');
112
110
  var cancelBtn = this.modal.querySelector('.lsa-tdx-feedback-cancel-btn');
113
- var backdrop = this.modal.querySelector('.lsa-tdx-feedback-modal-backdrop');
114
111
 
115
112
  if (closeBtn) {
116
113
  this.handlers.closeClick = function(e) {
@@ -128,14 +125,23 @@
128
125
  cancelBtn.addEventListener('click', this.handlers.cancelClick);
129
126
  }
130
127
 
131
- if (backdrop) {
132
- this.handlers.backdropClick = function(e) {
133
- if (e.target === backdrop) {
134
- self.hideModal();
135
- }
136
- };
137
- backdrop.addEventListener('click', this.handlers.backdropClick);
138
- }
128
+ // Backdrop click: on a native <dialog>, a click whose target is the
129
+ // dialog element itself (not its content) lands on the ::backdrop.
130
+ this.handlers.dialogClick = function(e) {
131
+ if (e.target === self.modal) {
132
+ self.hideModal();
133
+ }
134
+ };
135
+ this.modal.addEventListener('click', this.handlers.dialogClick);
136
+
137
+ // Runs on every close — button, backdrop, or the dialog's native Escape
138
+ // handling — so cleanup happens once, wherever the close came from.
139
+ this.handlers.dialogClose = function() {
140
+ document.body.style.overflow = self._previousBodyOverflow || '';
141
+ self.resetForm();
142
+ self.hideMessage();
143
+ };
144
+ this.modal.addEventListener('close', this.handlers.dialogClose);
139
145
 
140
146
  // Form submission
141
147
  this.handlers.formSubmit = function(e) {
@@ -143,20 +149,23 @@
143
149
  self.submitFeedback();
144
150
  };
145
151
  this.form.addEventListener('submit', this.handlers.formSubmit);
146
-
147
- // Escape key to close modal
148
- this.handlers.escapeKey = function(e) {
149
- if (e.key === 'Escape' && self.modal && self.modal.style.display !== 'none') {
150
- self.hideModal();
151
- }
152
- };
153
- document.addEventListener('keydown', this.handlers.escapeKey);
154
152
  },
155
153
 
156
154
  showModal: function() {
157
155
  if (!this.modal) return;
158
156
 
159
- this.modal.style.display = 'block';
157
+ // Native modal dialog: top layer + page inert + built-in focus trap.
158
+ if (typeof this.modal.showModal === 'function') {
159
+ this.modal.showModal();
160
+ } else {
161
+ // Older browsers without <dialog>: fall back to a visible dialog.
162
+ this.modal.setAttribute('open', '');
163
+ }
164
+
165
+ // Save the host's body overflow so we can RESTORE it on close rather
166
+ // than blindly clearing it (which would clobber a scroll lock another
167
+ // component may have set).
168
+ this._previousBodyOverflow = document.body.style.overflow;
160
169
  document.body.style.overflow = 'hidden';
161
170
 
162
171
  // Focus on first input
@@ -173,10 +182,13 @@
173
182
  hideModal: function() {
174
183
  if (!this.modal) return;
175
184
 
176
- this.modal.style.display = 'none';
177
- document.body.style.overflow = '';
178
- this.resetForm();
179
- this.hideMessage();
185
+ // The 'close' event handler restores body scroll + resets the form.
186
+ if (typeof this.modal.close === 'function') {
187
+ this.modal.close();
188
+ } else {
189
+ this.modal.removeAttribute('open');
190
+ if (this.handlers.dialogClose) this.handlers.dialogClose();
191
+ }
180
192
  },
181
193
 
182
194
  resetForm: function() {
@@ -1,37 +1,100 @@
1
1
  /* LsaTdxFeedback Styles - Self-contained and namespaced */
2
2
 
3
+ /*
4
+ * Themeable tokens. Override any of these in your own stylesheet (loaded after
5
+ * this one) to rebrand the modal without copying it. Defaults target WCAG 2.2
6
+ * AAA: 7:1 text contrast and 44px minimum touch targets, in light and dark.
7
+ */
8
+ :root {
9
+ --lsa-tdx-feedback-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
10
+
11
+ --lsa-tdx-feedback-surface: #ffffff;
12
+ --lsa-tdx-feedback-text: #1a1a1a;
13
+ --lsa-tdx-feedback-text-muted: #33383f; /* >= 7:1 on the surface */
14
+ --lsa-tdx-feedback-border: #ced4da;
15
+
16
+ --lsa-tdx-feedback-field-bg: #ffffff;
17
+ --lsa-tdx-feedback-field-text: #1a1a1a;
18
+ --lsa-tdx-feedback-placeholder: #4b5563; /* >= 7:1 on the field */
19
+
20
+ --lsa-tdx-feedback-accent: #1e3a8a; /* white text on this is >= 7:1 */
21
+ --lsa-tdx-feedback-accent-hover: #172e6e;
22
+ --lsa-tdx-feedback-on-accent: #ffffff;
23
+ --lsa-tdx-feedback-focus-ring: rgba(30, 58, 138, 0.35);
24
+
25
+ --lsa-tdx-feedback-backdrop: rgba(0, 0, 0, 0.5);
26
+ --lsa-tdx-feedback-target-min: 44px; /* WCAG 2.5.5 */
27
+
28
+ --lsa-tdx-feedback-success-bg: #d4edda;
29
+ --lsa-tdx-feedback-success-text: #0f5132; /* >= 7:1 on success-bg */
30
+ --lsa-tdx-feedback-success-border: #badbcc;
31
+ --lsa-tdx-feedback-error-bg: #f8d7da;
32
+ --lsa-tdx-feedback-error-text: #842029; /* >= 7:1 on error-bg */
33
+ --lsa-tdx-feedback-error-border: #f5c2c7;
34
+ }
35
+
36
+ @media (prefers-color-scheme: dark) {
37
+ :root {
38
+ --lsa-tdx-feedback-surface: #1e232b;
39
+ --lsa-tdx-feedback-text: #f1f5f9;
40
+ --lsa-tdx-feedback-text-muted: #c4ccd6;
41
+ --lsa-tdx-feedback-border: #333b45;
42
+
43
+ --lsa-tdx-feedback-field-bg: #12161c;
44
+ --lsa-tdx-feedback-field-text: #f1f5f9;
45
+ --lsa-tdx-feedback-placeholder: #b3bcc9;
46
+
47
+ /* accent + white text stay AAA on the dark surface */
48
+
49
+ --lsa-tdx-feedback-success-bg: #0f2e1d;
50
+ --lsa-tdx-feedback-success-text: #a7f3d0;
51
+ --lsa-tdx-feedback-success-border: #1c4a30;
52
+ --lsa-tdx-feedback-error-bg: #3b1418;
53
+ --lsa-tdx-feedback-error-text: #fecaca;
54
+ --lsa-tdx-feedback-error-border: #5b1d23;
55
+ }
56
+ }
57
+
3
58
  /* Trigger Button */
4
59
  .lsa-tdx-feedback-trigger-btn {
5
60
  position: fixed;
6
61
  bottom: 20px;
7
62
  right: 20px;
8
- background: #0D6EFD;
9
- color: white;
63
+ min-width: var(--lsa-tdx-feedback-target-min);
64
+ min-height: var(--lsa-tdx-feedback-target-min);
65
+ background: var(--lsa-tdx-feedback-accent);
66
+ color: var(--lsa-tdx-feedback-on-accent);
10
67
  border: none;
11
68
  border-radius: 25px;
12
69
  padding: 12px 20px;
13
70
  font-size: 14px;
14
71
  font-weight: 600;
15
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
72
+ font-family: var(--lsa-tdx-feedback-font);
16
73
  cursor: pointer;
17
- box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
74
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
18
75
  transition: all 0.2s ease;
19
76
  z-index: 9998;
20
77
  display: flex;
21
78
  align-items: center;
79
+ justify-content: center;
22
80
  gap: 8px;
23
81
  }
24
82
 
25
83
  .lsa-tdx-feedback-trigger-btn:hover {
26
- background: #0056b3;
84
+ background: var(--lsa-tdx-feedback-accent-hover);
27
85
  transform: translateY(-2px);
28
- box-shadow: 0 6px 20px rgba(0, 123, 255, 0.4);
86
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
29
87
  }
30
88
 
31
89
  .lsa-tdx-feedback-trigger-btn:active {
32
90
  transform: translateY(0);
33
91
  }
34
92
 
93
+ .lsa-tdx-feedback-trigger-btn:focus-visible {
94
+ outline: 3px solid var(--lsa-tdx-feedback-accent);
95
+ outline-offset: 2px;
96
+ }
97
+
35
98
  .lsa-tdx-feedback-trigger-text {
36
99
  display: inline;
37
100
  }
@@ -44,45 +107,41 @@
44
107
  font-size: 13px;
45
108
  }
46
109
 
110
+ /* min-width/min-height keep the 44px floor even with the label hidden */
47
111
  .lsa-tdx-feedback-trigger-text {
48
112
  display: none;
49
113
  }
50
114
  }
51
115
 
52
- /* Modal Styles */
116
+ /*
117
+ * Modal — a native <dialog>. The browser promotes it to the top layer (renders
118
+ * above all page content, no z-index needed) and makes the rest of the page
119
+ * inert. The dialog element itself is the visible panel; the backdrop is the
120
+ * ::backdrop pseudo-element.
121
+ */
53
122
  .lsa-tdx-feedback-modal {
54
- position: fixed;
55
- top: 0;
56
- left: 0;
57
- width: 100%;
58
- height: 100%;
59
- z-index: 9999;
60
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
61
- }
62
-
63
- .lsa-tdx-feedback-modal-backdrop {
64
- position: absolute;
65
- top: 0;
66
- left: 0;
67
- width: 100%;
68
- height: 100%;
69
- background: rgba(0, 0, 0, 0.5);
70
- backdrop-filter: blur(4px);
71
- }
72
-
73
- .lsa-tdx-feedback-modal-content {
74
- position: relative;
75
- background: white;
123
+ padding: 0;
124
+ border: none;
76
125
  border-radius: 12px;
77
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
126
+ background: var(--lsa-tdx-feedback-surface);
127
+ color: var(--lsa-tdx-feedback-text);
128
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
78
129
  max-width: 500px;
79
130
  width: 90%;
80
131
  max-height: 90vh;
81
132
  overflow-y: auto;
82
- margin: 5vh auto;
133
+ font-family: var(--lsa-tdx-feedback-font);
134
+ }
135
+
136
+ .lsa-tdx-feedback-modal[open] {
83
137
  animation: lsa-tdx-feedback-modal-enter 0.3s ease-out;
84
138
  }
85
139
 
140
+ .lsa-tdx-feedback-modal::backdrop {
141
+ background: var(--lsa-tdx-feedback-backdrop);
142
+ backdrop-filter: blur(4px);
143
+ }
144
+
86
145
  @keyframes lsa-tdx-feedback-modal-enter {
87
146
  from {
88
147
  opacity: 0;
@@ -94,9 +153,20 @@
94
153
  }
95
154
  }
96
155
 
156
+ @media (prefers-reduced-motion: reduce) {
157
+ .lsa-tdx-feedback-modal[open] {
158
+ animation: none;
159
+ }
160
+ }
161
+
162
+ /* Content is now a plain wrapper — the dialog element carries the panel look. */
163
+ .lsa-tdx-feedback-modal-content {
164
+ position: relative;
165
+ }
166
+
97
167
  .lsa-tdx-feedback-modal-header {
98
168
  padding: 24px 24px 16px;
99
- border-bottom: 1px solid #e9ecef;
169
+ border-bottom: 1px solid var(--lsa-tdx-feedback-border);
100
170
  display: flex;
101
171
  justify-content: space-between;
102
172
  align-items: center;
@@ -106,18 +176,19 @@
106
176
  margin: 0;
107
177
  font-size: 20px;
108
178
  font-weight: 600;
109
- color: #212529;
179
+ color: var(--lsa-tdx-feedback-text);
110
180
  }
111
181
 
112
182
  .lsa-tdx-feedback-close-btn {
113
183
  background: none;
114
184
  border: none;
115
185
  font-size: 24px;
116
- color: #6c757d;
186
+ line-height: 1;
187
+ color: var(--lsa-tdx-feedback-text-muted);
117
188
  cursor: pointer;
118
189
  padding: 0;
119
- width: 32px;
120
- height: 32px;
190
+ min-width: var(--lsa-tdx-feedback-target-min);
191
+ min-height: var(--lsa-tdx-feedback-target-min);
121
192
  display: flex;
122
193
  align-items: center;
123
194
  justify-content: center;
@@ -126,8 +197,13 @@
126
197
  }
127
198
 
128
199
  .lsa-tdx-feedback-close-btn:hover {
129
- background: #f8f9fa;
130
- color: #495057;
200
+ background: var(--lsa-tdx-feedback-border);
201
+ color: var(--lsa-tdx-feedback-text);
202
+ }
203
+
204
+ .lsa-tdx-feedback-close-btn:focus-visible {
205
+ outline: 3px solid var(--lsa-tdx-feedback-accent);
206
+ outline-offset: 2px;
131
207
  }
132
208
 
133
209
  .lsa-tdx-feedback-modal-body {
@@ -142,7 +218,7 @@
142
218
  display: block;
143
219
  margin-bottom: 6px;
144
220
  font-weight: 500;
145
- color: #495057;
221
+ color: var(--lsa-tdx-feedback-text);
146
222
  font-size: 14px;
147
223
  }
148
224
 
@@ -151,7 +227,12 @@
151
227
  .lsa-tdx-feedback-form-group textarea {
152
228
  width: 100%;
153
229
  padding: 10px 12px;
154
- border: 1px solid #ced4da;
230
+ min-height: var(--lsa-tdx-feedback-target-min);
231
+ /* Pin field colors so a theme-aware host app's global form styles can't
232
+ bleed light-on-light (or dark-on-dark) into the modal. */
233
+ background-color: var(--lsa-tdx-feedback-field-bg);
234
+ color: var(--lsa-tdx-feedback-field-text);
235
+ border: 1px solid var(--lsa-tdx-feedback-border);
155
236
  border-radius: 6px;
156
237
  font-size: 14px;
157
238
  font-family: inherit;
@@ -159,12 +240,17 @@
159
240
  box-sizing: border-box;
160
241
  }
161
242
 
243
+ .lsa-tdx-feedback-form-group input::placeholder,
244
+ .lsa-tdx-feedback-form-group textarea::placeholder {
245
+ color: var(--lsa-tdx-feedback-placeholder);
246
+ }
247
+
162
248
  .lsa-tdx-feedback-form-group input:focus,
163
249
  .lsa-tdx-feedback-form-group select:focus,
164
250
  .lsa-tdx-feedback-form-group textarea:focus {
165
251
  outline: none;
166
- border-color: #007bff;
167
- box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
252
+ border-color: var(--lsa-tdx-feedback-accent);
253
+ box-shadow: 0 0 0 3px var(--lsa-tdx-feedback-focus-ring);
168
254
  }
169
255
 
170
256
  .lsa-tdx-feedback-form-group textarea {
@@ -176,7 +262,7 @@
176
262
  display: block;
177
263
  margin-top: 4px;
178
264
  font-size: 12px;
179
- color: #6c757d;
265
+ color: var(--lsa-tdx-feedback-text-muted);
180
266
  }
181
267
 
182
268
  .lsa-tdx-feedback-modal-footer {
@@ -188,6 +274,7 @@
188
274
 
189
275
  .lsa-tdx-feedback-btn {
190
276
  padding: 10px 20px;
277
+ min-height: var(--lsa-tdx-feedback-target-min);
191
278
  border-radius: 6px;
192
279
  font-size: 14px;
193
280
  font-weight: 500;
@@ -196,33 +283,39 @@
196
283
  border: 1px solid transparent;
197
284
  display: inline-flex;
198
285
  align-items: center;
286
+ justify-content: center;
199
287
  gap: 6px;
200
288
  font-family: inherit;
201
289
  }
202
290
 
291
+ .lsa-tdx-feedback-btn:focus-visible {
292
+ outline: 3px solid var(--lsa-tdx-feedback-accent);
293
+ outline-offset: 2px;
294
+ }
295
+
203
296
  .lsa-tdx-feedback-btn-primary {
204
- background: #007bff;
205
- color: white;
297
+ background: var(--lsa-tdx-feedback-accent);
298
+ color: var(--lsa-tdx-feedback-on-accent);
206
299
  }
207
300
 
208
301
  .lsa-tdx-feedback-btn-primary:hover:not(:disabled) {
209
- background: #0056b3;
302
+ background: var(--lsa-tdx-feedback-accent-hover);
210
303
  }
211
304
 
212
305
  .lsa-tdx-feedback-btn-primary:disabled {
213
- background: #6c757d;
306
+ opacity: 0.65;
214
307
  cursor: not-allowed;
215
308
  }
216
309
 
217
310
  .lsa-tdx-feedback-btn-secondary {
218
- background: white;
219
- color: #6c757d;
220
- border-color: #ced4da;
311
+ background: var(--lsa-tdx-feedback-surface);
312
+ color: var(--lsa-tdx-feedback-text-muted);
313
+ border-color: var(--lsa-tdx-feedback-border);
221
314
  }
222
315
 
223
316
  .lsa-tdx-feedback-btn-secondary:hover {
224
- background: #f8f9fa;
225
- color: #495057;
317
+ background: var(--lsa-tdx-feedback-border);
318
+ color: var(--lsa-tdx-feedback-text);
226
319
  }
227
320
 
228
321
  .lsa-tdx-feedback-loading-spinner {
@@ -234,6 +327,12 @@
234
327
  to { transform: rotate(360deg); }
235
328
  }
236
329
 
330
+ @media (prefers-reduced-motion: reduce) {
331
+ .lsa-tdx-feedback-loading-spinner {
332
+ animation-duration: 0s;
333
+ }
334
+ }
335
+
237
336
  /* Messages */
238
337
  .lsa-tdx-feedback-message {
239
338
  margin: 16px 24px 0;
@@ -243,23 +342,22 @@
243
342
  }
244
343
 
245
344
  .lsa-tdx-feedback-message.success {
246
- background: #d4edda;
247
- color: #155724;
248
- border: 1px solid #c3e6cb;
345
+ background: var(--lsa-tdx-feedback-success-bg);
346
+ color: var(--lsa-tdx-feedback-success-text);
347
+ border: 1px solid var(--lsa-tdx-feedback-success-border);
249
348
  }
250
349
 
251
350
  .lsa-tdx-feedback-message.error {
252
- background: #f8d7da;
253
- color: #721c24;
254
- border: 1px solid #f5c6cb;
351
+ background: var(--lsa-tdx-feedback-error-bg);
352
+ color: var(--lsa-tdx-feedback-error-text);
353
+ border: 1px solid var(--lsa-tdx-feedback-error-border);
255
354
  }
256
355
 
257
356
  /* Mobile Responsive */
258
357
  @media (max-width: 768px) {
259
- .lsa-tdx-feedback-modal-content {
260
- margin: 2vh auto;
261
- max-height: 96vh;
358
+ .lsa-tdx-feedback-modal {
262
359
  width: 95%;
360
+ max-height: 96vh;
263
361
  }
264
362
 
265
363
  .lsa-tdx-feedback-modal-header,
@@ -274,6 +372,5 @@
274
372
 
275
373
  .lsa-tdx-feedback-btn {
276
374
  width: 100%;
277
- justify-content: center;
278
375
  }
279
376
  }
@@ -4,8 +4,29 @@ module LsaTdxFeedback
4
4
  before_action :set_lsa_tdx_feedback_data
5
5
 
6
6
  def create
7
- ticket_client = TicketClient.new
7
+ feedback_data = build_feedback_data
8
8
 
9
+ # No fallback configured: preserve the original behavior exactly.
10
+ return create_via_tdx(feedback_data) if LsaTdxFeedback.configuration.fallback.nil?
11
+
12
+ # Fallback configured: file a TDX ticket when TDX is configured and the
13
+ # call succeeds; otherwise hand the feedback to the fallback so it's never
14
+ # lost (e.g. TDX creds absent, OAuth/API failure).
15
+ if LsaTdxFeedback.configuration.valid?
16
+ begin
17
+ ticket = TicketClient.new.create_feedback_ticket(feedback_data)
18
+ return render_ticket_created(ticket)
19
+ rescue StandardError => e
20
+ Rails.logger.error "LsaTdxFeedback: ticket failed (#{e.class}: #{e.message}); using fallback"
21
+ end
22
+ end
23
+
24
+ deliver_via_fallback(feedback_data)
25
+ end
26
+
27
+ private
28
+
29
+ def build_feedback_data
9
30
  # Build the title and include the app name if available
10
31
  title = feedback_params[:title].presence || "User Feedback - #{feedback_params[:category]}"
11
32
 
@@ -13,7 +34,7 @@ module LsaTdxFeedback
13
34
  title = "[#{@lsa_tdx_feedback_app_name}]: #{title}"
14
35
  end
15
36
 
16
- feedback_data = {
37
+ {
17
38
  title: title,
18
39
  feedback: feedback_params[:feedback],
19
40
  category: feedback_params[:category],
@@ -23,33 +44,51 @@ module LsaTdxFeedback
23
44
  additional_info: feedback_params[:additional_info],
24
45
  priority_id: priority_from_category(feedback_params[:category])
25
46
  }
47
+ end
26
48
 
27
- begin
28
- ticket_response = ticket_client.create_feedback_ticket(feedback_data)
29
-
30
- render json: {
31
- success: true,
32
- message: 'Thank you for your feedback! Your ticket has been created.',
33
- ticket_id: ticket_response['ID']
34
- }, status: :created
35
- rescue LsaTdxFeedback::Error => e
36
- Rails.logger.error "LsaTdxFeedback Error: #{e.message}"
37
-
38
- render json: {
39
- success: false,
40
- message: 'Sorry, there was an error submitting your feedback. Please try again later.'
41
- }, status: :unprocessable_entity
42
- rescue StandardError => e
43
- Rails.logger.error "Unexpected error in LsaTdxFeedback: #{e.message}\n#{e.backtrace.join("\n")}"
44
-
45
- render json: {
46
- success: false,
47
- message: 'Sorry, there was an unexpected error. Please try again later.'
48
- }, status: :internal_server_error
49
- end
49
+ # The original create path, unchanged — used when no fallback is configured.
50
+ def create_via_tdx(feedback_data)
51
+ render_ticket_created(TicketClient.new.create_feedback_ticket(feedback_data))
52
+ rescue LsaTdxFeedback::Error => e
53
+ Rails.logger.error "LsaTdxFeedback Error: #{e.message}"
54
+
55
+ render json: {
56
+ success: false,
57
+ message: 'Sorry, there was an error submitting your feedback. Please try again later.'
58
+ }, status: :unprocessable_entity
59
+ rescue StandardError => e
60
+ Rails.logger.error "Unexpected error in LsaTdxFeedback: #{e.message}\n#{e.backtrace.join("\n")}"
61
+
62
+ render json: {
63
+ success: false,
64
+ message: 'Sorry, there was an unexpected error. Please try again later.'
65
+ }, status: :internal_server_error
50
66
  end
51
67
 
52
- private
68
+ def render_ticket_created(ticket_response)
69
+ render json: {
70
+ success: true,
71
+ message: 'Thank you for your feedback! Your ticket has been created.',
72
+ ticket_id: ticket_response['ID']
73
+ }, status: :created
74
+ end
75
+
76
+ def deliver_via_fallback(feedback_data)
77
+ LsaTdxFeedback.configuration.fallback.call(feedback_data)
78
+
79
+ render json: {
80
+ success: true,
81
+ message: 'Thank you for your feedback!'
82
+ }, status: :created
83
+ rescue StandardError => e
84
+ # A failing fallback must not surface as a 500 — report a retryable error.
85
+ Rails.logger.error "LsaTdxFeedback: fallback delivery failed (#{e.class}: #{e.message})"
86
+
87
+ render json: {
88
+ success: false,
89
+ message: 'Sorry, there was an error submitting your feedback. Please try again later.'
90
+ }, status: :unprocessable_entity
91
+ end
53
92
 
54
93
  def feedback_params
55
94
  params.require(:feedback).permit(:title, :feedback, :category, :email, :url, :user_agent, :additional_info)
@@ -1,59 +1,62 @@
1
1
  <!-- LsaTdxFeedback Modal -->
2
- <div id="lsa-tdx-feedback-modal" class="lsa-tdx-feedback-modal" style="display: none;">
3
- <div class="lsa-tdx-feedback-modal-backdrop"></div>
2
+ <%# Native <dialog>: the browser promotes it to the top layer (renders above all
3
+ page content with no z-index needed) and makes the rest of the page inert,
4
+ so it can't be covered by, or fight, host chrome. The backdrop is the
5
+ ::backdrop pseudo-element (no manual backdrop div). %>
6
+ <dialog id="lsa-tdx-feedback-modal" class="lsa-tdx-feedback-modal" aria-labelledby="lsa-tdx-feedback-title">
4
7
  <div class="lsa-tdx-feedback-modal-content">
5
8
  <div class="lsa-tdx-feedback-modal-header">
6
- <h2>
7
- <%= "[#{@lsa_tdx_feedback_app_name}]: " if defined?(@lsa_tdx_feedback_app_name) && @lsa_tdx_feedback_app_name.present? %>Send Feedback
9
+ <h2 id="lsa-tdx-feedback-title">
10
+ <%= "[#{@lsa_tdx_feedback_app_name}]: " if defined?(@lsa_tdx_feedback_app_name) && @lsa_tdx_feedback_app_name.present? %><%= t('lsa_tdx_feedback.modal.title') %>
8
11
  </h2>
9
- <button type="button" class="lsa-tdx-feedback-close-btn" aria-label="Close">&times;</button>
12
+ <button type="button" class="lsa-tdx-feedback-close-btn" aria-label="<%= t('lsa_tdx_feedback.modal.close') %>">&times;</button>
10
13
  </div>
11
14
 
12
15
  <div class="lsa-tdx-feedback-modal-body">
13
16
  <form id="lsa-tdx-feedback-form">
14
17
  <div class="lsa-tdx-feedback-form-group">
15
- <label for="lsa-tdx-feedback-category">Category</label>
18
+ <label for="lsa-tdx-feedback-category"><%= t('lsa_tdx_feedback.modal.category_label') %></label>
16
19
  <select id="lsa-tdx-feedback-category" name="category" required>
17
- <option value="">Select a category</option>
18
- <option value="bug">Bug Report</option>
19
- <option value="suggestion">Suggestion</option>
20
- <option value="feature">Feature Request</option>
21
- <option value="general">General Feedback</option>
22
- <option value="other">Other</option>
20
+ <option value=""><%= t('lsa_tdx_feedback.modal.category_prompt') %></option>
21
+ <option value="bug"><%= t('lsa_tdx_feedback.modal.categories.bug') %></option>
22
+ <option value="suggestion"><%= t('lsa_tdx_feedback.modal.categories.suggestion') %></option>
23
+ <option value="feature"><%= t('lsa_tdx_feedback.modal.categories.feature') %></option>
24
+ <option value="general"><%= t('lsa_tdx_feedback.modal.categories.general') %></option>
25
+ <option value="other"><%= t('lsa_tdx_feedback.modal.categories.other') %></option>
23
26
  </select>
24
27
  </div>
25
28
 
26
29
  <div class="lsa-tdx-feedback-form-group">
27
- <label for="lsa-tdx-feedback-feedback">Your Feedback *</label>
30
+ <label for="lsa-tdx-feedback-feedback"><%= t('lsa_tdx_feedback.modal.feedback_label') %> *</label>
28
31
  <textarea
29
32
  id="lsa-tdx-feedback-feedback"
30
33
  name="feedback"
31
34
  required
32
35
  rows="5"
33
- placeholder="Please describe your feedback in detail..."
36
+ placeholder="<%= t('lsa_tdx_feedback.modal.feedback_placeholder') %>"
34
37
  ></textarea>
35
38
  </div>
36
39
 
37
40
  <div class="lsa-tdx-feedback-form-group">
38
- <label for="lsa-tdx-feedback-email">Your Email *</label>
41
+ <label for="lsa-tdx-feedback-email"><%= t('lsa_tdx_feedback.modal.email_label') %> *</label>
39
42
  <input
40
43
  type="email"
41
44
  id="lsa-tdx-feedback-email"
42
45
  name="email"
43
- placeholder="your.email@example.com"
46
+ placeholder="<%= t('lsa_tdx_feedback.modal.email_placeholder') %>"
44
47
  required
45
48
  value="<%= @lsa_tdx_feedback_user_email if defined?(@lsa_tdx_feedback_user_email) && @lsa_tdx_feedback_user_email.present? %>"
46
49
  >
47
- <small class="lsa-tdx-feedback-help-text">Required - We'll use this to follow up on your feedback</small>
50
+ <small class="lsa-tdx-feedback-help-text"><%= t('lsa_tdx_feedback.modal.email_help') %></small>
48
51
  </div>
49
52
 
50
53
  <div class="lsa-tdx-feedback-form-group">
51
- <label for="lsa-tdx-feedback-additional-info">Additional Information</label>
54
+ <label for="lsa-tdx-feedback-additional-info"><%= t('lsa_tdx_feedback.modal.additional_info_label') %></label>
52
55
  <textarea
53
56
  id="lsa-tdx-feedback-additional-info"
54
57
  name="additional_info"
55
58
  rows="3"
56
- placeholder="Any additional context or information..."
59
+ placeholder="<%= t('lsa_tdx_feedback.modal.additional_info_placeholder') %>"
57
60
  ></textarea>
58
61
  </div>
59
62
 
@@ -65,16 +68,16 @@
65
68
 
66
69
  <div class="lsa-tdx-feedback-modal-footer">
67
70
  <button type="button" class="lsa-tdx-feedback-btn lsa-tdx-feedback-btn-secondary lsa-tdx-feedback-cancel-btn">
68
- Cancel
71
+ <%= t('lsa_tdx_feedback.modal.cancel') %>
69
72
  </button>
70
73
  <button type="submit" form="lsa-tdx-feedback-form" class="lsa-tdx-feedback-btn lsa-tdx-feedback-btn-primary lsa-tdx-feedback-submit-btn">
71
- <span class="lsa-tdx-feedback-btn-text">Send Feedback</span>
74
+ <span class="lsa-tdx-feedback-btn-text"><%= t('lsa_tdx_feedback.modal.submit') %></span>
72
75
  <span class="lsa-tdx-feedback-loading-spinner" style="display: none;">
73
76
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
74
77
  <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"/>
75
78
  <path d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" fill="currentColor"/>
76
79
  </svg>
77
- Sending...
80
+ <%= t('lsa_tdx_feedback.modal.sending') %>
78
81
  </span>
79
82
  </button>
80
83
  </div>
@@ -84,12 +87,17 @@
84
87
  <div class="lsa-tdx-feedback-message-content"></div>
85
88
  </div>
86
89
  </div>
87
- </div>
90
+ </dialog>
88
91
 
89
- <!-- Feedback Button -->
90
- <button id="lsa-tdx-feedback-trigger" class="lsa-tdx-feedback-trigger-btn" title="Send Feedback">
91
- <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
92
- <path d="M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4l4 4 4-4h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/>
93
- </svg>
94
- <span class="lsa-tdx-feedback-trigger-text">Feedback</span>
95
- </button>
92
+ <%# Floating trigger button. Rendered by default; pass trigger: false to
93
+ lsa_tdx_feedback_modal to omit it and open the modal from your own control
94
+ via window.LsaTdxFeedback.showModal(). Defaults to true for callers that
95
+ render the partial without the local (backward compatible). %>
96
+ <% if local_assigns.fetch(:trigger, true) %>
97
+ <button id="lsa-tdx-feedback-trigger" class="lsa-tdx-feedback-trigger-btn" title="<%= t('lsa_tdx_feedback.modal.title') %>">
98
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
99
+ <path d="M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4l4 4 4-4h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/>
100
+ </svg>
101
+ <span class="lsa-tdx-feedback-trigger-text"><%= t('lsa_tdx_feedback.modal.trigger_label') %></span>
102
+ </button>
103
+ <% end %>
@@ -0,0 +1,30 @@
1
+ # All keys are scoped under `lsa_tdx_feedback.*` so they merge into a host
2
+ # app's I18n without colliding with its own keys. Rails auto-loads this file
3
+ # (Rails::Engine adds config/locales/**/*.yml to I18n.load_path). A host can
4
+ # translate to other locales, or override any string, by defining the same
5
+ # scoped keys in its own locale files (which load after the engine's, so they
6
+ # win).
7
+ en:
8
+ lsa_tdx_feedback:
9
+ modal:
10
+ title: "Send Feedback"
11
+ close: "Close"
12
+ category_label: "Category"
13
+ category_prompt: "Select a category"
14
+ categories:
15
+ bug: "Bug Report"
16
+ suggestion: "Suggestion"
17
+ feature: "Feature Request"
18
+ general: "General Feedback"
19
+ other: "Other"
20
+ feedback_label: "Your Feedback"
21
+ feedback_placeholder: "Please describe your feedback in detail..."
22
+ email_label: "Your Email"
23
+ email_placeholder: "your.email@example.com"
24
+ email_help: "Required - We'll use this to follow up on your feedback"
25
+ additional_info_label: "Additional Information"
26
+ additional_info_placeholder: "Any additional context or information..."
27
+ cancel: "Cancel"
28
+ submit: "Send Feedback"
29
+ sending: "Sending..."
30
+ trigger_label: "Feedback"
@@ -24,18 +24,9 @@ module LsaTdxFeedback
24
24
  nil
25
25
  end
26
26
 
27
- # Logging
28
- Rails.logger.info "set_lsa_tdx_feedback_data called"
29
- Rails.logger.info " respond_to? current_user_email_for_feedback: #{respond_to?(:current_user_email_for_feedback, true)}"
30
- Rails.logger.info " current_user available: #{respond_to?(:current_user, true)}"
31
- Rails.logger.info " current_user: #{current_user&.email || 'nil'}"
27
+ return unless respond_to?(:current_user_email_for_feedback, true)
32
28
 
33
- if respond_to?(:current_user_email_for_feedback, true)
34
- @lsa_tdx_feedback_user_email = current_user_email_for_feedback
35
- Rails.logger.info " @lsa_tdx_feedback_user_email set to: #{@lsa_tdx_feedback_user_email}"
36
- else
37
- Rails.logger.info " current_user_email_for_feedback method not available"
38
- end
29
+ @lsa_tdx_feedback_user_email = current_user_email_for_feedback
39
30
  end
40
31
 
41
32
  # Override this method in your ApplicationController to provide user email
@@ -4,7 +4,15 @@ module LsaTdxFeedback
4
4
  :default_type_id, :default_form_id, :default_classification,
5
5
  :default_status_id, :default_priority_id, :default_responsible_group_id,
6
6
  :default_service_id, :default_source_id, :service_offering_id, :account_id,
7
- :cache_store, :cache_expiry
7
+ :cache_store, :cache_expiry,
8
+ # Optional callable invoked with the feedback data hash when a
9
+ # ticket can't be filed — TDX not configured, or the API call
10
+ # raises — so feedback is never lost. It receives the same
11
+ # feedback_data the ticket would have used (see
12
+ # FeedbackController#create) and is responsible for delivering
13
+ # it (e.g. emailing an admin). nil (default) preserves the
14
+ # original behavior: no fallback, controller returns an error.
15
+ :fallback
8
16
 
9
17
  def initialize
10
18
  # All values must be configured by the application for security
@@ -27,6 +35,9 @@ module LsaTdxFeedback
27
35
  # Cache configuration
28
36
  @cache_store = :redis_cache_store
29
37
  @cache_expiry = 3600 # 1 hour for OAuth tokens
38
+
39
+ # Optional delivery fallback (see attr_accessor above)
40
+ @fallback = nil
30
41
  end
31
42
 
32
43
  def oauth_scope
@@ -1,3 +1,3 @@
1
1
  module LsaTdxFeedback
2
- VERSION = '1.0.4'
2
+ VERSION = '2.0.1'
3
3
  end
@@ -5,9 +5,15 @@ module LsaTdxFeedback
5
5
  "HELPER IS WORKING!".html_safe
6
6
  end
7
7
 
8
- # Renders the feedback modal and trigger button
9
- def lsa_tdx_feedback_modal
10
- render(partial: 'lsa_tdx_feedback/shared/feedback_modal')
8
+ # Renders the feedback modal and, by default, the floating trigger button.
9
+ #
10
+ # Pass trigger: false to render the modal WITHOUT the built-in floating
11
+ # button, and open it yourself from your own control (a nav link, a footer
12
+ # button, etc.) via `window.LsaTdxFeedback.showModal()`. Useful when the
13
+ # fixed bottom-right button collides with other fixed page chrome, or when
14
+ # you want feedback reachable from a specific place in your own UI.
15
+ def lsa_tdx_feedback_modal(trigger: true)
16
+ render(partial: 'lsa_tdx_feedback/shared/feedback_modal', locals: { trigger: trigger })
11
17
  end
12
18
 
13
19
  # Includes the feedback gem CSS
@@ -27,10 +33,11 @@ module LsaTdxFeedback
27
33
  (css + js).html_safe
28
34
  end
29
35
 
30
- # All-in-one helper that includes both modal and assets
31
- def lsa_tdx_feedback
36
+ # All-in-one helper that includes both modal and assets.
37
+ # Passes trigger: through to lsa_tdx_feedback_modal (see there).
38
+ def lsa_tdx_feedback(trigger: true)
32
39
  assets = lsa_tdx_feedback_assets
33
- modal = lsa_tdx_feedback_modal
40
+ modal = lsa_tdx_feedback_modal(trigger: trigger)
34
41
  (assets + modal).html_safe
35
42
  end
36
43
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lsa_tdx_feedback
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - LSA Rails Team
@@ -185,6 +185,7 @@ files:
185
185
  - app/assets/stylesheets/lsa_tdx_feedback.css
186
186
  - app/controllers/lsa_tdx_feedback/feedback_controller.rb
187
187
  - app/views/lsa_tdx_feedback/shared/_feedback_modal.html.erb
188
+ - config/locales/en.yml
188
189
  - config/routes.rb
189
190
  - example_application_tests.rb
190
191
  - lib/lsa_tdx_feedback.rb
@@ -216,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
217
  - !ruby/object:Gem::Version
217
218
  version: '0'
218
219
  requirements: []
219
- rubygems_version: 3.6.9
220
+ rubygems_version: 4.0.12
220
221
  specification_version: 4
221
222
  summary: A self-contained Rails gem for collecting user feedback via TDX API for LSA
222
223
  applications