studio-engine 0.88.0 → 0.89.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/CHANGELOG.md +16 -0
- data/app/views/studio/surveys/_script.html.erb +31 -13
- data/app/views/studio/surveys/_styles.html.erb +13 -2
- data/app/views/studio/surveys/show.html.erb +13 -8
- data/lib/studio/survey.rb +9 -0
- data/lib/studio/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eeb8af5c3521e31b4bda82538bc2053bf4ff364008f616454ce89aa267d30643
|
|
4
|
+
data.tar.gz: b4374bb37c27468e3b20df3656973a1818df2445aa7b0a0b6c242dc2d67f08fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c107ea4e17a719e1612a243550c0427bc5868e5ebc0219ab913e12312d4580fee715788afd80cdb6617f7ef4b8df799e67e8507fd3851db2d2f71e85fcf07bd0
|
|
7
|
+
data.tar.gz: f41f199936bd5bd48bf226b0df833679c956ca84671c86971f9a115b88c666885ebe4229814fd17b240cf8bed981da5660315dc75bdd8185ee9df4c51f185a1a
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.89.0 — 2026-10-05
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **A survey opens on question 1.** The intro screen and its Start button are
|
|
12
|
+
gone: a respondent lands on "Question 1 of N" with the progress bar started,
|
|
13
|
+
and question 1 shows no Back button. The definition's `intro` becomes a quiet
|
|
14
|
+
lead line above question 1, and its `title` stays the page's `h1`; when the
|
|
15
|
+
title repeats question 1 (`Studio::Survey#title_repeats_first_question?`), the
|
|
16
|
+
stepper hides it visually so the same words are not painted twice. Number keys
|
|
17
|
+
and Enter work from the first paint. Returning respondents still resume on
|
|
18
|
+
their first unanswered question, and the no-JavaScript form shows the title
|
|
19
|
+
and intro at the top. No host change is needed.
|
|
20
|
+
- **The survey's "Press a number to choose" hint** no longer shows at phone width
|
|
21
|
+
(under 640px), as well as on touch screens.
|
|
22
|
+
|
|
7
23
|
## 0.88.0 — 2026-10-05
|
|
8
24
|
|
|
9
25
|
### Added
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
|
|
20
20
|
var form = root.querySelector("[data-survey-form]");
|
|
21
21
|
var steps = Array.prototype.slice.call(root.querySelectorAll("[data-survey-step]"));
|
|
22
|
-
|
|
22
|
+
// Every step is a question: the survey opens on question 1, no intro screen.
|
|
23
|
+
var total = steps.length;
|
|
24
|
+
var head = root.querySelector("[data-survey-head]");
|
|
23
25
|
var back = root.querySelector("[data-survey-back]");
|
|
24
26
|
var next = root.querySelector("[data-survey-next]");
|
|
25
27
|
var submit = root.querySelector("[data-survey-submit]");
|
|
@@ -128,21 +130,23 @@
|
|
|
128
130
|
}
|
|
129
131
|
});
|
|
130
132
|
current = index;
|
|
131
|
-
var
|
|
133
|
+
var first = index === 0;
|
|
132
134
|
var last = index === steps.length - 1;
|
|
133
|
-
|
|
135
|
+
// Nothing comes before question 1, so it has no Back. Next keeps the
|
|
136
|
+
// right edge on its own (margin-left: auto), so the row does not shift.
|
|
137
|
+
back.hidden = first;
|
|
134
138
|
next.hidden = last;
|
|
135
|
-
next.textContent = onIntro ? "Start" : "Next";
|
|
136
139
|
submit.hidden = !last;
|
|
140
|
+
// The title and intro lead question 1 only; later screens are the question alone.
|
|
141
|
+
if (head) head.hidden = !first;
|
|
137
142
|
// The number-key shortcut only means something on a choice or scale.
|
|
138
143
|
var step = steps[index];
|
|
139
144
|
hint.hidden = !(step.dataset.key && (isChoice(step) || isMulti(step)));
|
|
140
145
|
|
|
141
|
-
var position =
|
|
146
|
+
var position = index + 1;
|
|
142
147
|
fill.style.width = (total ? (position / total) * 100 : 0) + "%";
|
|
143
148
|
bar.setAttribute("aria-valuenow", String(position));
|
|
144
|
-
|
|
145
|
-
count.textContent = onIntro ? "" : "Question " + index + " of " + total;
|
|
149
|
+
count.textContent = "Question " + position + " of " + total;
|
|
146
150
|
|
|
147
151
|
var focusTarget = steps[index].querySelector("[data-survey-focus]");
|
|
148
152
|
if (focusTarget && direction) focusTarget.focus({ preventScroll: true });
|
|
@@ -182,7 +186,7 @@
|
|
|
182
186
|
// The first required question still missing an answer, for a Submit that
|
|
183
187
|
// jumps ahead (Enter on the last step, or a mid-survey reload).
|
|
184
188
|
form.addEventListener("submit", function (event) {
|
|
185
|
-
for (var i =
|
|
189
|
+
for (var i = 0; i < steps.length; i++) {
|
|
186
190
|
if (steps[i].dataset.required === "true" && !answered(steps[i])) {
|
|
187
191
|
event.preventDefault();
|
|
188
192
|
go(i);
|
|
@@ -230,7 +234,7 @@
|
|
|
230
234
|
timers[step.dataset.key] = setTimeout(function () { save(step); }, 900);
|
|
231
235
|
});
|
|
232
236
|
|
|
233
|
-
|
|
237
|
+
function onKey(event) {
|
|
234
238
|
if (event.defaultPrevented || event.altKey) return;
|
|
235
239
|
// A tap on an already-chosen option fires pointerdown but no change, so
|
|
236
240
|
// the flag would outlive it and make the next arrow key advance.
|
|
@@ -256,15 +260,29 @@
|
|
|
256
260
|
option.focus({ preventScroll: true });
|
|
257
261
|
pointerPick = isChoice(step);
|
|
258
262
|
option.dispatchEvent(new Event("change", { bubbles: true }));
|
|
259
|
-
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
root.addEventListener("keydown", onKey);
|
|
266
|
+
// The survey opens on question 1 with nothing focused, so a first key
|
|
267
|
+
// lands on <body>, outside the root. Take those keys too, and only those:
|
|
268
|
+
// a key aimed at any other control on the page is not the survey's. The
|
|
269
|
+
// listener retires itself once a Turbo visit has taken this root away.
|
|
270
|
+
function onPageKey(event) {
|
|
271
|
+
if (!root.isConnected) { document.removeEventListener("keydown", onPageKey); return; }
|
|
272
|
+
var target = event.target;
|
|
273
|
+
if (target === document.body || target === document.documentElement) onKey(event);
|
|
274
|
+
}
|
|
275
|
+
document.addEventListener("keydown", onPageKey);
|
|
260
276
|
|
|
261
277
|
var resume = root.dataset.resumeIndex;
|
|
262
|
-
|
|
278
|
+
// resume-index is the question's own index, and every step is a question.
|
|
279
|
+
var start = resume === "" || resume === undefined ? 0 : parseInt(resume, 10);
|
|
280
|
+
if (!(start >= 0 && start < steps.length)) start = 0;
|
|
263
281
|
var hasErrors = root.dataset.hasErrors === "true";
|
|
264
|
-
// A fresh visitor
|
|
282
|
+
// A fresh visitor lands on question 1; a returning one on their next
|
|
265
283
|
// question, and a failed submit on the first question it flagged.
|
|
266
284
|
if (restored >= 0) start = restored;
|
|
267
|
-
render(start
|
|
285
|
+
render(start, null);
|
|
268
286
|
if (restored < 0 && (start > 0 || hasErrors)) {
|
|
269
287
|
var focusTarget = steps[current].querySelector("[data-survey-focus]");
|
|
270
288
|
if (focusTarget) focusTarget.focus({ preventScroll: true });
|
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
.studio-survey__title { margin: 0 0 .75rem; font-size: 1.75rem; line-height: 1.2; font-weight: 800; color: var(--color-text); outline: none; }
|
|
19
19
|
.studio-survey__intro-copy { margin: 0 0 1rem; font-size: 1.0625rem; line-height: 1.55; color: var(--color-text-body); white-space: pre-line; }
|
|
20
20
|
.studio-survey__meta { margin: 0; font-size: .8125rem; color: var(--color-text-muted); }
|
|
21
|
+
.studio-survey__head { margin: 0 0 2.25rem; }
|
|
22
|
+
/* Stepped, the head leads question 1 quietly: a small title (or none, when it
|
|
23
|
+
repeats question 1, kept for screen readers), the intro as one lead line,
|
|
24
|
+
and no meta, since the count above already says "Question 1 of N". */
|
|
25
|
+
.studio-survey.is-enhanced .studio-survey__head { margin-bottom: 1.25rem; }
|
|
26
|
+
.studio-survey.is-enhanced .studio-survey__title { margin: 0 0 .375rem; font-size: 1rem; line-height: 1.4; font-weight: 700; color: var(--color-text-secondary); }
|
|
27
|
+
.studio-survey.is-enhanced .studio-survey__head--echo .studio-survey__title { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
28
|
+
.studio-survey.is-enhanced .studio-survey__intro-copy { margin: 0; font-size: .9375rem; line-height: 1.5; color: var(--color-text-secondary); }
|
|
29
|
+
.studio-survey.is-enhanced .studio-survey__meta { display: none; }
|
|
21
30
|
.studio-survey__question { display: block; width: 100%; padding: 0; margin: 0 0 .5rem; font-size: 1.375rem; line-height: 1.3; font-weight: 700; color: var(--color-text); outline: none; }
|
|
22
31
|
.studio-survey__qnum { display: block; margin-bottom: .375rem; font-size: .75rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--color-text-muted); }
|
|
23
32
|
.studio-survey.is-enhanced .studio-survey__qnum { display: none; }
|
|
@@ -68,7 +77,9 @@
|
|
|
68
77
|
.studio-survey__forward { margin-left: auto; }
|
|
69
78
|
.studio-survey__status { min-height: 1.25rem; margin: .5rem 0 0; font-size: .75rem; text-align: center; color: var(--color-text-muted); }
|
|
70
79
|
.studio-survey__hint { margin: .25rem 0 0; font-size: .75rem; text-align: center; color: var(--color-text-muted); }
|
|
71
|
-
|
|
80
|
+
/* The hint speaks to a keyboard. A touch screen has none, and a phone-width
|
|
81
|
+
window is a phone more often than not, so neither shows it. */
|
|
82
|
+
@media (hover: none), (pointer: coarse), (max-width: 639px) { .studio-survey__hint { display: none; } }
|
|
72
83
|
|
|
73
84
|
.studio-survey__card { padding: 2rem 1.5rem; text-align: center; border: 1px solid var(--color-border); border-radius: 1.25rem; background: var(--color-surface); }
|
|
74
85
|
.studio-survey__burst { font-size: 3rem; line-height: 1; margin-bottom: .75rem; }
|
|
@@ -86,7 +97,7 @@
|
|
|
86
97
|
@media (min-width: 640px) {
|
|
87
98
|
.studio-survey { padding-top: 3rem; }
|
|
88
99
|
.studio-survey__question { font-size: 1.625rem; }
|
|
89
|
-
.studio-survey__title { font-size: 2.125rem; }
|
|
100
|
+
.studio-survey:not(.is-enhanced) .studio-survey__title { font-size: 2.125rem; }
|
|
90
101
|
.studio-survey__scale { gap: .75rem; }
|
|
91
102
|
.studio-survey__tile { min-height: 5.5rem; }
|
|
92
103
|
.studio-survey__face { font-size: 2.5rem; }
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<%# /surveys/:slug — the survey (Studio::SurveysController#show, docs/SURVEYS.md).
|
|
2
2
|
|
|
3
3
|
ONE FORM, TWO PRESENTATIONS. Without JavaScript every question renders in
|
|
4
|
-
order and the Submit button posts the lot. The
|
|
5
|
-
.is-enhanced and turns the same markup into one question
|
|
6
|
-
|
|
4
|
+
order under the title and intro, and the Submit button posts the lot. The
|
|
5
|
+
script below adds .is-enhanced and turns the same markup into one question
|
|
6
|
+
per screen, opening on question 1 with no intro screen, with a progress
|
|
7
|
+
bar, Back (from question 2 on) and Next, number-key and Enter shortcuts, and an
|
|
7
8
|
autosave on each answer. Submit is still the same POST either way, so the
|
|
8
9
|
server's validation is the one rule. %>
|
|
9
10
|
<% total = @survey.questions.size %>
|
|
@@ -28,10 +29,14 @@
|
|
|
28
29
|
|
|
29
30
|
<%= form_with url: studio_survey_path(@survey.slug), method: :post, local: true,
|
|
30
31
|
html: { class: "studio-survey__form", data: { survey_form: true } } do %>
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
<%# The head is not a step: the survey opens straight on question 1. The
|
|
33
|
+
title stays the page's h1 for a screen reader; when it says what
|
|
34
|
+
question 1 asks, the stepper hides it visually rather than paint the
|
|
35
|
+
same words twice. The intro is a quiet lead line over question 1. %>
|
|
36
|
+
<header class="studio-survey__head<%= " studio-survey__head--echo" if @survey.title_repeats_first_question? %>" data-survey-head>
|
|
37
|
+
<h1 class="studio-survey__title" id="studio-survey-title"><%= @survey.title %></h1>
|
|
33
38
|
<% if @survey.intro %>
|
|
34
|
-
<p class="studio-survey__intro-copy"><%= @survey.intro %></p>
|
|
39
|
+
<p class="studio-survey__intro-copy" data-survey-intro><%= @survey.intro %></p>
|
|
35
40
|
<% end %>
|
|
36
41
|
<p class="studio-survey__meta">
|
|
37
42
|
<%= pluralize(total, "question") %> · about <%= pluralize(minutes, "minute") %><% if @survey.required_keys.any? %> · <span aria-hidden="true">*</span> required<% end %>
|
|
@@ -39,7 +44,7 @@
|
|
|
39
44
|
<% if @errors.any? %>
|
|
40
45
|
<p class="studio-survey__error" role="alert">A few answers need another look before this can be sent.</p>
|
|
41
46
|
<% end %>
|
|
42
|
-
</
|
|
47
|
+
</header>
|
|
43
48
|
|
|
44
49
|
<% @survey.questions.each_with_index do |question, index| %>
|
|
45
50
|
<%= render "studio/surveys/question", question: question, index: index, total: total,
|
|
@@ -48,7 +53,7 @@
|
|
|
48
53
|
|
|
49
54
|
<div class="studio-survey__nav">
|
|
50
55
|
<button type="button" class="btn btn-neutral" data-survey-back hidden>Back</button>
|
|
51
|
-
<button type="button" class="btn btn-primary studio-survey__forward" data-survey-next hidden>
|
|
56
|
+
<button type="button" class="btn btn-primary studio-survey__forward" data-survey-next hidden>Next</button>
|
|
52
57
|
<button type="submit" class="btn btn-primary studio-survey__forward" data-survey-submit>Submit</button>
|
|
53
58
|
</div>
|
|
54
59
|
<p class="studio-survey__status" aria-live="polite" data-survey-status></p>
|
data/lib/studio/survey.rb
CHANGED
|
@@ -241,6 +241,15 @@ module Studio
|
|
|
241
241
|
def keys = questions.map(&:key)
|
|
242
242
|
def required_keys = questions.select(&:required?).map(&:key)
|
|
243
243
|
|
|
244
|
+
# True when the title says what the first question asks ("How was your
|
|
245
|
+
# first game?" twice). The survey opens on that question, so the stepper
|
|
246
|
+
# keeps the title as the page's heading for a screen reader but does not
|
|
247
|
+
# paint it twice. Compared on letters and digits alone, ignoring case.
|
|
248
|
+
def title_repeats_first_question?
|
|
249
|
+
squash = ->(text) { text.to_s.downcase.gsub(/[^[:alnum:]]/, "") }
|
|
250
|
+
squash.call(title) == squash.call(questions.first.label)
|
|
251
|
+
end
|
|
252
|
+
|
|
244
253
|
private
|
|
245
254
|
|
|
246
255
|
def validate!
|
data/lib/studio/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.89.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|