rails-markup 1.4.7 → 1.5.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 +4 -4
- data/README.md +34 -7
- data/app/assets/javascripts/rails_markup/toolbar/10-styles.js +104 -62
- data/app/assets/javascripts/rails_markup/toolbar/20-dom.js +31 -17
- data/app/assets/javascripts/rails_markup/toolbar/40-events.js +71 -26
- data/app/assets/javascripts/rails_markup/toolbar/50-sync.js +2 -2
- data/app/assets/javascripts/rails_markup/toolbar/60-render.js +128 -69
- data/app/assets/javascripts/rails_markup/toolbar/90-init.js +13 -0
- data/app/views/layouts/rails_markup/application.html.erb +17 -123
- data/app/views/rails_markup/dashboard/_annotation_card.html.erb +4 -8
- data/app/views/rails_markup/dashboard/_annotation_list.html.erb +4 -0
- data/app/views/rails_markup/dashboard/_feedback_modal.html.erb +4 -0
- data/app/views/rails_markup/dashboard/_filters.html.erb +31 -22
- data/app/views/rails_markup/dashboard/_modal_navigation.html.erb +81 -0
- data/app/views/rails_markup/dashboard/_styles.html.erb +118 -36
- data/app/views/rails_markup/dashboard/_toolbar_preferences.html.erb +32 -0
- data/app/views/rails_markup/dashboard/board.html.erb +42 -55
- data/app/views/rails_markup/dashboard/index.html.erb +5 -23
- data/app/views/rails_markup/dashboard/show.html.erb +10 -8
- data/lib/rails_markup/configuration.rb +3 -4
- data/lib/rails_markup/version.rb +1 -1
- metadata +4 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
(() => {
|
|
3
|
+
const layout = document.querySelector('.rm-layout');
|
|
4
|
+
const dialog = layout.querySelector('.rm-feedback-modal');
|
|
5
|
+
const panel = dialog.querySelector('#detail-panel');
|
|
6
|
+
let opener;
|
|
7
|
+
let request;
|
|
8
|
+
let changed = false;
|
|
9
|
+
|
|
10
|
+
function renderDetail(html) {
|
|
11
|
+
const document = new DOMParser().parseFromString(html, 'text/html');
|
|
12
|
+
const detail = document.querySelector('turbo-frame#detail-panel');
|
|
13
|
+
if (!detail) throw new Error('Feedback unavailable');
|
|
14
|
+
panel.innerHTML = detail.innerHTML;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
layout.addEventListener('click', async event => {
|
|
18
|
+
const link = event.target.closest("a[data-turbo-frame='detail-panel']");
|
|
19
|
+
if (!link || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
request?.abort();
|
|
22
|
+
request = new AbortController();
|
|
23
|
+
const current = request;
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(link.href, {signal: current.signal});
|
|
26
|
+
if (!response.ok) throw new Error('Could not load feedback');
|
|
27
|
+
const html = await response.text();
|
|
28
|
+
if (current.signal.aborted) return;
|
|
29
|
+
renderDetail(html);
|
|
30
|
+
opener = link;
|
|
31
|
+
link.closest('.rm-card').classList.add('rm-card-active');
|
|
32
|
+
dialog.showModal();
|
|
33
|
+
panel.querySelector('.rm-back')?.focus();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.name !== 'AbortError') window.location.href = link.href;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
dialog.addEventListener('click', event => {
|
|
40
|
+
if (event.target !== dialog) return;
|
|
41
|
+
const rect = dialog.getBoundingClientRect();
|
|
42
|
+
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
dialog.addEventListener('close', () => {
|
|
46
|
+
request?.abort();
|
|
47
|
+
panel.innerHTML = '';
|
|
48
|
+
document.querySelectorAll('.rm-card-active').forEach(card => card.classList.remove('rm-card-active'));
|
|
49
|
+
opener?.focus({preventScroll: true});
|
|
50
|
+
// Refresh the current view and its filters after a saved change.
|
|
51
|
+
if (changed) window.location.reload();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
dialog.addEventListener('submit', async event => {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
event.stopPropagation();
|
|
57
|
+
const form = event.target;
|
|
58
|
+
const button = event.submitter;
|
|
59
|
+
if (button?.disabled) return;
|
|
60
|
+
if (button) button.disabled = true;
|
|
61
|
+
request?.abort();
|
|
62
|
+
request = new AbortController();
|
|
63
|
+
const current = request;
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(form.action, {method: form.method, body: new FormData(form), signal: current.signal});
|
|
66
|
+
if (!response.ok) throw new Error('Could not save feedback');
|
|
67
|
+
const html = await response.text();
|
|
68
|
+
if (current.signal.aborted || !dialog.open) return;
|
|
69
|
+
changed = true;
|
|
70
|
+
renderDetail(html);
|
|
71
|
+
panel.querySelector('.rm-back')?.focus();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.name === 'AbortError') return;
|
|
74
|
+
let alert = panel.querySelector('[role="alert"]');
|
|
75
|
+
if (!alert) { alert = document.createElement('p'); alert.setAttribute('role', 'alert'); form.prepend(alert); }
|
|
76
|
+
alert.textContent = 'Could not save. Please try again.';
|
|
77
|
+
if (button) button.disabled = false;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
})();
|
|
81
|
+
</script>
|
|
@@ -1,25 +1,43 @@
|
|
|
1
1
|
<style id="rm-dashboard-styles" data-turbo-permanent>
|
|
2
|
-
/*
|
|
3
|
-
.rm-
|
|
4
|
-
.rm-
|
|
5
|
-
.rm-
|
|
6
|
-
.rm-
|
|
7
|
-
|
|
8
|
-
.rm-
|
|
9
|
-
.rm-
|
|
2
|
+
/* Shared by the default layout and dashboards embedded in a host layout. */
|
|
3
|
+
.rm-workspace-heading { margin-bottom: 20px; }
|
|
4
|
+
.rm-workspace-heading h2 { margin: 0 0 8px; font-size: 22px; font-weight: 650; line-height: 1.2; letter-spacing: -.04em; color: #25272d; }
|
|
5
|
+
.rm-workspace-heading p { margin: 0; font-size: 12px; line-height: 1.6; color: #7a8494; max-width: 64ch; }
|
|
6
|
+
.rm-filter-tools { display: flex; align-items: center; gap: 8px; margin-left: auto; }
|
|
7
|
+
.rm-search-form { display: flex; flex: 1; min-width: 0; gap: 8px; align-items: center; }
|
|
8
|
+
.rm-search-form .rm-select { width: 100%; max-width: 360px; min-width: 0; cursor: text; }
|
|
9
|
+
.rm-filter-row { padding: 0 0 12px; border-bottom: 1px solid #dce1e8; }
|
|
10
|
+
.rm-status-tabs { gap: 4px; }
|
|
11
|
+
.rm-status-tabs > .rm-pill { border-color: transparent; background: transparent; }
|
|
12
|
+
.rm-status-tabs > .rm-pill-active { background: #eef0ff; color: #4338ca; border-color: #dce0ff; }
|
|
13
|
+
.rm-card-number { margin-left: auto; color: #596579; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
14
|
+
.rm-empty-title { margin: 0 0 8px; color: #25272d; font-size: 20px; font-weight: 600; letter-spacing: -.02em; }
|
|
15
|
+
.rm-empty-help { max-width: 42ch; margin: 10px auto 0; color: #596579; line-height: 1.7; }
|
|
16
|
+
.rm-pill:focus-visible, .rm-card-link:focus-visible, .rm-btn:focus-visible, .rm-select:focus-visible, .rm-back:focus-visible { outline: 3px solid #6366f1; outline-offset: 3px; }
|
|
17
|
+
.rm-pill, .rm-btn, .rm-select, .rm-input, .rm-back { font-family: inherit; box-sizing: border-box; }
|
|
18
|
+
.rm-detail, .rm-card, .rm-stat, .rm-board-column { box-sizing: border-box; }
|
|
19
|
+
.rm-card-content, .rm-detail-content, .rm-thread-entry, .rm-detail-meta dd { overflow-wrap: anywhere; min-width: 0; }
|
|
20
|
+
.rm-stats { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 0; margin-bottom: 24px; background: #fff; border: 1px solid #dce1e8; border-radius: 12px; overflow: hidden; }
|
|
21
|
+
.rm-stat { padding: 20px; min-width: 0; border-right: 1px solid #e5e7eb; }
|
|
22
|
+
.rm-stat:last-child { border-right: 0; }
|
|
23
|
+
.rm-stat-value { font-size: 27px; font-weight: 600; line-height: 1.2; font-variant-numeric: tabular-nums; letter-spacing: -.035em; }
|
|
24
|
+
.rm-stat-label { font-size: 12px; color: #6b7280; margin-top: 4px; letter-spacing: 0; }
|
|
25
|
+
|
|
26
|
+
.rm-filters { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
|
|
27
|
+
.rm-pill { display: inline-flex; align-items: center; min-height: 32px; padding: 5px 10px; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; border: 1px solid #e5e7eb; background: #fff; color: #6b7280; transition: all 0.15s; text-decoration: none; }
|
|
10
28
|
.rm-pill:hover { border-color: #818cf8; color: #4361ee; text-decoration: none; }
|
|
11
29
|
.rm-pill-active { background: #4361ee; color: #fff; border-color: #4361ee; }
|
|
12
30
|
.rm-pill-active:hover { background: #3730a3; color: #fff; }
|
|
13
|
-
.rm-select { padding:
|
|
31
|
+
.rm-select { min-height: 32px; padding: 5px 10px; border-radius: 8px; border: 1px solid #e5e7eb; font-size: 13px; background: #fff; color: #374151; cursor: pointer; max-width: 240px; }
|
|
14
32
|
|
|
15
|
-
.rm-card { background:
|
|
16
|
-
.rm-card:hover {
|
|
33
|
+
.rm-card { background: transparent; border:0; border-bottom:1px solid #e8ebef; border-radius:0; padding:14px 10px; margin-bottom:0; transition: border-color 0.15s; position: relative; }
|
|
34
|
+
.rm-card:hover { background:#f2f4f8; }
|
|
17
35
|
.rm-card-link { display: block; color: inherit; }
|
|
18
36
|
.rm-card-link:hover { text-decoration: none; }
|
|
19
37
|
.rm-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
20
38
|
.rm-card-content { font-size: 14px; color: #374151; line-height: 1.6; }
|
|
21
|
-
.rm-card-meta { display: flex; align-items: center; gap: 12px; margin-top:
|
|
22
|
-
.rm-card-meta code { font-size: 11px; background:
|
|
39
|
+
.rm-card-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin-top: 6px; font-size: 11px; color: #596579; }
|
|
40
|
+
.rm-card-meta code { font-size: 11px; background:transparent; padding:0; border-radius: 4px; color: #6b7280; }
|
|
23
41
|
|
|
24
42
|
.rm-badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; text-transform: capitalize; }
|
|
25
43
|
.rm-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
|
@@ -39,10 +57,10 @@
|
|
|
39
57
|
.rm-severity-important { background: #fff7ed; color: #9a3412; }
|
|
40
58
|
.rm-severity-suggestion { background: #f3f4f6; color: #6b7280; }
|
|
41
59
|
|
|
42
|
-
.rm-detail { background: #fff; border: 1px solid #e5e7eb; border-radius:
|
|
60
|
+
.rm-detail { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; }
|
|
43
61
|
.rm-detail-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
44
|
-
.rm-detail-content { font-size: 15px; line-height: 1.
|
|
45
|
-
.rm-detail-meta { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; font-size: 13px; margin-bottom: 24px; padding: 16px; background: #f9fafb; border-radius: 8px; }
|
|
62
|
+
.rm-detail-content { font-size: 15px; line-height: 1.65; margin-bottom: 16px; white-space: pre-wrap; }
|
|
63
|
+
.rm-detail-meta { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px 16px; font-size: 13px; margin-bottom: 24px; padding: 16px; background: #f9fafb; border-radius: 8px; }
|
|
46
64
|
.rm-detail-meta dt { color: #6b7280; font-weight: 500; }
|
|
47
65
|
.rm-detail-meta dd { color: #374151; }
|
|
48
66
|
.rm-detail-meta dd code { font-size: 12px; background: #e5e7eb; padding: 1px 5px; border-radius: 3px; }
|
|
@@ -52,17 +70,17 @@
|
|
|
52
70
|
.rm-thread-entry { padding: 12px 16px; border-radius: 8px; margin-bottom: 8px; font-size: 13px; line-height: 1.5; }
|
|
53
71
|
.rm-thread-agent { background: #eff6ff; border-left: 3px solid #3b82f6; }
|
|
54
72
|
.rm-thread-user { background: #f9fafb; border-left: 3px solid #e5e7eb; }
|
|
55
|
-
.rm-thread-role { font-weight: 600; font-size: 11px;
|
|
56
|
-
.rm-thread-time { font-size:
|
|
73
|
+
.rm-thread-role { font-weight: 600; font-size: 11px; letter-spacing: 0; margin-bottom: 4px; }
|
|
74
|
+
.rm-thread-time { font-size: 12px; color: #596579; margin-top: 4px; }
|
|
57
75
|
|
|
58
76
|
.rm-actions { display: flex; gap: 8px; margin-top: 24px; flex-wrap: wrap; }
|
|
59
|
-
.rm-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius:
|
|
77
|
+
.rm-btn { display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 8px 16px; border-radius: 6px; font-size: 12px; font-weight: 500; border: 1px solid #e5e7eb; background: #fff; color: #374151; cursor: pointer; transition: all 0.15s; }
|
|
60
78
|
.rm-btn:hover { border-color: #818cf8; color: #4361ee; text-decoration: none; }
|
|
61
79
|
.rm-btn-primary { background: #4361ee; color: #fff; border-color: #4361ee; }
|
|
62
80
|
.rm-btn-primary:hover { background: #3730a3; color: #fff; }
|
|
63
|
-
.rm-btn-success { background: #
|
|
81
|
+
.rm-btn-success { background: #047857; color: #fff; border-color: #047857; }
|
|
64
82
|
.rm-btn-success:hover { background: #059669; color: #fff; }
|
|
65
|
-
.rm-btn-danger { background: #
|
|
83
|
+
.rm-btn-danger { background: #b91c1c; color: #fff; border-color: #b91c1c; }
|
|
66
84
|
.rm-btn-danger:hover { background: #dc2626; color: #fff; }
|
|
67
85
|
|
|
68
86
|
.rm-form-group { margin-bottom: 12px; }
|
|
@@ -73,16 +91,20 @@
|
|
|
73
91
|
.rm-back { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: #6b7280; margin-bottom: 16px; border: none; background: none; cursor: pointer; }
|
|
74
92
|
.rm-back:hover { color: #4361ee; text-decoration: none; }
|
|
75
93
|
|
|
76
|
-
.rm-empty { text-align: center; padding:
|
|
77
|
-
.rm-empty-icon {
|
|
94
|
+
.rm-empty { text-align: center; padding: 56px 24px; color: #596579; border: 1px dashed #cbd2df; border-radius: 12px; background: #fff; }
|
|
95
|
+
.rm-empty-icon { display: grid; place-items: center; width: 48px; height: 48px; margin: 0 auto 18px; border-radius: 14px; background: #eef0ff; color: #4f46e5; font-size: 26px; }
|
|
78
96
|
.rm-empty-text { font-size: 14px; }
|
|
79
97
|
|
|
80
|
-
.rm-layout { display: flex; gap:
|
|
81
|
-
.rm-main { flex: 1; min-width: 0; }
|
|
82
|
-
.rm-panel { width: 420px; flex-shrink: 0; position: sticky; top: 80px; max-height: calc(100vh - 100px); overflow-y: auto; }
|
|
83
|
-
.rm-panel .rm-detail { border-left: 3px solid #818cf8; }
|
|
98
|
+
.rm-layout { display: flex; align-items: flex-start; gap: 20px; }
|
|
99
|
+
.rm-main { flex: 1; min-width: 0; width:100%; }
|
|
84
100
|
.rm-card-active { border-color: #818cf8 !important; background: #f5f3ff; }
|
|
85
|
-
.rm-
|
|
101
|
+
.rm-feedback-modal { width:min(620px, calc(100vw - 32px)); max-height:calc(100dvh - 32px); padding:0; border:1px solid #dce1e8; border-radius:16px; background:#fff; box-shadow:0 24px 80px #18213830; }
|
|
102
|
+
.rm-feedback-modal::backdrop { background:#18213866; backdrop-filter:blur(3px); }
|
|
103
|
+
.rm-modal-frame { display:block; }
|
|
104
|
+
.rm-feedback-modal .rm-detail { border:0; margin:0; border-radius:0; }
|
|
105
|
+
body:has(.rm-feedback-modal[open]) { overflow:hidden; }
|
|
106
|
+
.rm-board-help summary { padding:4px 8px; font-size:18px; }
|
|
107
|
+
.rm-board-help summary::after { display:none; }
|
|
86
108
|
|
|
87
109
|
.rm-author { font-weight: 500; color: #6b7280; }
|
|
88
110
|
|
|
@@ -91,25 +113,85 @@
|
|
|
91
113
|
.rm-load-more-btn:hover { border-color: #818cf8; color: #4361ee; background: #f5f3ff; }
|
|
92
114
|
.rm-load-more-btn:disabled { opacity: 0.6; cursor: wait; }
|
|
93
115
|
|
|
94
|
-
.rm-board { display: grid; grid-template-columns: repeat(4, 1fr); gap:
|
|
95
|
-
.rm-board-column { background: #
|
|
96
|
-
.rm-board-column-header { font-size:
|
|
116
|
+
.rm-board { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; min-height: 300px; }
|
|
117
|
+
.rm-board-column { background: #f2f4f7; border:1px solid #e8ebef; border-radius: 10px; padding: 8px; min-width:0; min-height: 160px; }
|
|
118
|
+
.rm-board-column-header { font-size: 12px; font-weight: 600; padding: 6px 4px 12px; display: flex; justify-content: space-between; align-items: center; gap:6px; }
|
|
119
|
+
.rm-board-column-header > span:first-child { display:flex; align-items:center; gap:6px; }
|
|
97
120
|
.rm-board-cards { display: flex; flex-direction: column; gap: 8px; min-height: 100px; }
|
|
98
121
|
.rm-board-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; cursor: grab; font-size: 13px; transition: box-shadow 0.15s; }
|
|
99
122
|
.rm-board-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
|
100
123
|
.rm-board-card:active { cursor: grabbing; }
|
|
124
|
+
.rm-board-card .rm-card { padding:0; border:0; background:transparent; }
|
|
125
|
+
.rm-board-card .rm-card-content { font-size:13px; line-height:1.5; }
|
|
126
|
+
.rm-board-card .rm-card-meta { gap:4px 8px; }
|
|
127
|
+
.rm-board-card:has(.rm-card-active) { border-color:#818cf8; background:#f5f3ff; }
|
|
128
|
+
.rm-board-card-actions { margin-top:8px; font-size:11px; color:#596579; }
|
|
129
|
+
.rm-board-card-actions summary { cursor:pointer; width:fit-content; }
|
|
130
|
+
.rm-board-card-actions:not([open]) > select { display:none; }
|
|
131
|
+
.rm-board-card-actions .rm-select { width:100%; margin-top:6px; font-size:12px; }
|
|
132
|
+
.rm-board-empty { text-align:center; padding:24px 8px; font-size:12px; color:#798494; }
|
|
133
|
+
.rm-board-toolbar { justify-content:space-between; }
|
|
134
|
+
.rm-board-toolbar .rm-filter-tools { margin-left:0; }
|
|
101
135
|
.rm-drag-over { background: #eff6ff; border: 2px dashed #818cf8; }
|
|
102
136
|
|
|
103
137
|
@media (max-width: 900px) {
|
|
104
138
|
.rm-layout { flex-direction: column; }
|
|
105
|
-
.rm-panel { width: 100%; position: static; max-height: none; }
|
|
106
139
|
.rm-board { grid-template-columns: repeat(2, 1fr); }
|
|
107
140
|
}
|
|
108
141
|
@media (max-width: 640px) {
|
|
109
|
-
.rm-stats {
|
|
110
|
-
.rm-stat {
|
|
111
|
-
.rm-
|
|
112
|
-
.rm-
|
|
142
|
+
.rm-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
143
|
+
.rm-stat { padding: 16px; border-bottom: 1px solid #e5e7eb; }
|
|
144
|
+
.rm-stat:last-child { grid-column: 1 / -1; border-bottom: 0; }
|
|
145
|
+
.rm-filters { gap: 8px; }
|
|
146
|
+
.rm-filter-row { align-items: stretch; }
|
|
147
|
+
.rm-filter-row .rm-search-form { flex-basis: 100%; }
|
|
148
|
+
.rm-filter-row .rm-select { max-width: 100%; }
|
|
149
|
+
.rm-filter-tools { margin-left: 0; flex-wrap: wrap; }
|
|
150
|
+
.rm-status-tabs .rm-filter-tools { flex-basis: 100%; margin-top: 8px; }
|
|
151
|
+
.rm-status-tabs > form { flex-basis: 100%; }
|
|
152
|
+
.rm-detail { padding: 20px; }
|
|
153
|
+
.rm-card { padding: 12px 4px; }
|
|
154
|
+
.rm-detail-header { gap: 8px; }
|
|
113
155
|
.rm-board { grid-template-columns: 1fr; }
|
|
114
156
|
}
|
|
157
|
+
@media (prefers-reduced-motion: reduce) { .rm-pill, .rm-card, .rm-btn, .rm-board-card { transition: none; } }
|
|
158
|
+
|
|
159
|
+
.rm-toolbar-preferences { display:flex;align-items:center;justify-content:flex-end;gap:12px;flex-wrap:wrap;margin-bottom:20px;font-size:12px;color:#596579; }
|
|
160
|
+
.rm-toolbar-visibility { display:flex;align-items:center;gap:12px;border:1px solid #dce1e8;border-radius:10px;padding:10px 14px;background:#fff;color:#253247;cursor:pointer;font:inherit;font-size:13px; }
|
|
161
|
+
.rm-toolbar-visibility-track { width:32px;height:18px;border-radius:20px;background:#94a3b8;padding:3px; }
|
|
162
|
+
.rm-toolbar-visibility-track::after { content:"";display:block;width:12px;height:12px;border-radius:50%;background:#fff; }
|
|
163
|
+
.rm-toolbar-visibility[aria-checked="true"] .rm-toolbar-visibility-track { background:#4f46e5; }
|
|
164
|
+
.rm-toolbar-visibility[aria-checked="true"] .rm-toolbar-visibility-track::after { transform:translateX(14px); }
|
|
165
|
+
|
|
166
|
+
.rm-heading-count { font-size:12px;font-weight:500;letter-spacing:0;color:#7a8494;vertical-align:middle;margin-left:6px; }
|
|
167
|
+
.rm-tab-count { margin-left:6px;font-size:11px;color:#8590a0;font-variant-numeric:tabular-nums; }
|
|
168
|
+
.rm-status-tabs { border-bottom:1px solid #e8ebef;padding-bottom:8px; }
|
|
169
|
+
.rm-status-tabs > .rm-pill-active { background:#eef0ff;border-color:transparent; }
|
|
170
|
+
.rm-status-tabs .rm-filter-tools .rm-pill { background:transparent;border-color:transparent;color:#7a8494; }
|
|
171
|
+
.rm-status-tabs .rm-filter-tools .rm-pill-active { background:#e9edf3;color:#253247; }
|
|
172
|
+
.rm-row-status { display:inline-flex;align-items:center;gap:5px; }
|
|
173
|
+
.rm-row-status .rm-dot { width:5px;height:5px; }
|
|
174
|
+
.rm-options { position:relative;flex-shrink:0; }
|
|
175
|
+
.rm-options > summary, .rm-context > summary { cursor:pointer;list-style:none;font-size:12px;color:#64748b;padding:7px 10px;border-radius:6px; }
|
|
176
|
+
.rm-options > summary::-webkit-details-marker, .rm-context > summary::-webkit-details-marker { display:none; }
|
|
177
|
+
.rm-options > summary::after, .rm-context > summary::after { content:"⌄";margin-left:8px; }
|
|
178
|
+
.rm-options[open] > summary { background:#e9edf3;color:#253247; }
|
|
179
|
+
.rm-options:not([open]) > .rm-options-content, .rm-context:not([open]) > :not(summary) { display:none; }
|
|
180
|
+
.rm-options-content { position:absolute;right:0;top:calc(100% + 8px);z-index:20;width:300px;max-width:calc(100vw - 40px);padding:16px;display:flex;flex-wrap:wrap;gap:10px;border:1px solid #e2e6ed;border-radius:10px;background:#fff;box-shadow:0 8px 28px #25324714; }
|
|
181
|
+
.rm-board-toolbar > .rm-filter-tools { position:relative; }
|
|
182
|
+
.rm-board-help { position:static; }
|
|
183
|
+
.rm-board-help .rm-options-content { left:0;right:auto;width:280px;font-size:12px;line-height:1.6; }
|
|
184
|
+
.rm-options-content .rm-select { width:100%;max-width:none; }
|
|
185
|
+
.rm-options-content .rm-filter-tools { margin-left:0; }
|
|
186
|
+
.rm-options-heading { width:100%;font-size:11px;font-weight:600;color:#7a8494;margin-top:6px; }
|
|
187
|
+
.rm-options-content .rm-toolbar-preferences { margin:0;justify-content:flex-start;font-size:11px;gap:8px; }
|
|
188
|
+
.rm-options-content .rm-toolbar-preferences [role="status"] { order:1; }
|
|
189
|
+
.rm-options-content .rm-toolbar-visibility { padding:7px 10px;font-size:12px; }
|
|
190
|
+
.rm-context { border-top:1px solid #edf0f4;margin:12px 0; }
|
|
191
|
+
.rm-context > summary { padding:10px 0; }
|
|
192
|
+
.rm-context .rm-detail-meta { margin:0;padding:12px;font-size:12px; }
|
|
193
|
+
.rm-detail-meta dd { margin:0; }
|
|
194
|
+
.rm-board-options { float:right; }
|
|
195
|
+
.rm-attachment { display:block;max-width:100%;border:1px solid #e5e7eb;border-radius:6px;margin:12px 0; }
|
|
196
|
+
@media(max-width:640px) { .rm-status-tabs { gap:0; }.rm-status-tabs > .rm-pill { font-size:11px;padding:5px 7px; }.rm-status-tabs .rm-filter-tools { flex-basis:auto;margin-left:auto;margin-top:4px; }.rm-options-content { width:280px; }.rm-filter-row .rm-search-form { flex-basis:0; }.rm-toolbar-preferences { justify-content:flex-start; } }
|
|
115
197
|
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<div class="rm-toolbar-preferences">
|
|
2
|
+
<span id="rm-launcher-save-status" role="status">Visible on the app · this browser</span>
|
|
3
|
+
<button type="button" class="rm-toolbar-visibility" id="rm-launcher-visibility" role="switch" aria-label="Show floating toolbar" aria-checked="<%= RailsMarkup.config.fab_visible %>"><span>Floating toolbar</span><span class="rm-toolbar-visibility-track" aria-hidden="true"></span></button>
|
|
4
|
+
</div>
|
|
5
|
+
<script>
|
|
6
|
+
(() => {
|
|
7
|
+
const button = document.getElementById("rm-launcher-visibility");
|
|
8
|
+
const status = document.getElementById("rm-launcher-save-status");
|
|
9
|
+
const endpoint = <%= raw((rails_markup.root_path.sub(%r{/+$}, "") + "/api").to_json) %>;
|
|
10
|
+
const key = `rm-toolbar-settings:${encodeURIComponent(endpoint)}`;
|
|
11
|
+
const defaultVisible = <%= RailsMarkup.config.fab_visible ? "true" : "false" %>;
|
|
12
|
+
const read = () => {
|
|
13
|
+
const value = JSON.parse(localStorage.getItem(key) || "{}");
|
|
14
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
15
|
+
};
|
|
16
|
+
const render = settings => {
|
|
17
|
+
const visible = typeof settings.fabVisible === "boolean" ? settings.fabVisible : defaultVisible;
|
|
18
|
+
button.setAttribute("aria-checked", String(visible));
|
|
19
|
+
status.textContent = `${visible ? "Visible" : "Hidden"} on the app · this browser`;
|
|
20
|
+
};
|
|
21
|
+
try { render(read()); } catch (_) { status.textContent = "Browser preferences unavailable"; }
|
|
22
|
+
button.addEventListener("click", () => {
|
|
23
|
+
try {
|
|
24
|
+
const settings = read();
|
|
25
|
+
settings.fabVisible = button.getAttribute("aria-checked") !== "true";
|
|
26
|
+
localStorage.setItem(key, JSON.stringify(settings));
|
|
27
|
+
render(settings);
|
|
28
|
+
|
|
29
|
+
} catch (_) { status.textContent = "Could not save preference"; }
|
|
30
|
+
});
|
|
31
|
+
})();
|
|
32
|
+
</script>
|
|
@@ -1,65 +1,50 @@
|
|
|
1
1
|
<% content_for(:title) { "Board" } %>
|
|
2
|
-
<% content_for(:page_title) { "Annotations Board" } %>
|
|
3
|
-
|
|
4
2
|
<%= render "styles" %>
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
<div class="rm-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
<div class="rm-filters rm-board-toolbar">
|
|
5
|
+
<div class="rm-filter-tools" role="group" aria-label="View">
|
|
6
|
+
<a href="<%= rails_markup.root_path(status: 'all') %>" class="rm-pill">List</a>
|
|
7
|
+
<a href="<%= rails_markup.board_path %>" class="rm-pill rm-pill-active" aria-current="page">Board</a>
|
|
8
|
+
<details class="rm-options rm-board-help"><summary aria-label="About the board"><span aria-hidden="true">ⓘ</span></summary><div class="rm-options-content">Open a card to review. Drag or use Move to change its status.</div></details>
|
|
9
|
+
</div>
|
|
10
|
+
<details class="rm-options"><summary>Options</summary><div class="rm-options-content">
|
|
11
|
+
<div class="rm-options-heading">Toolbar</div>
|
|
12
|
+
<%= render "toolbar_preferences" %>
|
|
13
|
+
</div></details>
|
|
10
14
|
</div>
|
|
11
15
|
|
|
12
|
-
<div class="rm-board">
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<% column_config.each do |status, config| %>
|
|
21
|
-
<div class="rm-board-column" data-status="<%= status %>"
|
|
22
|
-
style="border-top: 3px solid <%= config[:color] %>;">
|
|
23
|
-
<div class="rm-board-column-header" style="background: <%= config[:bg] %>; color: <%= config[:color] %>;">
|
|
24
|
-
<span><%= config[:label] %></span>
|
|
25
|
-
<span style="font-size:12px;font-weight:400;"><%= @columns[status].size %></span>
|
|
26
|
-
</div>
|
|
27
|
-
<div class="rm-board-cards">
|
|
28
|
-
<% @columns[status].each do |annotation| %>
|
|
29
|
-
<div class="rm-board-card" draggable="true" data-annotation-id="<%= annotation.id %>">
|
|
30
|
-
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
|
|
31
|
-
<span class="rm-badge rm-intent-<%= annotation.intent %>" style="font-size:10px;"><%= annotation.intent %></span>
|
|
32
|
-
<% if annotation.severity != "suggestion" %>
|
|
33
|
-
<span class="rm-badge rm-severity-<%= annotation.severity %>" style="font-size:10px;"><%= annotation.severity %></span>
|
|
34
|
-
<% end %>
|
|
35
|
-
<% if annotation.author_name.present? %>
|
|
36
|
-
<span style="font-size:10px;color:#9ca3af;margin-left:auto;"><%= annotation.author_name %></span>
|
|
37
|
-
<% end %>
|
|
38
|
-
</div>
|
|
39
|
-
<div style="font-size:13px;line-height:1.5;color:#374151;"><%= truncate(annotation.content, length: 120) %></div>
|
|
40
|
-
<div style="margin-top:6px;font-size:10px;color:#d1d5db;">
|
|
41
|
-
<code style="font-size:10px;"><%= truncate(annotation.page_url, length: 30) %></code>
|
|
42
|
-
·
|
|
43
|
-
<span title="<%= annotation.created_at.strftime("%B %d, %Y at %H:%M") %>"><%= time_ago_in_words(annotation.created_at) %></span>
|
|
44
|
-
</div>
|
|
45
|
-
<%# Touch fallback for drag-and-drop: pick a status directly %>
|
|
46
|
-
<select class="rm-board-move" data-annotation-id="<%= annotation.id %>"
|
|
47
|
-
aria-label="Move annotation to status"
|
|
48
|
-
style="margin-top:8px;width:100%;font-size:11px;padding:4px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#6b7280;">
|
|
49
|
-
<% %w[pending acknowledged resolved dismissed].each do |s| %>
|
|
50
|
-
<option value="<%= s %>" <%= "selected" if annotation.status == s %>><%= s.capitalize %></option>
|
|
51
|
-
<% end %>
|
|
52
|
-
</select>
|
|
16
|
+
<div class="rm-layout rm-board-layout">
|
|
17
|
+
<div class="rm-main">
|
|
18
|
+
<div class="rm-board">
|
|
19
|
+
<% {pending: "Pending", acknowledged: "Acknowledged", resolved: "Resolved", dismissed: "Dismissed"}.each do |status, label| %>
|
|
20
|
+
<section class="rm-board-column" data-status="<%= status %>" aria-label="<%= label %> feedback">
|
|
21
|
+
<div class="rm-board-column-header">
|
|
22
|
+
<span><span class="rm-dot rm-dot-<%= status %>"></span><%= label %></span>
|
|
23
|
+
<span class="rm-tab-count"><%= @columns[status].size %></span>
|
|
53
24
|
</div>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
25
|
+
<div class="rm-board-cards">
|
|
26
|
+
<% @columns[status].each do |annotation| %>
|
|
27
|
+
<div class="rm-board-card" draggable="true" data-annotation-id="<%= annotation.id %>">
|
|
28
|
+
<%= render "annotation_card", annotation: annotation, board: true %>
|
|
29
|
+
<details class="rm-board-card-actions"><summary>Move</summary>
|
|
30
|
+
<select class="rm-board-move rm-select" data-annotation-id="<%= annotation.id %>" aria-label="Move annotation to status">
|
|
31
|
+
<% %w[pending acknowledged resolved dismissed].each do |value| %>
|
|
32
|
+
<option value="<%= value %>" <%= "selected" if annotation.status == value %>><%= value.capitalize %></option>
|
|
33
|
+
<% end %>
|
|
34
|
+
</select>
|
|
35
|
+
</details>
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
38
|
+
<div class="rm-board-empty" <%= 'hidden' if @columns[status].any? %>>No feedback</div>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
<% end %>
|
|
59
42
|
</div>
|
|
60
|
-
|
|
43
|
+
</div>
|
|
44
|
+
<%= render "feedback_modal" %>
|
|
61
45
|
</div>
|
|
62
46
|
|
|
47
|
+
|
|
63
48
|
<script>
|
|
64
49
|
(function() {
|
|
65
50
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
@@ -98,7 +83,7 @@
|
|
|
98
83
|
});
|
|
99
84
|
|
|
100
85
|
function moveCard(id, newStatus) {
|
|
101
|
-
const card = document.querySelector('[data-annotation-id="' + id + '"]');
|
|
86
|
+
const card = document.querySelector('.rm-board-card[data-annotation-id="' + id + '"]');
|
|
102
87
|
if (!card || card.closest('[data-status]').dataset.status === newStatus) return;
|
|
103
88
|
|
|
104
89
|
// Optimistically move the card to the target column, then reconcile.
|
|
@@ -127,8 +112,10 @@
|
|
|
127
112
|
function updateColumnCounts() {
|
|
128
113
|
document.querySelectorAll('.rm-board-column').forEach(col => {
|
|
129
114
|
const count = col.querySelectorAll('.rm-board-card').length;
|
|
130
|
-
const countEl = col.querySelector('.rm-board-column-header
|
|
115
|
+
const countEl = col.querySelector('.rm-board-column-header > .rm-tab-count');
|
|
131
116
|
if (countEl) countEl.textContent = count;
|
|
117
|
+
const empty = col.querySelector('.rm-board-empty');
|
|
118
|
+
if (empty) empty.hidden = count > 0;
|
|
132
119
|
});
|
|
133
120
|
}
|
|
134
121
|
})();
|
|
@@ -3,36 +3,18 @@
|
|
|
3
3
|
|
|
4
4
|
<%= render "styles" %>
|
|
5
5
|
|
|
6
|
+
<div class="rm-workspace-heading">
|
|
7
|
+
<h2>Feedback <span class="rm-heading-count"><%= @total_count %></span></h2>
|
|
8
|
+
<p><%= @pending_count %> pending<% if @acknowledged_count.positive? %> · <%= @acknowledged_count %> in progress<% end %></p>
|
|
9
|
+
</div>
|
|
6
10
|
|
|
7
11
|
<div class="rm-layout">
|
|
8
12
|
<div class="rm-main">
|
|
9
13
|
<turbo-frame id="annotations-content">
|
|
10
|
-
<%= render "stats" %>
|
|
11
14
|
<%= render "filters" %>
|
|
12
15
|
<%= render "annotation_list" %>
|
|
13
16
|
</turbo-frame>
|
|
14
17
|
</div>
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
</turbo-frame>
|
|
19
|
+
<%= render "feedback_modal" %>
|
|
18
20
|
</div>
|
|
19
|
-
|
|
20
|
-
<script>
|
|
21
|
-
document.addEventListener("turbo:frame-load", (e) => {
|
|
22
|
-
if (e.target.id !== "detail-panel") return;
|
|
23
|
-
// Show panel (hidden when empty)
|
|
24
|
-
e.target.classList.remove("rm-panel-empty");
|
|
25
|
-
// Highlight active card
|
|
26
|
-
document.querySelectorAll(".rm-card-active").forEach(c => c.classList.remove("rm-card-active"));
|
|
27
|
-
const src = e.target.getAttribute("src") || "";
|
|
28
|
-
const match = src.match(/annotations\/(\d+)/);
|
|
29
|
-
if (match) {
|
|
30
|
-
const card = document.querySelector(`[data-annotation-id="${match[1]}"]`);
|
|
31
|
-
if (card) card.classList.add("rm-card-active");
|
|
32
|
-
}
|
|
33
|
-
// Scroll to panel on mobile
|
|
34
|
-
if (window.innerWidth <= 900) {
|
|
35
|
-
e.target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
</script>
|
|
@@ -5,20 +5,21 @@
|
|
|
5
5
|
<turbo-frame id="detail-panel">
|
|
6
6
|
<div class="rm-detail">
|
|
7
7
|
<%# Close button %>
|
|
8
|
-
<button type="button" class="rm-back" onclick="var
|
|
8
|
+
<button type="button" class="rm-back" onclick="var dialog=this.closest('dialog'); if(dialog) { dialog.close(); return; } window.location.href='<%= j(rails_markup.root_path) %>';">✕ Close</button>
|
|
9
9
|
|
|
10
10
|
<%# Header badges %>
|
|
11
11
|
<div class="rm-detail-header">
|
|
12
12
|
<span class="rm-dot rm-dot-<%= @annotation.status %>"></span>
|
|
13
13
|
<span class="rm-badge rm-status-<%= @annotation.status %>"><%= @annotation.status %></span>
|
|
14
14
|
<span class="rm-badge rm-intent-<%= @annotation.intent %>"><%= @annotation.intent %></span>
|
|
15
|
-
|
|
15
|
+
<% if @annotation.severity != "suggestion" %><span class="rm-badge rm-severity-<%= @annotation.severity %>"><%= @annotation.severity %></span><% end %>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
<%# Content %>
|
|
19
19
|
<div class="rm-detail-content"><%= @annotation.content %></div>
|
|
20
20
|
|
|
21
21
|
<%# Metadata %>
|
|
22
|
+
<details class="rm-context"><summary>Context & details</summary>
|
|
22
23
|
<dl class="rm-detail-meta">
|
|
23
24
|
<dt>Page</dt>
|
|
24
25
|
<dd><code><%= @annotation.page_url %></code></dd>
|
|
@@ -51,11 +52,11 @@
|
|
|
51
52
|
<dd><span title="<%= @annotation.updated_at.strftime("%B %d, %Y at %H:%M:%S") %>"><%= time_ago_in_words(@annotation.updated_at) %> ago</span></dd>
|
|
52
53
|
<% end %>
|
|
53
54
|
|
|
54
|
-
<% if @annotation.metadata&.dig("screenshot").present? %>
|
|
55
|
-
<dt>Screenshot</dt>
|
|
56
|
-
<dd><img src="<%= @annotation.metadata["screenshot"] %>" style="max-width:100%;border:1px solid #e5e7eb;border-radius:8px;" alt="Element screenshot"></dd>
|
|
57
|
-
<% end %>
|
|
58
55
|
</dl>
|
|
56
|
+
</details>
|
|
57
|
+
<% if @annotation.metadata&.dig("screenshot").present? %>
|
|
58
|
+
<img src="<%= @annotation.metadata["screenshot"] %>" class="rm-attachment" alt="Element screenshot">
|
|
59
|
+
<% end %>
|
|
59
60
|
|
|
60
61
|
<%# Actions (only for non-terminal states) %>
|
|
61
62
|
<% if @annotation.status.in?(%w[pending acknowledged]) %>
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
<% end %>
|
|
68
69
|
|
|
69
70
|
<button type="button" class="rm-btn rm-btn-success" onclick="document.getElementById('resolve-form-<%= @annotation.id %>').style.display='block'">Resolve</button>
|
|
70
|
-
<button type="button" class="rm-btn
|
|
71
|
+
<button type="button" class="rm-btn" onclick="document.getElementById('dismiss-form-<%= @annotation.id %>').style.display='block'">Dismiss</button>
|
|
71
72
|
</div>
|
|
72
73
|
|
|
73
74
|
<%# Resolve form %>
|
|
@@ -110,8 +111,8 @@
|
|
|
110
111
|
<% end %>
|
|
111
112
|
|
|
112
113
|
<%# Reply form %>
|
|
114
|
+
<details class="rm-context"><summary>Reply to this feedback</summary>
|
|
113
115
|
<div class="rm-thread" style="<%= @annotation.thread.empty? ? 'margin-top: 24px; border-top: 1px solid #e5e7eb; padding-top: 20px;' : '' %>">
|
|
114
|
-
<h3>Add reply</h3>
|
|
115
116
|
<%= form_tag(rails_markup.annotation_path(@annotation, action_type: "reply"), method: :patch) do %>
|
|
116
117
|
<div class="rm-form-group">
|
|
117
118
|
<textarea name="message" class="rm-input" rows="2" placeholder="Type a reply..." required></textarea>
|
|
@@ -125,5 +126,6 @@
|
|
|
125
126
|
<button type="submit" class="rm-btn rm-btn-primary">Reply</button>
|
|
126
127
|
<% end %>
|
|
127
128
|
</div>
|
|
129
|
+
</details>
|
|
128
130
|
</div>
|
|
129
131
|
</turbo-frame>
|
|
@@ -33,10 +33,9 @@ module RailsMarkup
|
|
|
33
33
|
# Default: true
|
|
34
34
|
attr_accessor :toolbar_enabled
|
|
35
35
|
|
|
36
|
-
# Show the floating
|
|
37
|
-
# When false, the
|
|
38
|
-
#
|
|
39
|
-
# provides its own trigger or wants read-only pins. Requires toolbar_enabled.
|
|
36
|
+
# Show the floating toolbar (FAB and its expanded controls).
|
|
37
|
+
# When false, the whole dock is hidden; pins and synchronization stay active.
|
|
38
|
+
# Browser preferences can restore it from the engine dashboard. Requires toolbar_enabled.
|
|
40
39
|
# Default: true
|
|
41
40
|
attr_accessor :fab_visible
|
|
42
41
|
|
data/lib/rails_markup/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-markup
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- InventList
|
|
@@ -135,9 +135,12 @@ files:
|
|
|
135
135
|
- app/views/rails_markup/dashboard/_annotation_card.html.erb
|
|
136
136
|
- app/views/rails_markup/dashboard/_annotation_list.html.erb
|
|
137
137
|
- app/views/rails_markup/dashboard/_annotation_page.html.erb
|
|
138
|
+
- app/views/rails_markup/dashboard/_feedback_modal.html.erb
|
|
138
139
|
- app/views/rails_markup/dashboard/_filters.html.erb
|
|
140
|
+
- app/views/rails_markup/dashboard/_modal_navigation.html.erb
|
|
139
141
|
- app/views/rails_markup/dashboard/_stats.html.erb
|
|
140
142
|
- app/views/rails_markup/dashboard/_styles.html.erb
|
|
143
|
+
- app/views/rails_markup/dashboard/_toolbar_preferences.html.erb
|
|
141
144
|
- app/views/rails_markup/dashboard/board.html.erb
|
|
142
145
|
- app/views/rails_markup/dashboard/index.html.erb
|
|
143
146
|
- app/views/rails_markup/dashboard/show.html.erb
|