ideasbugs 0.8.1 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31f280c65befb3e365927e554002f568f82b363c203385e5cf4fc14300f2a8f9
4
- data.tar.gz: ca2dae3df55aa97f5d623efbc10299c032996670fa58ece0d5dba512e2877e9b
3
+ metadata.gz: 722346f32d21b2a7007ffb9b04aeda8bf66e8194dc3eaa544bc3ea4bbdb2fe47
4
+ data.tar.gz: ca6574200a4e00570ed5a26aa9f4ed758c06192789336f4852622766b471fb5e
5
5
  SHA512:
6
- metadata.gz: b24be9267ce67ac8dcf0e481dcca9aaf44bc3a012a8831b5d9bb0cd49478bb57c3e5398ea2acd0d3b68fae23b3f006c2da9553817d7bde5cb72cabb7e5d053e8
7
- data.tar.gz: 9d44fb857d2c66e582a7efc6ea180baec1f04e3b0a29b12339c8ac08b8180e76c7a4e89febda22b7064808ac155d7b812492ef775c4f8be2acbfbaf857f16b8e
6
+ metadata.gz: aede97be97f27ce8e4cb615efc2e4ffb864ac7b1a7d09a1c6dc67fe2429a2ebb42cc01b2e65541d41a4a2908406fef9d6a90ae886a1e87ce93d78e51f3ca9d30
7
+ data.tar.gz: e1266b1bd43e2a14b5ad92c1fd1f29752bb13ee925d46a1a5939dc9ec3570d625b0d49b8e6de3d17e7cb054e4d066e86493abc8ec26043c567aa6062ffb06ed5
data/AGENTS.md CHANGED
@@ -112,7 +112,7 @@ customer.feedback.open
112
112
  ### Do not
113
113
 
114
114
  - **Do not copy the widget JavaScript into `app/javascript`, or add a `<script>` tag for it.** `ideasbugs_tag` renders what is needed and the engine serves the code same-origin. There is no build step and nothing for esbuild/importmap/Tailwind to know about.
115
- - **Do not build your own dashboard.** Use the mounted one; `config.admin_layout = "admin/application"` renders it inside an existing admin shell.
115
+ - **Do not rebuild the dashboard, and do not edit views inside the gem.** To put it inside an admin you already have, set `config.base_controller_class = "Admin::BaseController"` — it inherits that controller's layout, helpers, authentication and request context. For the shell alone, `config.admin_layout = "admin/application"`. Both work with nothing else wired up: the dashboard's own assets are declared by its views, not the layout.
116
116
  - **Do not expose screenshots by blob URL** — the gated route exists so a leaked signed URL cannot hand over a customer's screenshot.
117
117
  - **Do not set config outside the initializer.** `rate_limit` in particular is read once when the controller class loads; assigning config per-request mutates it process-wide.
118
118
  - **Do not convert the status strings to an enum** (see above).
@@ -134,6 +134,7 @@ Everything is optional; a fresh install works with zero config. Full list with c
134
134
  | `max_screenshots`, `max_screenshot_size` | `3`, `5.megabytes` | Enforced server-side |
135
135
  | `storage_service` | app default | A `storage.yml` key for a dedicated bucket |
136
136
  | `show_button`, `button_label` | `true`, localized | `false` = open from `data-ideasbugs-open` |
137
+ | `base_controller_class` | `ActionController::Base` | Controller the dashboard inherits. Name your admin's and it adopts that layout, helpers, authentication and request context. Public endpoints never inherit it. |
137
138
  | `admin_layout` | `ideasbugs/application` | Render inside your admin shell |
138
139
  | `rate_limit` | `{ to: 10, within: 60 }` | Rails 7.2+; ignored on 7.1. `nil` disables |
139
140
  | `mount_path` | `"/feedback"` | Keep in sync with `mount_ideasbugs at:` |
@@ -143,6 +144,8 @@ Everything is optional; a fresh install works with zero config. Full list with c
143
144
 
144
145
  | Symptom | Cause |
145
146
  | --- | --- |
147
+ | `NameError` for one of your own helpers in the dashboard | `isolate_namespace` scopes `helper` to the engine. Use `config.base_controller_class` so the dashboard inherits your helpers, rather than `admin_layout` alone. |
148
+ | `NotNullViolation` attaching a file on a uuid-keyed app | The tables were generated bigint. Set `primary_key_type` in `config.generators` before installing, or migrate them to uuid. |
146
149
  | `/feedback` returns 403 "Set Ideasbugs.config.authorize_admin to grant access" | Exactly what it says: still at the development-only default |
147
150
  | No Feedback button | `ideasbugs_tag` missing from the rendered layout, `config.enabled` false, or `show_button = false` with no opener of your own |
148
151
  | Submissions rejected with an invalid-token error | The layout is missing `csrf_meta_tags` |
@@ -152,6 +155,10 @@ Everything is optional; a fresh install works with zero config. Full list with c
152
155
 
153
156
  ---
154
157
 
158
+ ## One family
159
+
160
+ `testimonials`, `livechat`, `product_tours`, `i18n_proofreading` are the sibling engines. Same install shape, same host hooks (`base_controller_class`, `admin_layout`), same scoped dashboard CSS, same `primary_key_type`-aware migrations — so what you learn here transfers.
161
+
155
162
  ## Working on the gem itself
