saga 0.14.0 → 0.15

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: beaae8ecc51e2ef96e5343ffc064ab3290f490f722e44c68fa3a766986161371
4
- data.tar.gz: 9ccabb4c4e6ea3563d893bfe04054aebfd954fd26e825415bb56735003d64f08
3
+ metadata.gz: 2cafeea9d1c74e5312fb8cd3909e06492a9f2d6b0e4d25554aaf196bfc940aff
4
+ data.tar.gz: a59f8478840bc9e48d99d32d76847f570f440893759c3ba95b746a46bb964a3a
5
5
  SHA512:
6
- metadata.gz: add20ee8c3436a11c6141ec3e9e9ed82c9a16f4965894bb044633b5846cc8df4fe047f6b81fe12aad6ef167a65426b1a8f98c643e0a23a8110adb5bbbcf6bd9a
7
- data.tar.gz: 0772c7c89c5cd0ae6f1f6b1bc1aad3e3a48e44163fed93d6104c5e1ecb5e3d42ea052674bcec8c5ecf87e034217aad2e72f5a4afc301d1f38ef0ae66cd7cf23a
6
+ metadata.gz: ce5e4ccb1fae09c89f12bf82737f4917f1decdf109a93d739f793d28778f8b5997748d19eae7c34e147619702fe50a863f3b84ad42dbae4effb1ea880fcb3be3
7
+ data.tar.gz: b885628cd6d2f61c4a9a778a487ff6e1fef038f5e33ba2fbaf2e7fea9cbb02a962aea2bea12d8f88329689e69de429bd190fe38e74a0bbaf42ad9e29c4956304
data/lib/saga/planning.rb CHANGED
@@ -48,6 +48,16 @@ module Saga
48
48
  range_estimated
49
49
  end
50
50
 
51
+ def relative_estimated
52
+ relative_estimated = 0
53
+ @document.stories_as_flat_list.each do |story|
54
+ if story[:estimate] && story[:estimate][1] == :relative
55
+ relative_estimated += 1
56
+ end
57
+ end
58
+ relative_estimated
59
+ end
60
+
51
61
  def statusses
52
62
  statusses = {}
53
63
  @document.stories_as_flat_list.each do |story|
@@ -75,6 +85,9 @@ module Saga
75
85
  parts << ''
76
86
  parts << self.class.format_unestimated(unestimated) if unestimated > 0
77
87
  parts << self.class.format_range_estimated(range_estimated) if range_estimated > 0
88
+ if relative_estimated > 0
89
+ parts << self.class.format_relative_estimated(relative_estimated)
90
+ end
78
91
  parts << self.class.format_statusses(statusses) unless statusses.empty?
79
92
  end
80
93
  parts.shift if parts[0] == ''
@@ -92,6 +105,8 @@ module Saga
92
105
  estimate[0] * 40
93
106
  when :range
94
107
  0
108
+ when :relative
109
+ 0
95
110
  else
96
111
  estimate[0]
97
112
  end
@@ -115,6 +130,10 @@ module Saga
115
130
  "Range-estimate: #{format_stories_count(range_estimated)}"
116
131
  end
117
132
 
133
+ def self.format_relative_estimated(relative_estimated)
134
+ "Relative : #{format_stories_count(relative_estimated)}"
135
+ end
136
+
118
137
  def self.format_stories_count(count)
119
138
  count > 1 ? "#{count} stories" : 'one story'
120
139
  end
@@ -54,6 +54,7 @@ module Saga
54
54
 
55
55
  RE_STORY_NUMBER = /\#(\d+)/.freeze
56
56
  RE_STORY_ITERATION = /i(\d+)/.freeze
57
+ RE_STORY_RELATIVE_ESTIMATE = /(trivial|easy|simple|straightforward|hard|epic)/.freeze
57
58
  RE_STORY_ESTIMATE_PART = /(\d+)(d|w|h|)/.freeze
58
59
 
59
60
  def self.tokenize_story_attributes(input)
@@ -70,6 +71,8 @@ module Saga
70
71
  attributes[:id] = match[1].to_i
71
72
  elsif match = RE_STORY_ITERATION.match(part)
72
73
  attributes[:iteration] = match[1].to_i
74
+ elsif match = RE_STORY_RELATIVE_ESTIMATE.match(part)
75
+ attributes[:estimate] = [match[1], :relative]
73
76
  elsif match = /#{RE_STORY_ESTIMATE_PART}-#{RE_STORY_ESTIMATE_PART}/.match(part)
74
77
  estimate = "#{match[1, 2].join}-#{match[3, 2].join}"
75
78
  attributes[:estimate] = [estimate, :range]
data/lib/saga/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Saga
4
- VERSION = '0.14.0'.freeze
4
+ VERSION = '0.15'.freeze
5
5
  end
@@ -4,250 +4,219 @@
4
4
  <meta charset="utf-8">
5
5
  <title>Requirements <%= title %></title>
6
6
  <style>
7
- a
7
+ :root
8
8
  {
9
- text-decoration: none;
9
+ color-scheme: light dark;
10
+
11
+ --primary: hsl(232 0% 0%);
12
+ --background: white;
13
+ --done-background: hsl(232 90% 95%);
14
+ --dropped: hsl(232 0% 60%);
15
+ --target-background: hsl(40 90% 95%);
16
+ --border: hsl(232 0% 90%);
17
+ --link: hsl(232 80% 60%);
18
+ --link-active: hsl(232 80% 40%);
19
+
20
+ --inset-left: max(env(safe-area-inset-left), 1rem, 2.5vw);
21
+ --inset-right: max(env(safe-area-inset-right), 1rem, 2.5vw);
22
+ --inset-top: max(env(safe-area-inset-top), 1rem);
23
+ --inset-bottom: max(env(safe-area-inset-bottom), 1rem);
10
24
  }
11
- a:hover,
12
- a:active
25
+ @media (prefers-color-scheme: dark)
13
26
  {
14
- text-decoration: underline;
27
+ :root
28
+ {
29
+ --primary: hsl(232 0% 90%);
30
+ --background: hsl(232 0% 10%);
31
+ --done-background: hsl(232 90% 10%);
32
+ --dropped: hsl(232 0% 40%);
33
+ --target-background: hsl(40 90% 10%);
34
+ --border: hsl(232 0% 20%);
35
+ --link: hsl(232 80% 70%);
36
+ --link-active: hsl(232 80% 60%);
37
+ }
15
38
  }
16
39
  body
17
40
  {
18
- margin: 1.5em 1em;
19
41
  font-family: palatino, georgia, serif;
20
42
  line-height: 1.25;
43
+ -webkit-text-size-adjust: none;
44
+ text-size-adjust: none;
45
+ margin: var(--inset-top) var(--inset-right) var(--inset-bottom) var(--inset-left);
46
+ background: var(--background);
47
+ color: var(--primary);
21
48
  }
49
+ p,
22
50
  h1,
23
51
  h2,
24
- h3
52
+ h3,
53
+ ul,
54
+ li,
55
+ dl,
56
+ dt,
57
+ dd
25
58
  {
26
59
  margin: 0;
27
- font-size: 150%;
28
- line-height: 1;
60
+ padding: 0
61
+ }
62
+ h1,
63
+ h2,
64
+ h3
65
+ {
29
66
  font-weight: normal;
30
67
  }
68
+ h1
69
+ {
70
+ font-size: 162.5%;
71
+ }
31
72
  h2
32
73
  {
33
- margin: 1em 0;
74
+ font-size: 137.5%;
34
75
  }
35
76
  h3
36
77
  {
37
- margin: 0.75em 0 0.25em 0;
38
- font-size: 125%;
78
+ font-size: 118.75%;
39
79
  }
40
- table + h2,
41
- dl + h2,
42
- table + h3
80
+ h2,
81
+ h3
43
82
  {
44
- margin-top: 2em;
83
+ margin: 2rem 0 1.25rem 0;
45
84
  }
46
- p,
47
85
  ul
48
86
  {
49
- margin: 1em 0;
50
- }
51
- ul.authors
52
- {
53
- padding: 0;
54
- font-style: italic;
87
+ margin: 1rem 0;
55
88
  list-style-type: none;
56
- color: #444;
89
+ font-style: italic;
57
90
  }
58
- table
91
+ dl
59
92
  {
60
- margin: 1em -1em;
61
- border-collapse: collapse;
62
- empty-cells: show;
63
- border-bottom: 1px solid #ddd;
64
- margin-top: -1em;
93
+ display: grid;
94
+ grid-template-columns: auto 1fr;
95
+ gap: 0.5rem 1rem;
96
+ margin: 1rem 0;
65
97
  }
66
- tbody tr:not(.notes)
98
+ dt:after
67
99
  {
68
- border-top: 1px solid #ddd;
100
+ content: ':';
69
101
  }
70
- tbody tr.nested:not(.notes)
102
+ a[href]
71
103
  {
72
- border-top: 1px dashed #ddd;
104
+ color: var(--link);
73
105
  }
74
- tr.notes
106
+ a[href]:active
75
107
  {
76
- color: #444;
77
- font-style: italic;
108
+ color: var(--link-active);
78
109
  }
79
- th,
80
- td
110
+ div.stories
81
111
  {
82
- padding: .5em 0.25em;
83
- vertical-align: top;
84
- text-align: left;
112
+ display: grid;
113
+ grid-template-columns: fit-content(0) 1fr repeat(4, fit-content(0));
114
+ margin: 1rem calc(-1 * var(--inset-right)) 1rem calc(-1 * var(--inset-left));
85
115
  }
86
- th:first-child,
87
- td:first-child
116
+ h3,
117
+ div.stories > div.story
88
118
  {
89
- padding-left: 1em;
119
+ grid-column-start: 1;
120
+ grid-column-end: 6;
90
121
  }
91
- tr.nested th:first-child,
92
- tr.nested td:first-child
122
+ div.stories > h3
93
123
  {
94
- padding-left: 3em;
124
+ margin: 1.5rem var(--inset-right) 0.75rem var(--inset-left);
95
125
  }
96
- th:last-child,
97
- td:last-child
126
+ div.stories > h3:first-child
98
127
  {
99
- padding-right: 1em;
128
+ margin-top: 0;
100
129
  }
101
- th
130
+ div.stories > div.story
102
131
  {
103
- text-align: right;
132
+ display: grid;
133
+ grid-template-columns: subgrid;
134
+ border-top: 1px solid var(--border);
135
+ padding: 0.5rem;
104
136
  }
105
- tr.notes td
137
+ div.stories > div.story:not(:has(+ div.story))
106
138
  {
107
- padding-top: 0;
139
+ border-bottom: 1px solid var(--border);
108
140
  }
109
- td.story
141
+ div.story.done
110
142
  {
111
- width: 100%;
143
+ background: var(--done-background);
112
144
  }
113
- th.meta
145
+ div.story.dropped
114
146
  {
115
- cursor: help;
147
+ color: var(--dropped);
116
148
  }
117
- td.meta
149
+ div.story.dropped > div.content
118
150
  {
119
- text-align: right !important;
120
- white-space: nowrap;
151
+ text-decoration: line-through;
121
152
  }
122
- .dropped
153
+ div.story:target
123
154
  {
124
- color: #888;
125
- text-decoration: line-through;
155
+ background: var(--target-background);
126
156
  }
127
- .done
157
+ div.story > *
128
158
  {
129
- background-color: #f0f8ff;
159
+ padding: 0 0.5rem;
130
160
  }
131
- td.id
161
+ div.story > *:first-child
132
162
  {
133
- text-align: right !important;
163
+ padding-left: calc(var(--inset-right) - 0.5rem);
134
164
  }
135
- td.id a
165
+ div.story > *:last-child
136
166
  {
137
- color: #000;
167
+ padding-right: calc(var(--inset-left) - 0.5rem);
138
168
  }
139
- dl
169
+ div.story > a.id
140
170
  {
141
- margin: 1em -1em;
171
+ grid-column: 1;
172
+ color: inherit;
173
+ text-decoration: none;
142
174
  }
143
- dt,
144
- dd
175
+ div.story > div.content
145
176
  {
146
- display: block;
177
+ grid-column: 2;
147
178
  }
148
- dt
179
+ div.story > div.estimate
149
180
  {
150
- float: left;
151
- clear: both;
152
- margin: 0;
153
- padding: 0 0 0 1em;
154
- font-weight: bold;
181
+ grid-column: 3;
155
182
  }
156
- dt:after
183
+ div.story > div.iteration
157
184
  {
158
- content: ": ";
185
+ grid-column: 4;
186
+ font-variant-numeric: tabular-nums;
159
187
  }
160
- dd
188
+ div.story > div.status
161
189
  {
162
- margin: 0 0 1em 0;
163
- padding: 0 1em;
164
- display: block;
190
+ grid-column: 5;
165
191
  }
166
- @media (min-width: 40em) {
167
- body
168
- {
169
- margin: 2em;
170
- }
171
- table,
172
- dl
192
+ @media (max-width: 25rem) {
193
+ div.story > div.content
173
194
  {
174
- margin-left: -2em;
175
- margin-right: -2em;
195
+ grid-column: 2 / span 4;
176
196
  }
177
- th:first-child,
178
- td:first-child
197
+ div.story > div.estimate,
198
+ div.story > div.iteration,
199
+ div.story > div.status
179
200
  {
180
- padding-left: 2em;
181
- }
182
- th:last-child,
183
- td:last-child
184
- {
185
- padding-right: 2em;
186
- }
187
- tr.nested th:first-child,
188
- tr.nested td:first-child
189
- {
190
- padding-left: 4em;
191
- }
192
- dl
193
- {
194
- border-bottom: 1px solid #ddd;
195
- }
196
- dt
197
- {
198
- padding: .5em 0 0 2em;
199
- border-top: none;
200
- }
201
- dd
202
- {
203
- margin: 0;
204
- padding: .5em 2em .5em 20em;
205
- border-top: 1px solid #ddd;
201
+ padding-top: 0.25rem;
206
202
  }
207
203
  }
208
- @media print
204
+ div.content p:not(:first-of-type)
209
205
  {
210
- body
211
- {
212
- margin: 0;
213
- font-size: 10pt;
214
- }
215
- table,
216
- dl
217
- {
218
- margin-left: 0;
219
- margin-right: 0;
220
- }
221
- th:first-child,
222
- td:first-child
223
- {
224
- padding-left: 0;
225
- }
226
- tr.nested th:first-child,
227
- tr.nested td:first-child
228
- {
229
- padding-left: 2em;
230
- }
231
- th:last-child,
232
- td:last-child
233
- {
234
- padding-right: 0;
235
- }
236
- dl
237
- {
238
- border-bottom: 1px solid #ddd;
239
- }
240
- dt
241
- {
242
- padding: .5em 0 0 0;
243
- border-top: none;
244
- }
245
- dd
246
- {
247
- margin: 0;
248
- padding: .5em 0 .5em 40%;
249
- border-top: 1px solid #ddd;
250
- }
206
+ font-style: italic;
207
+ margin-top: 0.25rem;
208
+ }
209
+ div.story.nested div.content
210
+ {
211
+ padding-left: 2rem;
212
+ }
213
+ a.id,
214
+ div.estimate:not(.relative),
215
+ div.iteration
216
+ {
217
+ text-align: right;
218
+ white-space: nowrap;
219
+ font-variant-numeric: tabular-nums;
251
220
  }
252
221
  </style>
253
222
  </head>
@@ -256,67 +225,58 @@ dd
256
225
  <h1>Requirements <%= title %></h1>
257
226
 
258
227
  <% unless authors.empty? %>
259
- <ul class="authors">
228
+ <ul>
260
229
  <% authors.each do |author| %>
261
- <li><%= format_author(author) %></li>
230
+ <li><%= author(author) %></li>
262
231
  <% end %>
263
232
  </ul>
264
233
  <% end %>
265
234
 
266
235
  <% introduction.each do |paragraph| %>
267
- <p><%=h paragraph %></p>
236
+ <p><%= h paragraph %></p>
268
237
  <% end %>
269
238
 
239
+ <h2>User stories</h2>
240
+
270
241
  <% unless stories.empty? %>
271
- <h2>User stories</h2>
272
- <% stories.each do |header, stories| %>
273
- <% unless header.strip == '' %>
274
- <h3><%=h header %></h3>
275
- <% end %>
276
- <table>
277
- <thead>
278
- <tr>
279
- <th></th>
280
- <th class="meta" title="id">#</th>
281
- <th class="meta" title="Estimate">e</th>
282
- <th class="meta" title="Iteration">i</th>
283
- <th class="meta" title="Status">s</th>
284
- </tr>
285
- </thead>
286
- <tbody>
287
- <% stories.each do |story| %>
288
- <tr class="<%= story[:status] %>" id="story<%= story[:id] %>">
289
- <td class="story"><%=h story[:description] %></td>
290
- <td class="meta id"><%=h story[:id] %></td>
291
- <td class="meta estimate"><%= format_estimate(*story[:estimate]) if story[:estimate] %></td>
292
- <td class="meta iteration"><%=h story[:iteration] %></td>
293
- <td class="meta status"><%=h story[:status] %></td>
294
- </tr>
295
- <% if story[:notes] %>
296
- <tr class="notes <%= story[:status] %>">
297
- <td colspan="5"><%=h story[:notes] %></td>
298
- </tr>
299
- <% end %>
300
- <% if story[:stories] %>
301
- <% story[:stories].each do |nested| %>
302
- <tr class="nested <%= nested[:status] %>" id="story<%= nested[:id] %>">
303
- <td class="story"><%=h nested[:description] %></td>
304
- <td class="meta id"><%=h nested[:id] %></td>
305
- <td class="meta estimate"><%= format_estimate(*nested[:estimate]) if nested[:estimate] %></td>
306
- <td class="meta iteration"><%=h nested[:iteration] %></td>
307
- <td class="meta status"><%=h nested[:status] %></td>
308
- </tr>
309
- <% if nested[:notes] %>
310
- <tr class="nested notes <%= nested[:status] %>">
311
- <td colspan="5"><%=h nested[:notes] %></td>
312
- </tr>
313
- <% end %>
242
+ <div class="stories">
243
+ <% stories.each do |header, stories| %>
244
+ <% unless header.strip == '' %>
245
+ <h3><%= h header %></h3>
246
+ <% end %>
247
+
248
+ <% stories.each do |story| %>
249
+ <div class="story <%= story[:status] %>" id="<%= dom_story_id(story[:id]) %>">
250
+ <%= id(story[:id]) %>
251
+ <div class="content">
252
+ <p><%= h story[:description] %></p>
253
+ <% if story[:notes] %>
254
+ <p><%= h story[:notes] %></p>
314
255
  <% end %>
256
+ </div>
257
+ <%= estimate(*story[:estimate]) %>
258
+ <%= iteration(story[:iteration]) %>
259
+ <%= status(story[:status]) %>
260
+ </div>
261
+ <% if story[:stories] %>
262
+ <% story[:stories].each do |nested| %>
263
+ <div class="story nested <%= nested[:status] %>" id="<%= dom_story_id(nested[:id]) %>">
264
+ <%= id(nested[:id]) %>
265
+ <div class="content">
266
+ <p><%= h nested[:description] %></p>
267
+ <% if nested[:notes] %>
268
+ <p><%= h nested[:notes] %></p>
269
+ <% end %>
270
+ </div>
271
+ <%= estimate(*nested[:estimate]) %>
272
+ <%= iteration(nested[:iteration]) %>
273
+ <%= status(nested[:status]) %>
274
+ </div>
315
275
  <% end %>
316
276
  <% end %>
317
- </tbody>
318
- </table>
319
- <% end %>
277
+ <% end %>
278
+ <% end %>
279
+ </div>
320
280
  <% end %>
321
281
 
322
282
  <% definitions.each do |header, definitions| %>
@@ -325,8 +285,8 @@ dd
325
285
  <% end %>
326
286
  <dl>
327
287
  <% definitions.each do |definition| %>
328
- <dt><%=h definition[:title] %></dt>
329
- <dd><%=h definition[:definition] %></dd>
288
+ <dt><%= h definition[:title] %></dt>
289
+ <dd><%= h definition[:definition] %></dd>
330
290
  <% end %>
331
291
  </dl>
332
292
  <% end %>
@@ -1,4 +1,4 @@
1
- def format_author(author)
1
+ def author(author)
2
2
  parts = []
3
3
  parts << author[:name] if author[:name]
4
4
  parts << "<a href=\"mailto:#{author[:email]}\">#{author[:email]}</a>" if author[:email]
@@ -24,7 +24,35 @@ def format_estimate(cardinality, interval)
24
24
  pluralize(cardinality, 'day', 'days')
25
25
  when :weeks
26
26
  pluralize(cardinality, 'week', 'days')
27
+ when :relative
28
+ cardinality.gsub('straightforward', "straight\u00ADforward")
27
29
  else
28
30
  cardinality.to_s
29
31
  end
30
32
  end
33
+
34
+ def dom_story_id(id)
35
+ "story#{id}"
36
+ end
37
+
38
+ def id(id)
39
+ ["<a href=\"##{dom_story_id(id)}\" class=\"id\" title=\"ID\">#", id, '</a>'].join if id
40
+ end
41
+
42
+ def iteration(iteration)
43
+ ['<div class="iteration" title="Iteration">', iteration, '</div>'].join if iteration
44
+ end
45
+
46
+ def status(status)
47
+ ['<div class="status" title="Status">', status, '</div>'].join if status
48
+ end
49
+
50
+ def estimate(cardinality = nil, interval = nil)
51
+ if cardinality
52
+ [
53
+ "<div class=\"estimate #{interval}\" title=\"Estimate\">",
54
+ format_estimate(cardinality, interval),
55
+ '</div>'
56
+ ].join
57
+ end
58
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saga
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: '0.15'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manfred Stienstra