howdoc 0.3.1 → 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 168c62956710423757404d82987844f3b33d64ebdc112f9c5c0354cd336c0944
4
- data.tar.gz: 842bebc8325f407a398af4f87e050819325cdafea0d26ab568a694c91f3c765f
3
+ metadata.gz: 87dfef0ef3ca14057c666eaa64c003057f7b7f8ba17cb1f992375c4add7358e6
4
+ data.tar.gz: 5a3f3ecfccd3e57262e00b434ba69d79874251d25e74b527e8a6d03754e5c0ef
5
5
  SHA512:
6
- metadata.gz: 139dfdadfe51e236abfb19bff5cb95bd28d6cb8ddd7e817b1ca9cb93362cd5cb3b9b83313544c3b9d35423f58a1385d2db807794dd2723f1e0fd4287732f34ac
7
- data.tar.gz: 57d7cc5bafa03b0b103204e3a13a04015d5676445050a621cbca7b8be6e2290b01fd39bd4dd8db30377030277b66c01f57780b8462358a9a23a3b05be179159d
6
+ metadata.gz: 80ffc63f82dd61163966cd9afa236ff758b29ad02bf1dcf54486d41db1fcdee943bcbd337d0966a9a6c69aae624355d43b732164ac31425483dabc9c395812d0
7
+ data.tar.gz: 756e73c34a33a1a4b5cdb0b41f4cf2143414c3b97bc30096e62511d38961ff8cc07b44ba9e35c2956b0cb0e942bd9d669bb8574ec2d578a6dba368c8dce5135d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ The menu now fits a phone. Twenty guides listed above the page filled the
6
+ screen before the guide a reader had come for began, so a narrow screen folds
7
+ the list away behind a line they can tap, and a wide one, which has room for it
8
+ beside the page, shows it exactly as before.
9
+
10
+ - The menu on a narrow screen is a slim bar carrying the name of the
11
+ documentation set, the languages and one line to open the guides
12
+ (`howdoc.nav.guides`). It follows the reader down the page while the list is
13
+ folded, and steps out of the way while it is open.
14
+ - The index is the one page whose menu is the page, so there the list is open
15
+ from the start and follows the heading that says what the guides are for
16
+ instead of pushing it under twenty links.
17
+ - Only `shared/html/nav.haml` and the stylesheet changed, so an application
18
+ with a menu template of its own keeps what it has.
19
+
20
+ ## 0.4.0
21
+
22
+ Screenshots can now show what the step is about, the way a guide made by hand
23
+ does: an arrow on the button being pressed, a highlight around the words being
24
+ looked for.
25
+
26
+ - The marker is drawn in the browser between sizing the window and taking the
27
+ picture, and removed again straight after -- including when the picture could
28
+ not be taken, so nothing is left pointing at what has since moved.
29
+ - An arrow's tip goes on the middle of a button, and below anything small
30
+ enough that the arrow would cover it.
31
+ - A highlight measures the words themselves, through a range around the match,
32
+ rather than the paragraph that happens to hold them.
33
+ - `config.illustrate_actions` illustrates every action rather than only the
34
+ moments a reader arrives somewhere. That is what makes the pointer worth
35
+ having: a picture per click, each one marked.
36
+ - `config.marker`, `config.marker_colour`, `config.marker_dim` and
37
+ `config.marker_size` decide whether and how it is drawn.
38
+
3
39
  ## 0.3.1
4
40
 
5
41
  - The menu pass left a blank line behind every time it ran. A suite that builds
data/README.md CHANGED
@@ -114,6 +114,31 @@ By default `visit` and `assert_text` are illustrated and the action verbs are
114
114
  not, on the grounds that a picture is worth showing when the reader arrives
115
115
  somewhere, not every time they press a button.
116
116
 
117
+ ## Showing where to look
118
+
119
+ A screenshot of a busy page does not say which of forty things on it the step is
120
+ about, so howdoc draws the mark a person would draw by hand:
121
+
122
+ * an arrow with its tip on the button about to be pressed, on the field about to
123
+ be filled, on the menu about to be chosen from
124
+ * a highlight around the words an assertion was looking for
125
+
126
+ It is drawn in the browser between sizing the window and taking the picture, and
127
+ taken off again straight after, so the page the test goes on driving is the page
128
+ it was. There is no cursor in a headless screenshot to photograph -- the arrow is
129
+ drawn, and its tip moves below anything too small for it to sit on without
130
+ covering.
131
+
132
+ By default only the moments a reader arrives somewhere are illustrated. A guide
133
+ that shows every click is one line of configuration:
134
+
135
+ ```ruby
136
+ config.illustrate_actions = true # a picture per action, each one marked
137
+ config.marker_colour = '#e11d48' # the arrow and the highlight
138
+ config.marker_dim = true # dim the rest of the page around the mark
139
+ config.marker = false # or draw nothing at all
140
+ ```
141
+
117
142
  ## Your own helpers
118
143
 
119
144
  Real suites have helpers Capybara has no verb for -- a custom editor, a menu
@@ -238,6 +263,11 @@ an empty placeholder and the index pass -- which has just read every page back
238
263
 
239
264
  Both are optional. A template that carries neither is left exactly as it is.
240
265
 
266
+ Above the page rather than beside it -- a phone, a narrow window -- the list of
267
+ guides folds away behind a line the reader taps, so the guide begins where the
268
+ screen begins. The stylesheet unfolds it again where there is room beside the
269
+ page, and the index, whose menu is the page, is open from the start.
270
+
241
271
  The menu also links the same guide in every other language the application
242
272
  publishes. Guides are matched by their identifier rather than their file name,
243
273
  because a translated heading gives a translated file name, and each language
@@ -21,6 +21,8 @@ en:
21
21
  follow_steps: 'Follow these steps:'
22
22
  languages:
23
23
  en: English
24
+ nav:
25
+ guides: All guides
24
26
  pager:
25
27
  next: Next
26
28
  previous: Previous
@@ -21,6 +21,8 @@ et:
21
21
  follow_steps: 'Palun jälgige järgnevaid samme:'
22
22
  languages:
23
23
  et: Eesti
24
+ nav:
25
+ guides: Kõik juhendid
24
26
  pager:
25
27
  next: Järgmine
26
28
  previous: Eelmine
@@ -30,7 +30,10 @@ module Howdoc
30
30
  def fill_in(locator = nil, with:, currently_with: nil, fill_options: {}, **options)
31
31
  doc = Howdoc.extract_options!(options)
32
32
 
33
- narrate(fill_in_action(locator), doc, field: howdoc_field_label(locator), value: with)
33
+ narrate(
34
+ fill_in_action(locator), doc,
35
+ target: [:fillable_field, locator], field: howdoc_field_label(locator), value: with
36
+ )
34
37
 
35
38
  super(locator, with:, currently_with:, fill_options:, **options)
36
39
  end
@@ -38,7 +41,7 @@ module Howdoc
38
41
  def select(value = nil, from: nil, **options)
39
42
  doc = Howdoc.extract_options!(options)
40
43
 
41
- narrate(:select, doc, value:, field: howdoc_field_label(from))
44
+ narrate(:select, doc, target: [:select, from], value:, field: howdoc_field_label(from))
42
45
 
43
46
  super(value, from:, **options)
44
47
  end
@@ -46,7 +49,7 @@ module Howdoc
46
49
  def check(locator = nil, **options)
47
50
  doc = Howdoc.extract_options!(options)
48
51
 
49
- narrate(:check, doc, field: howdoc_field_label(locator))
52
+ narrate(:check, doc, target: [:checkbox, locator], field: howdoc_field_label(locator))
50
53
 
51
54
  super(locator, **options)
52
55
  end
@@ -54,7 +57,7 @@ module Howdoc
54
57
  def uncheck(locator = nil, **options)
55
58
  doc = Howdoc.extract_options!(options)
56
59
 
57
- narrate(:uncheck, doc, field: howdoc_field_label(locator))
60
+ narrate(:uncheck, doc, target: [:checkbox, locator], field: howdoc_field_label(locator))
58
61
 
59
62
  super(locator, **options)
60
63
  end
@@ -62,7 +65,7 @@ module Howdoc
62
65
  def choose(locator = nil, **options)
63
66
  doc = Howdoc.extract_options!(options)
64
67
 
65
- narrate(:choose, doc, field: howdoc_field_label(locator))
68
+ narrate(:choose, doc, target: [:radio_button, locator], field: howdoc_field_label(locator))
66
69
 
67
70
  super(locator, **options)
68
71
  end
@@ -70,7 +73,7 @@ module Howdoc
70
73
  def click_button(locator = nil, **options)
71
74
  doc = Howdoc.extract_options!(options)
72
75
 
73
- narrate(:click_button, doc, locator:)
76
+ narrate(:click_button, doc, target: [:button, locator], locator:)
74
77
 
75
78
  super(locator, **options)
76
79
  end
@@ -78,7 +81,7 @@ module Howdoc
78
81
  def click_link(locator = nil, **options)
79
82
  doc = Howdoc.extract_options!(options)
80
83
 
81
- narrate(:click_link, doc, locator:)
84
+ narrate(:click_link, doc, target: [:link, locator], locator:)
82
85
 
83
86
  super(locator, **options)
84
87
  end
@@ -86,7 +89,7 @@ module Howdoc
86
89
  def click_on(locator = nil, **options)
87
90
  doc = Howdoc.extract_options!(options)
88
91
 
89
- narrate(:click_on, doc, locator:)
92
+ narrate(:click_on, doc, target: [:link_or_button, locator], locator:)
90
93
 
91
94
  super(locator, **options)
92
95
  end
@@ -95,7 +98,7 @@ module Howdoc
95
98
  def attach_file(locator = nil, paths, make_visible: nil, **options)
96
99
  doc = Howdoc.extract_options!(options)
97
100
 
98
- narrate(:attach_file, doc, field: howdoc_field_label(locator))
101
+ narrate(:attach_file, doc, target: [:file_field, locator], field: howdoc_field_label(locator))
99
102
 
100
103
  super(locator, paths, make_visible:, **options)
101
104
  end
@@ -107,7 +110,10 @@ module Howdoc
107
110
  doc = Howdoc.extract_options!(options)
108
111
  result = super(locator, **options)
109
112
 
110
- narrate(:assert_text, doc, capture: true, arrival: true, text: locator)
113
+ narrate(
114
+ :assert_text, doc,
115
+ capture: true, arrival: true, text: locator, target: locator.to_s, marker_mode: :highlight
116
+ )
111
117
  result
112
118
  end
113
119
 
@@ -138,19 +144,48 @@ module Howdoc
138
144
  host.nil? ? visit_uri.to_s : "https://#{host}#{visit_uri}"
139
145
  end
140
146
 
141
- def narrate(action, doc_options, capture: false, arrival: false, **payload)
147
+ def narrate(action, doc_options, capture: false, arrival: false, target: nil,
148
+ marker_mode: :pointer, **payload)
142
149
  step = Howdoc.record(action, arrival:, nodoc: doc_options[:nodoc], **payload)
143
150
  return nil if step.nil?
144
151
 
145
- illustrate = doc_options.fetch(:screenshot, capture)
152
+ illustrate = doc_options.fetch(:screenshot, capture || illustrated_action?(target))
146
153
  return step unless illustrate
147
154
 
148
155
  Howdoc.capture(
149
156
  page,
150
157
  full_page: doc_options[:full_page] || false,
151
- no_screenshot: doc_options[:no_screenshot] || false
158
+ no_screenshot: doc_options[:no_screenshot] || false,
159
+ marker: howdoc_marker(target), marker_mode:
152
160
  )
153
161
  step
154
162
  end
163
+
164
+ # An action is illustrated only when the application asked for every click
165
+ # to be shown; a picture after each button is a lot of pictures, and worth
166
+ # it only when they are marked.
167
+ def illustrated_action?(target)
168
+ !target.nil? && Howdoc.config.illustrate_actions?
169
+ end
170
+
171
+ # What the marker should point at. A string is the words an assertion was
172
+ # looking for, which the browser finds for itself; anything else names a
173
+ # Capybara selector and is resolved here, in whatever scope the test is in,
174
+ # exactly as the action about to happen would resolve it.
175
+ #
176
+ # Never waits: the action itself is entitled to wait, but a picture is not
177
+ # worth holding a suite up for, and a marker nobody could place is simply
178
+ # not drawn.
179
+ def howdoc_marker(target)
180
+ return nil if target.nil? || !Howdoc.config.marker?
181
+ return target if target.is_a?(String)
182
+
183
+ kind, locator = target
184
+ return nil if locator.nil?
185
+
186
+ page.find(kind, locator, match: :first, wait: 0)
187
+ rescue StandardError
188
+ nil
189
+ end
155
190
  end
156
191
  end
@@ -35,6 +35,29 @@ module Howdoc
35
35
  # page, so that guides do not mix wildly different image proportions.
36
36
  attr_accessor :screenshot_height
37
37
 
38
+ # Whether a screenshot points at what the step is about: an arrow on the
39
+ # button being pressed, a highlight around the words being looked for. Off
40
+ # leaves the pictures exactly as the browser rendered them.
41
+ attr_accessor :marker
42
+
43
+ # The colour of that arrow and highlight. Red by convention, because a guide
44
+ # is read at a glance and red is the one colour an interface rarely uses for
45
+ # anything else.
46
+ attr_accessor :marker_colour
47
+
48
+ # Dims the rest of the page around what is marked. Emphatic, and worth it
49
+ # for a crowded screen; off by default because it changes every picture.
50
+ attr_accessor :marker_dim
51
+
52
+ # How tall the drawn pointer is, in CSS pixels.
53
+ attr_accessor :marker_size
54
+
55
+ # Whether an action -- a click, a choice, something typed -- is illustrated
56
+ # as well as narrated. Off by default: a picture belongs where the reader
57
+ # arrives somewhere new, not after every button. Turning it on is what makes
58
+ # the pointer worth having, so a guide shows every click.
59
+ attr_accessor :illustrate_actions
60
+
38
61
  # Files in the output directory that the clean task must leave alone,
39
62
  # typically hand-maintained landing pages.
40
63
  attr_accessor :preserved_files
@@ -53,19 +76,28 @@ module Howdoc
53
76
  attr_reader :locale_paths
54
77
 
55
78
  def initialize
79
+ reset_defaults
80
+ @template_paths = []
81
+ @locale_paths = []
82
+ @field_label = nil
83
+ @sort_key = nil
84
+ @group_label = nil
85
+ end
86
+
87
+ def reset_defaults
56
88
  @root = 'doc/howdoc'
57
89
  @enabled = false
58
90
  @host = nil
59
91
  @formats = %i[html]
60
92
  @locales = nil
61
93
  @screenshot_height = 940
94
+ @marker = true
95
+ @marker_colour = '#e11d48'
96
+ @marker_dim = false
97
+ @marker_size = 52
98
+ @illustrate_actions = false
62
99
  @preserved_files = %w[index.html .keep]
63
100
  @install_assets = true
64
- @template_paths = []
65
- @locale_paths = []
66
- @field_label = nil
67
- @sort_key = nil
68
- @group_label = nil
69
101
  end
70
102
 
71
103
  def register_template_path(path)
@@ -105,5 +137,13 @@ module Howdoc
105
137
  def enabled?
106
138
  !!@enabled
107
139
  end
140
+
141
+ def marker?
142
+ !!@marker
143
+ end
144
+
145
+ def illustrate_actions?
146
+ !!@illustrate_actions
147
+ end
108
148
  end
109
149
  end
@@ -0,0 +1,113 @@
1
+ // Drawn over the page for as long as it takes to photograph it: an arrow on
2
+ // what is about to be clicked, a highlight around the words that were looked
3
+ // for. Called with no target, it takes off whatever it drew last time.
4
+ var MARKER = 'howdoc-marker';
5
+ var POINTER =
6
+ // The arrow, in a 24x32 box with its tip at 0,0 -- the shape every operating
7
+ // system has drawn since 1981, so a reader knows it without being told.
8
+ 'M0 0 L0 22.6 L5.6 17.4 L9.4 25.9 L13.6 23.9 L9.8 15.7 L17.6 15.4 Z';
9
+
10
+ var target = arguments[0];
11
+ var options = arguments[1] || {};
12
+
13
+ var previous = document.getElementById(MARKER);
14
+ if (previous) { previous.parentNode.removeChild(previous); }
15
+ if (!target) { return null; }
16
+
17
+ var element = target;
18
+ var rect = null;
19
+
20
+ if (typeof target === 'string') {
21
+ // The words themselves, not the paragraph holding them: a range around
22
+ // the match measures what a reader would underline with a finger, where
23
+ // the element around it is as wide as the page.
24
+ var wanted = target.replace(/\\s+/g, ' ').trim();
25
+ var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
26
+ var node = null;
27
+ element = null;
28
+
29
+ while ((node = walker.nextNode())) {
30
+ var at = node.data.indexOf(wanted);
31
+ if (at === -1 || !node.parentElement || !node.parentElement.getClientRects().length) {
32
+ continue;
33
+ }
34
+
35
+ var range = document.createRange();
36
+ range.setStart(node, at);
37
+ range.setEnd(node, at + wanted.length);
38
+ rect = range.getBoundingClientRect();
39
+ element = node.parentElement;
40
+ break;
41
+ }
42
+ }
43
+
44
+ if (!element || !element.getClientRects().length) { return null; }
45
+ if (!rect) { rect = element.getBoundingClientRect(); }
46
+
47
+ // A field dressed by a widget -- select2 and its like -- leaves the real
48
+ // <select> hidden and all but sizeless in a corner of what the reader actually
49
+ // sees. Point at the thing on the screen: the nearest ancestor that is really
50
+ // laid out.
51
+ if (rect.width < 8 || rect.height < 8) {
52
+ var holder = element.parentElement;
53
+
54
+ while (holder && holder !== document.body) {
55
+ var box = holder.getBoundingClientRect();
56
+
57
+ if (box.width >= 8 && box.height >= 8) { element = holder; rect = box; break; }
58
+
59
+ holder = holder.parentElement;
60
+ }
61
+ }
62
+ var left = rect.left + window.scrollX;
63
+ var top = rect.top + window.scrollY;
64
+ var colour = options.colour || '#e11d48';
65
+ var padding = 6;
66
+
67
+ var layer = document.createElement('div');
68
+ layer.id = MARKER;
69
+ layer.style.cssText = 'position:absolute;left:0;top:0;width:0;height:0;' +
70
+ 'z-index:2147483647;pointer-events:none;';
71
+
72
+ var ring = document.createElement('div');
73
+ ring.style.cssText = 'position:absolute;box-sizing:border-box;' +
74
+ 'left:' + (left - padding) + 'px;top:' + (top - padding) + 'px;' +
75
+ 'width:' + (rect.width + padding * 2) + 'px;' +
76
+ 'height:' + (rect.height + padding * 2) + 'px;' +
77
+ 'border:3px solid ' + colour + ';border-radius:8px;' +
78
+ (options.mode === 'highlight' ? 'background:' + colour + '1f;' : '') +
79
+ (options.dim ? 'box-shadow:0 0 0 9999px rgba(15,23,42,0.45);' : '');
80
+ layer.appendChild(ring);
81
+
82
+ if (options.mode !== 'highlight') {
83
+ var size = options.size || 52;
84
+
85
+ // The tip goes on the middle of a button, where a hand would put it, and a
86
+ // little below the middle on a field or a select: there the value is read
87
+ // along the top of the box, and an arrow through the middle of it sits in
88
+ // the way of the very thing the reader is meant to see.
89
+ //
90
+ // On something small -- a link in a table, a word in a row -- the arrow would
91
+ // cover what it points at whatever we do, so it hangs below instead.
92
+ var small = rect.width < 120 || rect.height < 32;
93
+ var tipLeft = left + rect.width / 2;
94
+ var tipTop = small ? top + rect.height - 2 : top + rect.height * 0.62;
95
+ var arrow = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
96
+ arrow.setAttribute('viewBox', '0 0 24 32');
97
+ arrow.setAttribute('width', size);
98
+ arrow.setAttribute('height', size * 32 / 24);
99
+ arrow.style.cssText = 'position:absolute;left:' + tipLeft + 'px;top:' + tipTop + 'px;' +
100
+ 'filter:drop-shadow(0 2px 3px rgba(15,23,42,0.45));';
101
+
102
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
103
+ path.setAttribute('d', POINTER);
104
+ path.setAttribute('fill', '#ffffff');
105
+ path.setAttribute('stroke', colour);
106
+ path.setAttribute('stroke-width', '2.2');
107
+ path.setAttribute('stroke-linejoin', 'round');
108
+ arrow.appendChild(path);
109
+ layer.appendChild(arrow);
110
+ }
111
+
112
+ document.body.appendChild(layer);
113
+ return true;
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howdoc
4
+ # Shows the reader where to look.
5
+ #
6
+ # A screenshot of a busy page does not say which of forty things on it the
7
+ # step is about. A guide made by hand solves that with a red circle drawn over
8
+ # the button; this draws the same thing, in the browser, in the moment between
9
+ # the window being sized and the picture being taken -- and takes it off again
10
+ # afterwards, so the page the test goes on driving is the page it was.
11
+ #
12
+ # There is no cursor in a headless screenshot to photograph, so the pointer is
13
+ # drawn: an arrow with its tip on whatever is about to be clicked. An assertion
14
+ # gets no arrow, because nobody is pointing at anything -- the words the reader
15
+ # should see are highlighted instead.
16
+ #
17
+ # The drawing itself is JavaScript, and lives in JavaScript: marker.js beside
18
+ # this file. Asked to draw nothing, it takes off what it drew last time, which
19
+ # is how the marker is removed.
20
+ module Marker
21
+ SCRIPT = File.read(File.expand_path('marker.js', __dir__)).freeze
22
+
23
+ module_function
24
+
25
+ # Draws the marker, runs the block, and takes it off again -- including when
26
+ # the block raised, because a marker left behind would appear in the next
27
+ # picture, pointing at something that is no longer there.
28
+ def around(page, target, mode:)
29
+ return yield if target.nil? || !Howdoc.config.marker?
30
+
31
+ drawn = draw(page, target, mode:)
32
+
33
+ begin
34
+ yield
35
+ ensure
36
+ draw(page, nil, mode:) if drawn
37
+ end
38
+ end
39
+
40
+ def draw(page, target, mode:)
41
+ page.execute_script(SCRIPT, target, options(mode))
42
+ true
43
+ rescue StandardError
44
+ # A marker is a courtesy. A page that will not take one -- a frame gone, an
45
+ # element detached between finding it and drawing on it -- still has a
46
+ # picture worth taking.
47
+ false
48
+ end
49
+
50
+ def options(mode)
51
+ {
52
+ 'mode' => mode.to_s,
53
+ 'colour' => Howdoc.config.marker_colour,
54
+ 'dim' => Howdoc.config.marker_dim,
55
+ 'size' => Howdoc.config.marker_size
56
+ }
57
+ end
58
+ end
59
+ end
@@ -54,7 +54,7 @@ module Howdoc
54
54
  # Illustrates the step recorded most recently. Separate from +record+
55
55
  # because a screenshot has to be taken while the browser is still on the
56
56
  # page, whereas the sentence can be composed at any time.
57
- def capture(page, full_page: false, no_screenshot: false)
57
+ def capture(page, full_page: false, no_screenshot: false, marker: nil, marker_mode: :pointer)
58
58
  return nil if no_screenshot || !recording?
59
59
 
60
60
  step = current.last_step || current.new_step
@@ -64,7 +64,7 @@ module Howdoc
64
64
  page,
65
65
  path: File.join(current.image_dir, filename),
66
66
  height: Howdoc.config.screenshot_height,
67
- full_page:
67
+ full_page:, marker:, marker_mode:
68
68
  )
69
69
 
70
70
  step.screenshot = filename
@@ -7,7 +7,10 @@ module Howdoc
7
7
  module Screenshot
8
8
  module_function
9
9
 
10
- def capture(page, path:, height:, full_page: false)
10
+ # The marker is drawn after the window has been sized and not before: a
11
+ # resize lays the page out again, and an arrow placed by the old layout
12
+ # would point at where the button used to be.
13
+ def capture(page, path:, height:, full_page: false, marker: nil, marker_mode: :pointer)
11
14
  FileUtils.mkdir_p(File.dirname(path))
12
15
 
13
16
  width, current_height = page.current_window.size
@@ -15,7 +18,10 @@ module Howdoc
15
18
 
16
19
  page.current_window.resize_to(width, target) unless target == current_height
17
20
 
18
- page.save_screenshot(path, full: true)
21
+ Marker.around(page, marker, mode: marker_mode) do
22
+ page.save_screenshot(path, full: true)
23
+ end
24
+
19
25
  path
20
26
  end
21
27
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Howdoc
4
- VERSION = '0.3.1'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/howdoc.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'howdoc/narrator'
9
9
  require_relative 'howdoc/step'
10
10
  require_relative 'howdoc/edition'
11
11
  require_relative 'howdoc/document'
12
+ require_relative 'howdoc/marker'
12
13
  require_relative 'howdoc/screenshot'
13
14
  require_relative 'howdoc/recorder'
14
15
  require_relative 'howdoc/templates'
@@ -31,6 +31,12 @@ body.howdoc {
31
31
  padding: 3rem 1.25rem 6rem;
32
32
  }
33
33
 
34
+ /* The index ends where its menu begins, so the space below it is the menu's
35
+ to give. */
36
+ .howdoc-index {
37
+ padding-bottom: 1.5rem;
38
+ }
39
+
34
40
  .howdoc h1 {
35
41
  margin: 0 0 1rem;
36
42
  font-size: 1.9rem;
@@ -68,7 +74,7 @@ body.howdoc {
68
74
 
69
75
  .howdoc-toc {
70
76
  list-style: none;
71
- margin: 1.5rem 0 0;
77
+ margin: 1rem 0 0;
72
78
  padding: 0;
73
79
  }
74
80
 
@@ -96,19 +102,84 @@ body.howdoc {
96
102
  text-decoration: underline;
97
103
  }
98
104
 
99
- /* The menu sits beside the page it belongs to, and above it when there is no
100
- room beside. A guide written before the index pass has an empty menu, and an
101
- empty menu is no menu at all. */
105
+ /* The menu sits beside the page it belongs to, and folded above it when there
106
+ is no room beside. A guide written before the index pass has an empty menu,
107
+ and an empty menu is no menu at all. */
102
108
  .howdoc-nav:empty,
103
109
  .howdoc-pager:empty {
104
110
  display: none;
105
111
  }
106
112
 
113
+ /* Above the page, the menu is a slim bar the reader keeps within reach
114
+ wherever they have scrolled to, with the guides folded away behind a line
115
+ they can tap. Unfolded, the list is taller than the screen, so the bar stops
116
+ following along and scrolls away with the page like anything else. */
107
117
  .howdoc-nav {
108
- padding: 1.5rem 1.25rem;
118
+ position: sticky;
119
+ top: 0;
120
+ z-index: 2;
121
+ background: var(--howdoc-bg);
122
+ padding: 0.9rem 1.25rem 1rem;
109
123
  border-bottom: 1px solid var(--howdoc-rule);
110
124
  }
111
125
 
126
+ .howdoc-nav:has(.howdoc-nav-guides[open]) {
127
+ position: static;
128
+ }
129
+
130
+ /* The index is the one page whose menu is the page, so its menu is open and
131
+ there is nothing to fold. It comes after the heading that says what the
132
+ guides are for, rather than pushing that heading under twenty links. */
133
+ body.howdoc {
134
+ display: flex;
135
+ flex-direction: column;
136
+ }
137
+
138
+ .howdoc-nav-index {
139
+ order: 1;
140
+ }
141
+
142
+ /* Nothing to fold on the index: the list is the page, so the line that would
143
+ fold it away is not offered there. */
144
+ .howdoc-nav-index .howdoc-nav-summary {
145
+ display: none;
146
+ }
147
+
148
+ .howdoc-nav-summary {
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: space-between;
152
+ gap: 0.75rem;
153
+ margin-top: 0.75rem;
154
+ padding: 0.55rem 0.85rem;
155
+ border: 1px solid var(--howdoc-rule);
156
+ border-radius: 8px;
157
+ color: var(--howdoc-fg);
158
+ font-size: 0.95rem;
159
+ font-weight: 600;
160
+ cursor: pointer;
161
+ list-style: none;
162
+ }
163
+
164
+ .howdoc-nav-summary::-webkit-details-marker {
165
+ display: none;
166
+ }
167
+
168
+ /* The chevron that says the line can be tapped: down while the list is folded,
169
+ up while it is not. */
170
+ .howdoc-nav-summary::after {
171
+ content: '';
172
+ width: 0.45rem;
173
+ height: 0.45rem;
174
+ border-right: 2px solid var(--howdoc-muted);
175
+ border-bottom: 2px solid var(--howdoc-muted);
176
+ transform: translateY(-0.15rem) rotate(45deg);
177
+ }
178
+
179
+ .howdoc-nav-guides[open] > .howdoc-nav-summary::after {
180
+ transform: translateY(0.15rem) rotate(-135deg);
181
+ }
182
+
112
183
  @media (min-width: 60rem) {
113
184
  body.howdoc {
114
185
  display: grid;
@@ -116,15 +187,39 @@ body.howdoc {
116
187
  align-items: start;
117
188
  }
118
189
 
119
- .howdoc-nav {
190
+ .howdoc-nav,
191
+ .howdoc-nav:has(.howdoc-nav-guides[open]) {
120
192
  position: sticky;
121
193
  top: 0;
194
+ z-index: auto;
122
195
  max-height: 100vh;
123
196
  overflow-y: auto;
124
197
  padding: 2rem 1.25rem 3rem;
125
198
  border-right: 1px solid var(--howdoc-rule);
126
199
  border-bottom: 0;
127
200
  }
201
+
202
+ .howdoc-nav-index {
203
+ order: 0;
204
+ }
205
+
206
+ /* Beside the page there is room for the whole list, so the fold is undone:
207
+ the line to tap goes away and every guide is shown whether the reader ever
208
+ tapped it or not. Two rules for two kinds of browser -- the older kind
209
+ folds content away by not displaying it, the newer kind through
210
+ ::details-content. */
211
+ .howdoc-nav-summary {
212
+ display: none;
213
+ }
214
+
215
+ .howdoc-nav-guides > *:not(summary) {
216
+ display: block;
217
+ }
218
+
219
+ .howdoc-nav-guides::details-content {
220
+ content-visibility: visible;
221
+ block-size: auto;
222
+ }
128
223
  }
129
224
 
130
225
  .howdoc-nav-head {
@@ -2,7 +2,7 @@
2
2
  -# no other guide exists yet -- so a guide leaves the empty <nav> below and the
3
3
  -# index pass fills it in once every guide is on disk. The index renders it
4
4
  -# directly, because by then it knows.
5
- %nav.howdoc-nav{ data: { howdoc_nav: true } }
5
+ %nav.howdoc-nav{ class: ('howdoc-nav-index' if @current.nil?), data: { howdoc_nav: true } }
6
6
  .howdoc-nav-head
7
7
  %a.howdoc-nav-home{ href: './index.html' }= Howdoc::Narrator.phrase(:index, :title, locale: @locale)
8
8
  - if @languages.size > 1
@@ -13,13 +13,20 @@
13
13
  %a{ href: language[:href], lang: language[:code] }= language[:label]
14
14
  - else
15
15
  %span{ lang: language[:code] }= language[:label]
16
- %ul.howdoc-toc
17
- - previous_group = nil
18
- - @records.each do |record|
19
- - group = @config.group_label.call(record)
20
- - if group && group != previous_group
21
- %li.howdoc-toc-group= group
22
- - previous_group = group
23
- - current = @current && record.filename == @current.filename
24
- %li.howdoc-toc-entry{ class: ('howdoc-current' if current) }
25
- %a{ href: "./#{record.filename}", 'aria-current': (current ? 'page' : nil) }= record.heading
16
+ -# On a phone a list of twenty guides fills the screen before the guide the
17
+ -# reader came to read has begun, so the list is folded away behind a line
18
+ -# they can tap. A wide screen has room for it beside the page and unfolds it
19
+ -# again in the stylesheet. The index is the one page whose menu is the page,
20
+ -# so there it is open from the start.
21
+ %details.howdoc-nav-guides{ open: @current.nil? }
22
+ %summary.howdoc-nav-summary= Howdoc::Narrator.phrase(:nav, :guides, locale: @locale)
23
+ %ul.howdoc-toc
24
+ - previous_group = nil
25
+ - @records.each do |record|
26
+ - group = @config.group_label.call(record)
27
+ - if group && group != previous_group
28
+ %li.howdoc-toc-group= group
29
+ - previous_group = group
30
+ - current = @current && record.filename == @current.filename
31
+ %li.howdoc-toc-entry{ class: ('howdoc-current' if current) }
32
+ %a{ href: "./#{record.filename}", 'aria-current': (current ? 'page' : nil) }= record.heading
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: howdoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Priit Tark
@@ -59,6 +59,8 @@ files:
59
59
  - lib/howdoc/configuration.rb
60
60
  - lib/howdoc/document.rb
61
61
  - lib/howdoc/edition.rb
62
+ - lib/howdoc/marker.js
63
+ - lib/howdoc/marker.rb
62
64
  - lib/howdoc/minitest.rb
63
65
  - lib/howdoc/narrator.rb
64
66
  - lib/howdoc/navigation.rb