156
163
 
157
164
  ```bash
@@ -170,5 +177,6 @@ Conventions this codebase holds to — follow them rather than the first thing t
170
177
  - **Attachments stream through the engine's gate**, never a public blob URL.
171
178
  - **The widget is plain JS served same-origin by the engine** — no build step, no framework.
172
179
  - **The dummy app pins `config.active_job.queue_adapter = :test`.** Do not remove it or let it drift back to the `:async` default. Attaching a screenshot enqueues Active Storage's analysis job, and `:async` runs it on a background thread that checks out its own connection — writes no test transaction covers, landing in the middle of whatever runs next. That is a suite that fails order-dependently in a test which never created a row, and it is miserable to trace back.
180
+ - **`lib/ideasbugs/dashboard.css` is half shared.** Everything above the `GEM-SPECIFIC` banner is the design system all five gems in the family ship — the same tokens, the same `.page-head`/`.tabs`/`.filters`/`.card`/`.badge`/`button`, the same `.dashboard-shell` + `.record-row` + `.detail-panel` two-pane dashboard — identical in every repo apart from the `ib` prefix. Diff it against a sibling before changing it, and carry the change to the other four. Anything only this gem has goes below the banner. New dashboard markup reuses the shared class names rather than inventing a domain-specific one.
173
181
  - Every user-facing change bumps `lib/ideasbugs/version.rb` and adds a `CHANGELOG.md` entry that says what it costs, not only what it adds.
174
182
  - Commit messages are prose that explains the tradeoff — read `git log` before writing one.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ - **One design system across the family.** The stylesheet now opens with a
6
+ shared core — the colour tokens, `.page-head`, `.tabs`, `.filters`, `.card`,
7
+ `.badge`, buttons and form controls, and the `.dashboard-shell` +
8
+ `.record-row` + `.detail-panel` two-pane dashboard — identical in all five
9
+ gems of the family, apart from the `--ib-` prefix. The five had drifted:
10
+ sidebars between 380px and 430px, reading columns between 860px and 1020px,
11
+ two different tab styles and three different button styles. The sidebar is
12
+ now 330–430px everywhere, a reading page 1020px, a dashboard 1280px.
13
+ Everything below the `GEM-SPECIFIC` banner is what only this gem has.
14
+ - **The dashboard markup uses the shared class names.** `.triage-shell`,
15
+ `.triage-sidebar` and `.triage-detail` are `.dashboard-shell`,
16
+ `.dashboard-sidebar` and `.dashboard-detail`; `.feedback-list`,
17
+ `.feedback-row`, `.feedback-main`, `.feedback-topline`, `.feedback-message`,
18
+ `.feedback-meta` and `.feedback-time` are `.record-*`, with
19
+ `.feedback-message` becoming `.record-copy`; `.feedback-panel` is
20
+ `.detail-panel`; the attachment count sits in `.record-side`. If you styled
21
+ or scripted any of those names in a host app, that is the breaking change —
22
+ no configuration, route or database change is involved.
23
+ - **The narrow-screen rules work again.** The `max-width: 760px` block sat above
24
+ the component rules it means to override, and CSS nesting adds no specificity,
25
+ so the desktop grid won every tie: showing one pane at a time on a phone had
26
+ quietly stopped working. The media query moved to the end of the file.
27
+ - Smaller fixes that came with the shared core: a submit input is styled as a
28
+ button rather than a full-width field, the filter row keeps its search box and
29
+ button on one line, and a `code.key` truncates inside a list row instead of
30
+ wrapping over three lines.
31
+
3
32
  ## 0.8.0
4
33
 
5
34
  - **`config.admin_layout` now works on its own.** The dashboard's stylesheet was
data/README.md CHANGED
@@ -334,11 +334,24 @@ Tests run against a dummy Rails app in `test/dummy`; the widget is covered by
334
334
  Capybara system tests in a real browser. CI runs Rails 7.1 / 7.2 / 8.0 / 8.1
335
335
  against Ruby 3.2–3.4 (per-version Gemfiles in `gemfiles/`).
336
336
 
337
- ## Also by the same author
338
-
339
- - [testimonials](https://github.com/yshmarov/testimonials) testimonials,
340
- reviews and NPS for Rails. Its NPS detractor hook feeds straight into this gem.
341
- - [SupeRails](https://superails.com) — Rails screencasts.
337
+ ## One family
338
+
339
+ Five Rails engines built on the same backbone, so adopting a second one is
340
+ mostly muscle memory:
341
+
342
+ | Gem | What it does |
343
+ | --- | --- |
344
+ | [testimonials](https://github.com/yshmarov/testimonials) | Testimonials, reviews and NPS — text and video, collected in your own app |
345
+ | **ideasbugs** *(this gem)* | In-app bug reports and feature requests, with a triage queue |
346
+ | [livechat](https://github.com/yshmarov/livechat) | Live chat between your visitors and your agents, self-hosted |
347
+ | [product_tours](https://github.com/yshmarov/product_tours) | Product tours and video tutorials, shown in-app at the right moment |
348
+ | [i18n_proofreading](https://github.com/yshmarov/i18n_proofreading) | In-context translation fixes suggested by your own users |
349
+
350
+ They share the install shape (`generate <gem>:install`, mount, one initializer),
351
+ the same host hooks (`base_controller_class` to inherit your admin's controller,
352
+ `admin_layout` for just the shell), one dashboard design system — the same
353
+ two-pane layout, colour tokens and components in all five, scoped so it cannot
354
+ touch your own CSS — and migrations that follow your app's `primary_key_type`.
342
355
 
343
356
  ## License
344
357
 
@@ -1,4 +1,4 @@
1
- <section class="feedback-panel">
1
+ <section class="detail-panel">
2
2
  <div class="panel-head">
3
3
  <h1>
4
4
  <span class="badge kind-<%= feedback.kind.to_s.parameterize %>">
@@ -1,10 +1,12 @@
1
1
  <% content_for :body_class, 'ib-index' %>
2
2
 
3
3
  <%= render layout: 'ideasbugs/shared/dashboard' do %>
4
- <h1><%= t('ideasbugs.dashboard.title', default: 'IdeasBugs') %></h1>
4
+ <div class="page-head">
5
+ <h1><%= t('ideasbugs.dashboard.title', default: 'IdeasBugs') %></h1>
6
+ </div>
5
7
 
6
- <div class="triage-shell <%= 'has-selected' if @selected_feedback %>">
7
- <aside class="triage-sidebar" aria-label="<%= t('ideasbugs.dashboard.feedback', default: 'Feedback') %>">
8
+ <div class="dashboard-shell <%= 'has-selected' if @selected_feedback %>">
9
+ <aside class="dashboard-sidebar" aria-label="<%= t('ideasbugs.dashboard.feedback', default: 'Feedback') %>">
8
10
  <div class="tabs">
9
11
  <% Ideasbugs::Feedback::STATUSES.each do |status| %>
10
12
  <%= link_to feedbacks_path(status: status, kind: @kind, q: @query),
@@ -31,26 +33,26 @@
31
33
  <button><%= t('ideasbugs.dashboard.search', default: 'Search') %></button>
32
34
  </form>
33
35
 
34
- <div class="feedback-list">
36
+ <div class="record-list">
35
37
  <% if @feedbacks.any? %>
36
38
  <% @feedbacks.each do |feedback| %>
37
39
  <%= link_to feedbacks_path(status: @status, kind: @kind, q: @query, page: @page,
38
40
  feedback_id: feedback.id),
39
- class: ['feedback-row', ('active' if @selected_feedback&.id == feedback.id)].compact do %>
40
- <span class="feedback-main">
41
- <span class="feedback-topline">
41
+ class: ['record-row', ('active' if @selected_feedback&.id == feedback.id)].compact do %>
42
+ <span class="record-main">
43
+ <span class="record-topline">
42
44
  <span class="badge kind-<%= feedback.kind.to_s.parameterize %>">
43
45
  <%= t("ideasbugs.kinds.#{feedback.kind}", default: feedback.kind.humanize) %>
44
46
  </span>
45
47
  <span class="badge status-<%= feedback.status %>">
46
48
  <%= t("ideasbugs.statuses.#{feedback.status}", default: feedback.status.humanize) %>
47
49
  </span>
48
- <span class="muted feedback-time" title="<%= feedback.created_at %>">
50
+ <span class="muted record-time" title="<%= feedback.created_at %>">
49
51
  <%= time_ago_in_words(feedback.created_at) %>
50
52
  </span>
51
53
  </span>
52
- <span class="feedback-message"><%= truncate(feedback.message, length: 120) %></span>
53
- <span class="muted feedback-meta">
54
+ <span class="record-copy"><%= truncate(feedback.message, length: 120) %></span>
55
+ <span class="muted record-meta">
54
56
  <span>#<%= feedback.id %></span>
55
57
  <% if @show_section && feedback.section.present? %>
56
58
  <span class="feedback-section"><%= feedback.section %></span>
@@ -61,7 +63,7 @@
61
63
  </span>
62
64
  </span>
63
65
  <% if feedback.screenshots? %>
64
- <span class="muted attachment-count">📎 <%= feedback.screenshots.count %></span>
66
+ <span class="record-side muted">📎 <%= feedback.screenshots.count %></span>
65
67
  <% end %>
66
68
  <% end %>
67
69
  <% end %>
@@ -82,7 +84,7 @@
82
84
  </div>
83
85
  </aside>
84
86
 
85
- <main class="triage-detail" aria-label="<%= t('ideasbugs.dashboard.current_feedback', default: 'Current feedback') %>">
87
+ <main class="dashboard-detail" aria-label="<%= t('ideasbugs.dashboard.current_feedback', default: 'Current feedback') %>">
86
88
  <% if @selected_feedback %>
87
89
  <p class="mobile-back">
88
90
  <%= link_to t('ideasbugs.dashboard.back', default: '← All feedback'),
@@ -4,74 +4,59 @@
4
4
  *
5
5
  * :root custom properties, all `--ib-` prefixed so they can
6
6
  * neither overwrite a host's nor be overwritten by them.
7
- * .ib-page the page frame (was bare html/body). Only the gem's own
7
+ * .ib-page the page frame (was bare html/body). Only the gem's own
8
8
  * layout puts this class on <body>, so inside a host admin
9
9
  * these rules simply do not apply.
10
- * .ib-dashboard everything else, nested. Names like .container, .card
10
+ * .ib-dashboard everything else, nested. Names like .container, .card
11
11
  * and .tabs are confined to the dashboard's own markup, so
12
12
  * loading this file inside a host cannot restyle its chrome.
13
13
  *
14
14
  * Nothing here styles a bare `body`, `a`, `table` or `*` at the top level.
15
+ *
16
+ * Two sections follow: the design system shared by the whole gem family,
17
+ * then what only this gem has. A rule that belongs in the shared section
18
+ * belongs in all five copies of it.
19
+ *
20
+ * Order inside a section matters — components, then the page frame, then the
21
+ * narrow-screen overrides. Nesting adds no specificity, so the last rule
22
+ * wins the ties, and an override placed before the rule it means to beat
23
+ * silently does nothing.
15
24
  */
16
25
 
26
+ /* ---------------------------------------------------------------------------
27
+ * SHARED CORE — identical in testimonials, ideasbugs, livechat, product_tours
28
+ * and i18n_proofreading apart from the prefix. Diff it against a sibling gem
29
+ * before you change it, and carry the change to all five.
30
+ * ------------------------------------------------------------------------- */
17
31
 
18
32
  :root {
19
- color-scheme: light dark;
20
- --ib-bg: #f6f7f9; --ib-surface: #fff; --ib-text: #1c2024; --ib-muted: #6b7280;
21
- --ib-border: #e5e7eb; --ib-accent: #2563eb; --ib-accent-text: #fff;
22
- --ib-bug: #dc2626; --ib-feature: #16a34a; --ib-other: #6b7280;
23
- --ib-open: #2563eb; --in_review: #d97706; --ib-resolved: #16a34a;
33
+ color-scheme: light dark;
34
+ --ib-bg: #f6f7f9;
35
+ --ib-surface: #fff;
36
+ --ib-text: #1c2024;
37
+ --ib-muted: #6b7280;
38
+ --ib-border: #e5e7eb;
39
+ --ib-accent: #2563eb;
40
+ --ib-accent-text: #fff;
41
+ --ib-code: #f0f1f3;
42
+ /* The four semantic states every gem in the family badges something with. */
43
+ --ib-success: #16a34a;
44
+ --ib-warning: #d97706;
45
+ --ib-danger: #dc2626;
46
+ --ib-neutral: #6b7280;
24
47
  }
25
48
  @media (prefers-color-scheme: dark) {
26
49
  :root {
27
- --ib-bg: #111418; --ib-surface: #1a1f26; --ib-text: #e6e8ea; --ib-muted: #9aa2ab;
28
- --ib-border: #2a313a; --ib-accent: #3b82f6;
29
- }
30
- }
31
- @media (max-width: 760px) {
32
- .ib-page { font-size: 14px; }
33
- .ib-index, .ib-show { overflow-x: hidden; }
34
- .ib-index { overflow-y: auto; overflow-x: hidden; }
35
- .ib-index, .ib-index .container { min-height: 100vh; height: auto; }
36
- .ib-index .container { padding-bottom: 10px; }
37
- .ib-dashboard {
38
- & .container { padding: 14px 10px 24px; }
39
- & .feedback-panel,
40
- & .detail-scroll,
41
- & .card,
42
- & dl,
43
- & dd { min-width: 0; max-width: 100%; }
44
- & .feedback-panel .card { width: calc(100vw - 20px); }
45
- & .message,
46
- & dd,
47
- & .muted { max-width: calc(100vw - 54px); }
48
- & .triage-shell { display: block; min-height: calc(100vh - 62px); }
49
- & .triage-sidebar,
50
- & .triage-detail { min-height: calc(100vh - 62px); }
51
- & .triage-shell.has-selected .triage-sidebar { display: none; }
52
- & .triage-shell:not(.has-selected) .triage-detail { display: none; }
53
- & .feedback-list { overflow: visible; }
54
- & .feedback-row { padding: 12px; }
55
- & .feedback-meta { display: none; }
56
- & .triage-detail .feedback-panel { min-height: calc(100vh - 64px); }
57
- & .mobile-back { display: block; flex: 0 0 auto; margin: 10px 10px 0; font-size: 13px; }
58
- & .panel-head h1 { font-size: 18px; }
59
- & dl { grid-template-columns: 1fr; gap: 3px; }
50
+ --ib-bg: #111418;
51
+ --ib-surface: #1a1f26;
52
+ --ib-text: #e6e8ea;
53
+ --ib-muted: #9aa2ab;
54
+ --ib-border: #2a313a;
55
+ --ib-accent: #3b82f6;
56
+ --ib-code: #232a33;
60
57
  }
61
58
  }
62
59
 
63
- /* Page frame — the gem's own layout only. */
64
- .ib-page { margin: 0; background: var(--ib-bg); color: var(--ib-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
65
- /* Desktop triage: list on the left, selected feedback on the right. */
66
- .ib-index, .ib-index .container { height: 100vh; height: 100dvh; }
67
- .ib-index { overflow: hidden; }
68
- .ib-index .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
69
- .ib-index .container h1 { flex: 0 0 auto; margin-bottom: 14px; }
70
- /* Standalone show pages keep normal page scrolling. */
71
- .ib-show { min-height: 100vh; overflow: auto; }
72
- .ib-show .feedback-panel { gap: 0; }
73
- .ib-show .detail-scroll { overflow: visible; }
74
-
75
60
  /* Dashboard components — any layout. */
76
61
  .ib-dashboard {
77
62
  /* A scoping device, not a layout one: without this the wrapper would
@@ -81,86 +66,240 @@
81
66
  & * { box-sizing: border-box; }
82
67
  & a { color: var(--ib-accent); text-decoration: none; }
83
68
  & a:hover { text-decoration: underline; }
84
- & .container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
85
- & h1 { font-size: 22px; margin: 0 0 16px; }
86
- & .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--ib-border); margin-bottom: 16px; flex-wrap: wrap; }
87
- & .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--ib-muted); }
88
- & .tabs a.active { color: var(--ib-text); font-weight: 600; border: 1px solid var(--ib-border); border-bottom: 2px solid var(--ib-surface); background: var(--ib-surface); margin-bottom: -1px; }
89
- & .tabs a:hover { text-decoration: none; color: var(--ib-text); }
90
- & .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px; background: var(--ib-border); font-size: 12px; text-align: center; }
91
- & .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
92
- & .filters select,
93
- & .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--ib-border); border-radius: 8px; background: var(--ib-surface); color: var(--ib-text); font: inherit; }
94
- & .filters input[type=search] { flex: 1; max-width: 320px; }
95
- & .card { background: var(--ib-surface); border: 1px solid var(--ib-border); border-radius: 12px; overflow: hidden; }
96
- & .card.pad { padding: 16px; }
69
+ & :focus-visible { outline: 2px solid var(--ib-accent); outline-offset: 2px; }
70
+
71
+ /* Page furniture. `.container` is the reading width; an index page widens it
72
+ to 1280px in the page frame below. */
73
+ & .container { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
74
+ & h1 { margin: 0 0 16px; font-size: 22px; }
75
+ & h2 { margin: 0 0 12px; font-size: 16px; }
76
+ & .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
77
+ & .page-head h1 { margin: 0; }
78
+ & .lede { margin: 4px 0 0; color: var(--ib-muted); }
79
+ & .breadcrumb { margin: 0 0 12px; font-size: 13px; }
80
+ & .muted { color: var(--ib-muted); font-size: 13px; }
81
+ & .case-id { color: var(--ib-muted); font-weight: 400; font-size: 15px; }
82
+ & .spacer { flex: 1; }
83
+ & .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
84
+
85
+ /* Section switcher: the pills above the dashboard when a gem has more than
86
+ one section. Lives on the page, so a host `admin_layout` can drop it. */
87
+ & .nav { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
88
+ & .nav a { padding: 6px 16px; border: 1px solid var(--ib-border); border-radius: 999px; background: var(--ib-surface); color: var(--ib-muted); font-weight: 600; }
89
+ & .nav a:hover { text-decoration: none; color: var(--ib-text); }
90
+ & .nav a.active { border-color: var(--ib-accent); background: var(--ib-accent); color: var(--ib-accent-text); }
91
+ & .nav a.active:hover { color: var(--ib-accent-text); }
92
+
93
+ /* Tabs: one system for the whole family — an underlined row of equal-width
94
+ tabs, sized to sit at the top of the sidebar. */
95
+ & .tabs { display: flex; gap: 6px; }
96
+ & .tabs a { position: relative; display: flex; flex: 1 1 0; align-items: center; justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px; border-radius: 8px; color: var(--ib-muted); font-weight: 600; }
97
+ & .tabs a:hover { background: var(--ib-bg); color: var(--ib-text); text-decoration: none; }
98
+ & .tabs a.active { color: var(--ib-text); }
99
+ & .tabs a.active::after { content: ""; position: absolute; right: 20px; bottom: 0; left: 20px; height: 2px; border-radius: 999px; background: var(--ib-accent); }
100
+ & .count { display: inline-block; min-width: 20px; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, var(--ib-muted) 14%, transparent); font-size: 12px; text-align: center; font-variant-numeric: tabular-nums; }
101
+ & .tabs a.active .count { background: var(--ib-border); }
102
+
103
+ /* Filter row under the tabs: a full-width select over a search + submit row,
104
+ however many of the three a gem actually has. */
105
+ & .filters { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 12px; border-bottom: 1px solid var(--ib-border); }
106
+ & .filters label { flex: 1 1 100%; color: var(--ib-muted); font-size: 13px; }
107
+ & .filters select { flex: 1 1 100%; }
108
+ /* Basis 0, not auto: the controls below are `width: 100%`, which would
109
+ otherwise push the submit button onto its own row. */
110
+ & .filters input[type="search"] { flex: 1 1 0; min-width: 0; }
111
+ & .filters button,
112
+ & .filters input[type="submit"] { flex: 0 0 auto; }
113
+
114
+ /* Form controls. 16px so iOS does not zoom the page on focus. */
115
+ & input,
116
+ & select,
117
+ & textarea { width: 100%; min-height: 38px; padding: 7px 9px; border: 1px solid var(--ib-border); border-radius: 8px; background: var(--ib-surface); color: var(--ib-text); font: inherit; font-size: 16px; letter-spacing: 0; }
118
+ & textarea { resize: vertical; }
119
+ & input[type="checkbox"],
120
+ & input[type="radio"] { width: auto; min-width: 0; min-height: 0; }
121
+ & input:focus,
122
+ & select:focus,
123
+ & textarea:focus { border-color: var(--ib-accent); outline: 2px solid color-mix(in srgb, var(--ib-accent) 22%, transparent); outline-offset: 0; }
124
+ & .field { margin-bottom: 16px; }
125
+ & .field label { display: block; margin-bottom: 5px; font-weight: 600; }
126
+ & .field small { display: block; margin-top: 5px; color: var(--ib-muted); }
127
+
128
+ /* A submit input is a button, whatever the markup says. */
129
+ & button,
130
+ & .button,
131
+ & input[type="submit"] { display: inline-flex; width: auto; align-items: center; justify-content: center; min-height: 38px; padding: 8px 14px; border: 1px solid var(--ib-border); border-radius: 8px; background: var(--ib-surface); color: var(--ib-text); font: inherit; cursor: pointer; }
132
+ & button:hover,
133
+ & .button:hover,
134
+ & input[type="submit"]:hover { border-color: var(--ib-muted); text-decoration: none; }
135
+ & button.primary,
136
+ & .button.primary,
137
+ & input[type="submit"].primary { border-color: var(--ib-accent); background: var(--ib-accent); color: var(--ib-accent-text); }
138
+ & button.danger,
139
+ & .button.danger { color: var(--ib-danger); }
140
+ & button.small,
141
+ & .button.small { min-height: 32px; padding: 4px 10px; font-size: 13px; }
142
+ & .actions,
143
+ & .form-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
144
+ & .actions form { display: inline; }
145
+ & .form-actions { padding-top: 12px; }
146
+
147
+ & .card { overflow: hidden; border: 1px solid var(--ib-border); border-radius: 12px; background: var(--ib-surface); }
148
+ & .card.pad { padding: 16px; overflow: visible; }
97
149
  & table { width: 100%; border-collapse: collapse; }
98
150
  & th,
99
151
  & td { padding: 10px 14px; text-align: left; vertical-align: top; }
100
152
  & th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--ib-muted); border-bottom: 1px solid var(--ib-border); }
101
153
  & tr + tr td { border-top: 1px solid var(--ib-border); }
102
- & .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; }
154
+ & td.row-actions { white-space: nowrap; text-align: right; }
155
+ & td.row-actions form { display: inline-block; }
156
+ & td.row-actions form + form { margin-left: 6px; }
157
+
158
+ & .badge { display: inline-flex; padding: 2px 10px; border-radius: 999px; color: #fff; font-size: 12px; font-weight: 600; white-space: nowrap; }
159
+ & .badge.locale { border: 1px solid var(--ib-border); background: transparent; color: var(--ib-muted); }
160
+ & code.key { max-width: 100%; padding: 2px 7px; border-radius: 6px; background: var(--ib-code); font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
161
+
162
+ & .flash,
163
+ & .errors { margin-bottom: 16px; padding: 10px 12px; border: 1px solid var(--ib-border); border-radius: 8px; background: var(--ib-surface); }
164
+ & .flash.notice { border-color: var(--ib-success); }
165
+ & .flash.alert,
166
+ & .errors { border-color: var(--ib-danger); }
167
+ & .errors ul { margin-bottom: 0; }
168
+
169
+ & dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin: 0; }
170
+ & dt { color: var(--ib-muted); }
171
+ & dd { margin: 0; overflow-wrap: anywhere; }
172
+ & .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
173
+
174
+ & .empty { padding: 48px 16px; color: var(--ib-muted); text-align: center; }
175
+ & .empty-state { max-width: 320px; margin: auto; padding: 24px; text-align: center; }
176
+ & .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
177
+ & .empty-state p { margin: 0; }
178
+
179
+ /* The two-pane dashboard: the queue on the left, the selected record on the
180
+ right, each pane owning its own scroll. */
181
+ & .dashboard-shell { display: grid; flex: 1 1 auto; grid-template-columns: minmax(330px, 430px) 1fr; gap: 16px; min-height: 0; }
182
+ & .dashboard-sidebar { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; border: 1px solid var(--ib-border); border-radius: 8px; background: var(--ib-surface); }
183
+ & .dashboard-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; }
184
+ & .dashboard-sidebar .filters { flex: 0 0 auto; }
185
+
186
+ /* One row shape for every gem: a topline of badges and a timestamp, the copy
187
+ that identifies the record, then a muted line of ids and authors. */
188
+ & .record-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
189
+ & .record-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px; border-bottom: 1px solid var(--ib-border); color: var(--ib-text); }
190
+ & .record-row:hover { background: var(--ib-bg); text-decoration: none; }
191
+ & .record-row.active { background: color-mix(in srgb, var(--ib-accent) 9%, var(--ib-surface)); box-shadow: inset 3px 0 0 var(--ib-accent); }
192
+ & .record-main { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
193
+ & .record-topline,
194
+ & .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
195
+ & .record-name,
196
+ & .record-copy,
197
+ & .record-meta span,
198
+ /* A key is `overflow-wrap: anywhere` in prose; in a row it truncates. */
199
+ & .record-topline code.key,
200
+ & .record-meta code.key { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
201
+ & .record-name { font-weight: 600; }
202
+ & .record-copy { display: block; font-weight: 600; }
203
+ & .record-time { margin-left: auto; white-space: nowrap; }
204
+ & .record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
205
+ & .pager { display: flex; flex: 0 0 auto; justify-content: space-between; gap: 16px; min-height: 42px; margin: 0; padding: 10px 12px; border-top: 1px solid var(--ib-border); }
206
+
207
+ /* The selected record. `.detail-scroll` is the only part that scrolls, so the
208
+ panel heading and its actions stay put. */
209
+ & .dashboard-detail { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; }
210
+ & .dashboard-detail .detail-panel { flex: 1 1 auto; min-height: 0; }
211
+ & .dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
212
+ & .dashboard-detail .actions { margin-bottom: 0; }
213
+ & .detail-panel { display: flex; min-width: 0; flex-direction: column; }
214
+ /* The panel's own heading row: the title on the left, whatever acts on the
215
+ whole record on the right. */
216
+ & .panel-head { display: flex; flex: 0 0 auto; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
217
+ & .panel-head h1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0; }
218
+ & .detail-scroll { display: flex; min-height: 0; flex-direction: column; gap: 12px; }
219
+ & .mobile-back { display: none; }
220
+ }
221
+
222
+ /* Page frame — the gem's own layout only. After the components, so it wins the
223
+ specificity ties against them. */
224
+ .ib-page { margin: 0; min-height: 100vh; background: var(--ib-bg); color: var(--ib-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: 0; }
225
+ /* An index page is the dashboard: it fills the viewport and never scrolls
226
+ itself, because the queue and the detail pane each scroll on their own. */
227
+ .ib-index, .ib-index .container { height: 100vh; height: 100dvh; }
228
+ .ib-index { overflow: hidden; }
229
+ .ib-index .container { display: flex; max-width: 1280px; flex-direction: column; padding-bottom: 16px; }
230
+ .ib-index .nav,
231
+ .ib-index .page-head { flex: 0 0 auto; }
232
+ /* Standalone show pages keep normal page scrolling. */
233
+ .ib-show { min-height: 100vh; overflow: auto; }
234
+ .ib-show .detail-panel { width: 100%; }
235
+ .ib-show .detail-scroll { overflow: visible; }
236
+
237
+ /* Narrow screens: one pane at a time, the queue until a record is picked.
238
+ Last in the file, so these overrides actually reach the rules above. */
239
+ @media (max-width: 760px) {
240
+ .ib-page { font-size: 14px; }
241
+ .ib-index, .ib-show { overflow-x: hidden; }
242
+ .ib-index { overflow-y: auto; }
243
+ .ib-index, .ib-index .container { min-height: 100vh; height: auto; }
244
+ .ib-index .container { padding-bottom: 10px; }
245
+ .ib-dashboard {
246
+ & .container { padding: 14px 10px 24px; }
247
+ & .page-head { margin-bottom: 12px; }
248
+ & .detail-panel,
249
+ & .detail-scroll,
250
+ & .card,
251
+ & dl,
252
+ & dd { min-width: 0; max-width: 100%; }
253
+ & .dashboard-detail .card { width: 100%; }
254
+ & .message,
255
+ & .lede,
256
+ & dd,
257
+ & .muted { overflow-wrap: anywhere; }
258
+ & .dashboard-shell { display: block; min-height: calc(100vh - 62px); }
259
+ & .dashboard-sidebar,
260
+ & .dashboard-detail { min-height: calc(100vh - 62px); }
261
+ & .dashboard-shell.has-selected .dashboard-sidebar { display: none; }
262
+ & .dashboard-shell:not(.has-selected) .dashboard-detail { display: none; }
263
+ & .record-list { overflow: visible; }
264
+ & .record-row { padding: 12px; }
265
+ & .record-meta { display: none; }
266
+ & .dashboard-detail .detail-panel { min-height: calc(100vh - 64px); }
267
+ & .mobile-back { display: block; flex: 0 0 auto; margin: 4px 0 10px; font-size: 13px; }
268
+ & .panel-head h1 { font-size: 18px; }
269
+ & dl { grid-template-columns: 1fr; gap: 3px; }
270
+ & dd { margin-bottom: 8px; }
271
+ }
272
+ }
273
+
274
+ /* ---------------------------------------------------------------------------
275
+ * GEM-SPECIFIC — ideasbugs only: the kind and status badges, and the
276
+ * screenshots a reporter attached.
277
+ * ------------------------------------------------------------------------- */
278
+
279
+ :root {
280
+ --ib-bug: var(--ib-danger);
281
+ --ib-feature: var(--ib-success);
282
+ --ib-other: var(--ib-neutral);
283
+ --ib-open: var(--ib-accent);
284
+ --ib-in-review: var(--ib-warning);
285
+ --ib-resolved: var(--ib-success);
286
+ }
287
+
288
+ .ib-dashboard {
103
289
  & .badge.kind-bug { background: var(--ib-bug); }
104
290
  & .badge.kind-feature { background: var(--ib-feature); }
105
291
  & .badge.kind-other,
106
292
  & .badge.kind-default { background: var(--ib-other); }
107
293
  & .badge.status-open { background: var(--ib-open); }
108
- & .badge.status-in_review { background: var(--in_review); }
294
+ & .badge.status-in_review { background: var(--ib-in-review); }
109
295
  & .badge.status-resolved { background: var(--ib-resolved); }
110
- & .muted { color: var(--ib-muted); font-size: 13px; }
111
- & .pager { margin-top: 16px; display: flex; gap: 16px; }
112
- & .actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
113
- & .actions form { display: inline; }
114
- & button,
115
- & .button { padding: 8px 14px; border: 1px solid var(--ib-border); border-radius: 8px; cursor: pointer; background: var(--ib-surface); color: var(--ib-text); font: inherit; }
116
- & button.primary { background: var(--ib-accent); border-color: var(--ib-accent); color: var(--ib-accent-text); }
117
- & button.danger { color: var(--ib-bug); }
118
- & .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
119
- & dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
120
- & dt { color: var(--ib-muted); }
121
- & dd { margin: 0; overflow-wrap: anywhere; }
296
+
122
297
  & .shots { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; }
123
298
  & .shots img { max-width: 280px; max-height: 200px; border: 1px solid var(--ib-border); border-radius: 8px; }
124
- & .empty { padding: 48px 16px; text-align: center; color: var(--ib-muted); }
125
- & .breadcrumb { margin-bottom: 12px; font-size: 13px; }
126
- & .case-id { color: var(--ib-muted); font-weight: 400; font-size: 15px; }
127
- & .panel-head { flex: 0 0 auto; }
128
- & .panel-head h1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
129
- & .detail-scroll { display: flex; flex-direction: column; gap: 12px; min-height: 0; }
130
- & .feedback-panel { min-width: 0; display: flex; flex-direction: column; }
131
- & .feedback-panel .actions { flex: 0 0 auto; }
132
- & .feedback-panel .detail-scroll > .actions { margin: 0; }
133
- & .triage-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(320px, 400px) 1fr; gap: 16px; }
134
- & .triage-sidebar { min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--ib-surface); border: 1px solid var(--ib-border); border-radius: 8px; overflow: hidden; }
135
- & .triage-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent; border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
136
- & .triage-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px; border-radius: 8px; color: var(--ib-muted); font-weight: 600; }
137
- & .triage-sidebar .tabs a.active { color: var(--ib-text); border: 0; background: transparent; box-shadow: none; margin: 0; }
138
- & .triage-sidebar .tabs a.active::after { content: ""; position: absolute; left: 20px; right: 20px; bottom: 0; height: 2px; border-radius: 999px; background: var(--ib-accent); }
139
- & .triage-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--ib-text); background: var(--ib-bg); }
140
- & .triage-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--ib-muted) 14%, transparent); }
141
- & .triage-sidebar .tabs a.active .count { background: var(--ib-border); }
142
- & .triage-sidebar .filters { flex: 0 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) auto; margin: 0; padding: 12px; border-bottom: 1px solid var(--ib-border); }
143
- & .triage-sidebar .filters select { grid-column: 1 / -1; width: 100%; }
144
- & .triage-sidebar .filters input[type=search] { min-width: 0; max-width: none; }
145
- & .feedback-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
146
- & .feedback-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px; color: var(--ib-text); border-bottom: 1px solid var(--ib-border); }
147
- & .feedback-row:hover { text-decoration: none; background: var(--ib-bg); }
148
- & .feedback-row.active { background: color-mix(in srgb, var(--ib-accent) 9%, var(--ib-surface)); box-shadow: inset 3px 0 0 var(--ib-accent); }
149
- & .feedback-main { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
150
- & .feedback-topline,
151
- & .feedback-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
152
- & .feedback-message,
153
- & .feedback-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
154
- & .feedback-message { display: block; font-weight: 600; }
155
- & .feedback-time { margin-left: auto; white-space: nowrap; }
156
- & .attachment-count { align-self: start; white-space: nowrap; }
157
- & .triage-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--ib-border); }
158
- & .triage-detail { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
159
- & .triage-detail .feedback-panel { flex: 1 1 auto; min-height: 0; }
160
- & .triage-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
161
- & .triage-detail .actions { margin-bottom: 0; }
162
- & .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
163
- & .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
164
- & .empty-state p { margin: 0; }
165
- & .mobile-back { display: none; }
299
+ }
300
+
301
+ @media (max-width: 760px) {
302
+ .ib-dashboard {
303
+ & .shots img { max-width: 100%; }
304
+ }
166
305
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ideasbugs
4
- VERSION = '0.8.1'
4
+ VERSION = '0.9.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ideasbugs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov