todidnt 0.1.0 → 0.2.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.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/Gemfile CHANGED
@@ -2,6 +2,8 @@ source 'https://rubygems.org'
2
2
  ruby '1.9.3'
3
3
 
4
4
  gem 'chronic'
5
+ gem 'launchy'
6
+ gem 'tilt'
5
7
 
6
8
  group 'test' do
7
9
  gem 'minitest'
data/Gemfile.lock CHANGED
@@ -1,16 +1,22 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
+ addressable (2.3.5)
5
+ chronic (0.10.2)
6
+ launchy (2.4.2)
7
+ addressable (~> 2.3)
4
8
  metaclass (0.0.1)
5
9
  minitest (5.0.8)
6
10
  mocha (0.14.0)
7
11
  metaclass (~> 0.0.1)
8
- trollop (2.0)
12
+ tilt (2.0.0)
9
13
 
10
14
  PLATFORMS
11
15
  ruby
12
16
 
13
17
  DEPENDENCIES
18
+ chronic
19
+ launchy
14
20
  minitest
15
21
  mocha
16
- trollop
22
+ tilt
@@ -0,0 +1,64 @@
1
+ require 'tilt'
2
+ require 'erb'
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module Todidnt
7
+ class HTMLGenerator
8
+ SOURCE_PATH = File.join(
9
+ File.dirname(File.expand_path(__FILE__)),
10
+ '../../templates'
11
+ )
12
+ DESTINATION_PATH = '.todidnt'
13
+
14
+ def self.generate_common
15
+ # Create the destination folder unless it already exists.
16
+ Dir.mkdir(DESTINATION_PATH) unless Dir.exists?(DESTINATION_PATH)
17
+
18
+ # Copy over directories (e.g. js, css) to the destination.
19
+ common_dirs = []
20
+ Dir.chdir(SOURCE_PATH) do
21
+ common_dirs = Dir.glob('*').select do |dir|
22
+ File.directory?(dir)
23
+ end
24
+ end
25
+
26
+ common_dirs.each do |dir|
27
+ FileUtils.cp_r(
28
+ source_path(dir),
29
+ destination_path,
30
+ :remove_destination => true
31
+ )
32
+ end
33
+ end
34
+
35
+ def self.generate(template, context={})
36
+ generate_common
37
+
38
+ content_template = from_template(template)
39
+ layout_template = from_template(:layout)
40
+
41
+ inner_content = content_template.render nil, context
42
+ result = layout_template.render { inner_content }
43
+
44
+ file_name = destination_path("todidnt_#{template}.html")
45
+ File.open(file_name, 'w') do |file|
46
+ file.write(result)
47
+ end
48
+
49
+ File.absolute_path(file_name)
50
+ end
51
+
52
+ def self.source_path(path=nil)
53
+ path ? "#{SOURCE_PATH}/#{path}" : SOURCE_PATH
54
+ end
55
+
56
+ def self.destination_path(path=nil)
57
+ path ? "#{DESTINATION_PATH}/#{path}" : DESTINATION_PATH
58
+ end
59
+
60
+ def self.from_template(template)
61
+ Tilt::ERBTemplate.new(source_path("#{template}.erb"))
62
+ end
63
+ end
64
+ end
@@ -1,6 +1,6 @@
1
1
  module Todidnt
2
2
  class TodoLine
3
- IGNORE = %r{assets/js|third_?party|node_modules|jquery|Binary}
3
+ IGNORE = %r{assets/js|third_?party|node_modules|jquery|Binary|vendor}
4
4
 
5
5
  attr_reader :filename, :line_number, :content, :author, :timestamp
6
6
 
@@ -28,6 +28,7 @@ module Todidnt
28
28
  options = [
29
29
  ['--line-porcelain'],
30
30
  ['-L', "#{@line_number},#{@line_number}"],
31
+ ['-w'],
31
32
  [@filename]
32
33
  ]
33
34
 
@@ -48,5 +49,15 @@ module Todidnt
48
49
  def pretty
49
50
  "#{pretty_time} (#{author}, #{filename}:#{line_number}): #{content}"
50
51
  end
52
+
53
+ def to_hash
54
+ {
55
+ :time => pretty_time,
56
+ :author => author,
57
+ :filename => filename,
58
+ :line_number => line_number,
59
+ :content => content
60
+ }
61
+ end
51
62
  end
52
63
  end
data/lib/todidnt.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  require 'todidnt/git_repo'
2
2
  require 'todidnt/git_command'
3
3
  require 'todidnt/todo_line'
4
+ require 'todidnt/html_generator'
4
5
 
5
6
  require 'chronic'
7
+ require 'launchy'
6
8
 
7
9
  module Todidnt
8
10
  class CLI
9
- VALID_COMMANDS = %w{all overdue}
11
+ VALID_COMMANDS = %w{all overdue history}
10
12
 
11
13
  def self.run(command, options)
12
14
  if command && VALID_COMMANDS.include?(command)
@@ -20,14 +22,12 @@ module Todidnt
20
22
  end
21
23
 
22
24
  def self.all(options)
23
- all_lines = self.all_lines(options)
25
+ all_lines = self.all_lines(options).sort_by(&:timestamp)
24
26
 
25
- puts "\nResults:"
26
- all_lines.sort_by do |line|
27
- line.timestamp
28
- end.each do |line|
29
- puts line.pretty
30
- end
27
+ puts "\nOpening results..."
28
+
29
+ file_path = HTMLGenerator.generate(:all, :all_lines => all_lines)
30
+ Launchy.open("file://#{file_path}")
31
31
  end
32
32
 
33
33
  def self.overdue(options)
@@ -65,5 +65,172 @@ module Todidnt
65
65
  lines
66
66
  end
67
67
  end
68
+
69
+ def self.history(options)
70
+ GitRepo.new(options[:path]).run do |path|
71
+ log = GitCommand.new(:log, [['-G', 'TODO'], ['--format="COMMIT %an %ae %at"'], ['-p'], ['-U0']])
72
+
73
+ history = []
74
+
75
+ blame_hash = {}
76
+
77
+ puts "Going through log..."
78
+ patch_additions = []
79
+ patch_deletions = []
80
+ filename = nil
81
+ total = log.output_lines.count
82
+
83
+ log.output_lines.reverse.each do |line|
84
+ if (summary = /^COMMIT (.*) (.*) (.*)/.match(line))
85
+ name = summary[1]
86
+ email = summary[2]
87
+ time = summary[3]
88
+
89
+ unless filename =~ TodoLine::IGNORE
90
+ # Put the additions in the blame hash so when someone removes we
91
+ # can tell who the original author was. Mrrrh, this isn't going to
92
+ # work if people add the same string (pretty common e.g. # TODO).
93
+ # We can figure this out later though.
94
+ patch_additions.each do |line|
95
+ blame_hash[line] ||= []
96
+ blame_hash[line] << name
97
+ end
98
+
99
+ deletions_by_author = {}
100
+ patch_deletions.each do |line|
101
+ author = blame_hash[line] && blame_hash[line].pop
102
+
103
+ if author
104
+ deletions_by_author[author] ||= 0
105
+ deletions_by_author[author] += 1
106
+ else
107
+ puts "BAD BAD can't find original author: #{line}"
108
+ end
109
+ end
110
+
111
+ history << {
112
+ :timestamp => time.to_i,
113
+ :author => name,
114
+ :additions => patch_additions.count,
115
+ :deletions => deletions_by_author[name] || 0
116
+ }
117
+
118
+ deletions_by_author.delete(name)
119
+ deletions_by_author.each do |author, deletion_count|
120
+ history << {
121
+ :timestamp => time.to_i,
122
+ :author => author,
123
+ :additions => 0,
124
+ :deletions => deletion_count
125
+ }
126
+ end
127
+ end
128
+
129
+ patch_additions = []
130
+ patch_deletions = []
131
+ elsif (diff = /diff --git a\/(.*) b\/(.*)/.match(line))
132
+ filename = diff[1]
133
+ elsif (diff = /^\+(.*TODO.*)/.match(line))
134
+ patch_additions << diff[1]
135
+ elsif (diff = /^\-(.*TODO.*)/.match(line))
136
+ patch_deletions << diff[1]
137
+ end
138
+ end
139
+
140
+ history.sort_by! {|slice| slice[:timestamp]}
141
+ min_commit_date = Time.at(history.first[:timestamp])
142
+ max_commit_date = Time.at(history.last[:timestamp])
143
+
144
+ timespan = max_commit_date - min_commit_date
145
+
146
+ # Figure out what the interval should be based on the total timespan.
147
+ if timespan > 86400 * 365 * 10 # 10+ years
148
+ interval = 86400 * 365 # years
149
+ elsif timespan > 86400 * 365 * 5 # 5-10 years
150
+ interval = 86400 * (365 / 2) # 6 months
151
+ elsif timespan > 86400 * 365 # 2-5 years
152
+ interval = 86400 * (365 / 4) # 3 months
153
+ elsif timespan > 86400 * 30 * 6 # 6 months-3 year
154
+ interval = 86400 * 30 # months
155
+ elsif timespan > 86400 * 1 # 1 month - 6 months
156
+ interval = 86400 * 7
157
+ else # 0 - 2 months
158
+ interval = 86400 # days
159
+ end
160
+
161
+ original_interval_start = Time.new(min_commit_date.year, min_commit_date.month, min_commit_date.day).to_i
162
+ interval_start = original_interval_start
163
+ interval_end = interval_start + interval
164
+
165
+ puts "Finalizing timeline..."
166
+ buckets = []
167
+ current_bucket_authors = {}
168
+ bucket_total = 0
169
+
170
+ i = 0
171
+ # Going through the entire history of +/-'s of TODOs.
172
+ while i < history.length
173
+ should_increment = false
174
+ slice = history[i]
175
+ author = slice[:author]
176
+
177
+ # Does the current slice exist inside the bucket we're currently
178
+ # in? If so, add it to the author's total and go to the next slice.
179
+ if slice[:timestamp] >= interval_start && slice[:timestamp] < interval_end
180
+ current_bucket_authors[author] ||= 0
181
+ current_bucket_authors[author] += slice[:additions] - slice[:deletions]
182
+ bucket_total += slice[:additions] - slice[:deletions]
183
+ should_increment = true
184
+ end
185
+
186
+ # If we're on the last slice, or the next slice would have been
187
+ # in a new bucket, finish the current bucket.
188
+ if i == (history.length - 1) || history[i + 1][:timestamp] >= interval_end
189
+ buckets << {
190
+ :timestamp => Time.at(interval_start).strftime('%D'),
191
+ :authors => current_bucket_authors,
192
+ :total => bucket_total
193
+ }
194
+ interval_start += interval
195
+ interval_end += interval
196
+
197
+ current_bucket_authors = current_bucket_authors.clone
198
+ end
199
+
200
+ i += 1 if should_increment
201
+ end
202
+
203
+ authors = Set.new
204
+ contains_other = false
205
+ buckets.each do |bucket|
206
+ significant_authors = {}
207
+ other_count = 0
208
+ bucket[:authors].each do |author, count|
209
+ # Only include the author if they account for more than > 3% of
210
+ # the TODOs in this bucket.
211
+ if count > bucket[:total] * 0.03
212
+ significant_authors[author] = count
213
+ authors << author
214
+ else
215
+ other_count += count
216
+ end
217
+ end
218
+
219
+ if other_count > 0
220
+ significant_authors['Other'] = other_count
221
+ contains_other = true
222
+ end
223
+
224
+ bucket[:authors] = significant_authors
225
+ end
226
+
227
+ if contains_other
228
+ authors << 'Other'
229
+ end
230
+
231
+ file_path = HTMLGenerator.generate(:history, :data => {:history => buckets.map {|h| h[:authors].merge('Date' => h[:timestamp]) }, :authors => authors.to_a})
232
+ Launchy.open("file://#{file_path}")
233
+ end
234
+ end
68
235
  end
69
236
  end
data/templates/all.erb ADDED
@@ -0,0 +1,161 @@
1
+ <div id='all'></div>
2
+
3
+ <script type="text/jsx">
4
+ /** @jsx React.DOM */
5
+
6
+ var lines = <%= all_lines.map(&:to_hash).to_json %>;
7
+
8
+ var FilterableTODOList = React.createClass({
9
+ getInitialState: function() {
10
+ return {
11
+ filters: {}
12
+ };
13
+ },
14
+
15
+ handleFilterChange: function(name, value) {
16
+ newFilters = this.state.filters;
17
+ if (value === '') {
18
+ delete newFilters[name];
19
+ } else {
20
+ newFilters[name] = value;
21
+ }
22
+
23
+ this.setState({
24
+ filters: newFilters
25
+ });
26
+ },
27
+
28
+ render: function() {
29
+ return (
30
+ <div className='filterable-todo-list'>
31
+ <TODOFilters lines={this.props.lines} filters={this.state.filters} handleFilterChange={this.handleFilterChange} />
32
+ <TODOLineList lines={this.props.lines} filters={this.state.filters} />
33
+ </div>
34
+ );
35
+ }
36
+ });
37
+
38
+ var TODOFilters = React.createClass({
39
+ authors: function() {
40
+ return _.uniq(
41
+ _.map(this.props.lines, function(line) { return line.author; })
42
+ );
43
+ },
44
+
45
+ render: function() {
46
+ var authorFilter = <TODOSelectFilter name='author' options={this.authors()} handleFilterChange={this.props.handleFilterChange} />;
47
+
48
+ return (
49
+ <div className='controls'>
50
+ <div className='filters'>
51
+ Filter by:
52
+ {authorFilter}
53
+ </div>
54
+ </div>
55
+ );
56
+ }
57
+ });
58
+
59
+ var TODOSelectFilter = React.createClass({
60
+ selectElement: function() {
61
+ return this.refs[this.props.name + 'Select'];
62
+ },
63
+
64
+ handleSelect: function() {
65
+ value = this.selectElement().getDOMNode().value;
66
+ this.props.handleFilterChange(this.props.name, value);
67
+ },
68
+
69
+ componentDidMount: function() {
70
+ el = $(this.selectElement().getDOMNode());
71
+ el.chosen().change(this.handleSelect);
72
+ },
73
+
74
+ render: function() {
75
+ selectOptions = [];
76
+ this.props.options.forEach(function(option) {
77
+ selectOptions.push(<option>{option}</option>);
78
+ });
79
+
80
+ return (
81
+ <div className='filter'>
82
+ <select data-placeholder={this.props.name} id={this.props.name} ref={this.props.name + 'Select'}>
83
+ <option value='' selected>(All authors)</option>
84
+ {selectOptions}
85
+ </select>
86
+ </div>
87
+ );
88
+ }
89
+ });
90
+
91
+ var TODOLineList = React.createClass({
92
+ noFilters: function() {
93
+ return _.isEmpty(this.props.filters);
94
+ },
95
+
96
+ includeLine: function(line) {
97
+ filters = this.props.filters;
98
+
99
+ if (this.noFilters()) {
100
+ return true;
101
+ }
102
+
103
+ // If there are any filters, make sure the line fits all of them.
104
+ return _.every(Object.keys(filters), function(filter) {
105
+ return line[filter] === filters[filter];
106
+ });
107
+ },
108
+
109
+ render: function() {
110
+ var lines = [];
111
+ this.props.lines.forEach(function(line) {
112
+ if (this.includeLine(line)) {
113
+ lines.push(<TODOLine line={line} />);
114
+ }
115
+ }.bind(this));
116
+
117
+ var countMessage = "";
118
+ if (this.noFilters()) {
119
+ countMessage = 'There are ' + lines.length + ' TODOs total!';
120
+ } else {
121
+ countMessage = 'There are ' + lines.length + ' TODOs, out of ' + this.props.lines.length + ' total!';
122
+ }
123
+
124
+ return (
125
+ <div className='lines'>
126
+ {countMessage}
127
+ {lines}
128
+ </div>
129
+ );
130
+ }
131
+ });
132
+
133
+ var TODOLine = React.createClass({
134
+ render: function() {
135
+ return (
136
+ <div className='line'>
137
+ <div className='meta'>
138
+ <div className='padding'>
139
+ <span className='date'>{this.props.line.time}</span>
140
+ <span className='author'>{this.props.line.author}</span>
141
+ <span className='file'>
142
+ <a href={'../' + this.props.line.filename}>
143
+ {this.props.line.filename}:{this.props.line.line_number}
144
+ </a>
145
+ </span>
146
+ </div>
147
+ </div>
148
+ <div className='content'>
149
+ <div className='padding'>
150
+ <span>{this.props.line.content}</span>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ );
155
+ }
156
+ });
157
+
158
+ $(document).ready(function() {
159
+ React.renderComponent(<FilterableTODOList lines={lines} />, document.getElementById('all'));
160
+ });
161
+ </script>
@@ -0,0 +1,3 @@
1
+ /* Chosen v1.1.0 | (c) 2011-2013 by Harvest | MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md */
2
+
3
+ .chosen-container{position:relative;display:inline-block;vertical-align:middle;font-size:13px;zoom:1;*display:inline;-webkit-user-select:none;-moz-user-select:none;user-select:none}.chosen-container .chosen-drop{position:absolute;top:100%;left:-9999px;z-index:1010;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;width:100%;border:1px solid #aaa;border-top:0;background:#fff;box-shadow:0 4px 5px rgba(0,0,0,.15)}.chosen-container.chosen-with-drop .chosen-drop{left:0}.chosen-container a{cursor:pointer}.chosen-container-single .chosen-single{position:relative;display:block;overflow:hidden;padding:0 0 0 8px;height:23px;border:1px solid #aaa;border-radius:5px;background-color:#fff;background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#fff),color-stop(50%,#f6f6f6),color-stop(52%,#eee),color-stop(100%,#f4f4f4));background:-webkit-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:-moz-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:-o-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-clip:padding-box;box-shadow:0 0 3px #fff inset,0 1px 1px rgba(0,0,0,.1);color:#444;text-decoration:none;white-space:nowrap;line-height:24px}.chosen-container-single .chosen-default{color:#999}.chosen-container-single .chosen-single span{display:block;overflow:hidden;margin-right:26px;text-overflow:ellipsis;white-space:nowrap}.chosen-container-single .chosen-single-with-deselect span{margin-right:38px}.chosen-container-single .chosen-single abbr{position:absolute;top:6px;right:26px;display:block;width:12px;height:12px;background:url(chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-single .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single.chosen-disabled .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single .chosen-single div{position:absolute;top:0;right:0;display:block;width:18px;height:100%}.chosen-container-single .chosen-single div b{display:block;width:100%;height:100%;background:url(chosen-sprite.png) no-repeat 0 2px}.chosen-container-single .chosen-search{position:relative;z-index:1010;margin:0;padding:3px 4px;white-space:nowrap}.chosen-container-single .chosen-search input[type=text]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;margin:1px 0;padding:4px 20px 4px 5px;width:100%;height:auto;outline:0;border:1px solid #aaa;background:#fff url(chosen-sprite.png) no-repeat 100% -20px;background:url(chosen-sprite.png) no-repeat 100% -20px;font-size:1em;font-family:sans-serif;line-height:normal;border-radius:0}.chosen-container-single .chosen-drop{margin-top:-1px;border-radius:0 0 4px 4px;background-clip:padding-box}.chosen-container-single.chosen-container-single-nosearch .chosen-search{position:absolute;left:-9999px}.chosen-container .chosen-results{position:relative;overflow-x:hidden;overflow-y:auto;margin:0 4px 4px 0;padding:0 0 0 4px;max-height:240px;-webkit-overflow-scrolling:touch}.chosen-container .chosen-results li{display:none;margin:0;padding:5px 6px;list-style:none;line-height:15px;-webkit-touch-callout:none}.chosen-container .chosen-results li.active-result{display:list-item;cursor:pointer}.chosen-container .chosen-results li.disabled-result{display:list-item;color:#ccc;cursor:default}.chosen-container .chosen-results li.highlighted{background-color:#3875d7;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#3875d7),color-stop(90%,#2a62bc));background-image:-webkit-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:-moz-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:-o-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:linear-gradient(#3875d7 20%,#2a62bc 90%);color:#fff}.chosen-container .chosen-results li.no-results{display:list-item;background:#f4f4f4}.chosen-container .chosen-results li.group-result{display:list-item;font-weight:700;cursor:default}.chosen-container .chosen-results li.group-option{padding-left:15px}.chosen-container .chosen-results li em{font-style:normal;text-decoration:underline}.chosen-container-multi .chosen-choices{position:relative;overflow:hidden;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;width:100%;height:auto!important;height:1%;border:1px solid #aaa;background-color:#fff;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(1%,#eee),color-stop(15%,#fff));background-image:-webkit-linear-gradient(#eee 1%,#fff 15%);background-image:-moz-linear-gradient(#eee 1%,#fff 15%);background-image:-o-linear-gradient(#eee 1%,#fff 15%);background-image:linear-gradient(#eee 1%,#fff 15%);cursor:text}.chosen-container-multi .chosen-choices li{float:left;list-style:none}.chosen-container-multi .chosen-choices li.search-field{margin:0;padding:0;white-space:nowrap}.chosen-container-multi .chosen-choices li.search-field input[type=text]{margin:1px 0;padding:5px;height:15px;outline:0;border:0!important;background:transparent!important;box-shadow:none;color:#666;font-size:100%;font-family:sans-serif;line-height:normal;border-radius:0}.chosen-container-multi .chosen-choices li.search-field .default{color:#999}.chosen-container-multi .chosen-choices li.search-choice{position:relative;margin:3px 0 3px 5px;padding:3px 20px 3px 5px;border:1px solid #aaa;border-radius:3px;background-color:#e4e4e4;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),color-stop(100%,#eee));background-image:-webkit-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-moz-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-o-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-clip:padding-box;box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);color:#333;line-height:13px;cursor:default}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close{position:absolute;top:4px;right:3px;display:block;width:12px;height:12px;background:url(chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover{background-position:-42px -10px}.chosen-container-multi .chosen-choices li.search-choice-disabled{padding-right:5px;border:1px solid #ccc;background-color:#e4e4e4;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),color-stop(100%,#eee));background-image:-webkit-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-moz-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-o-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);color:#666}.chosen-container-multi .chosen-choices li.search-choice-focus{background:#d4d4d4}.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close{background-position:-42px -10px}.chosen-container-multi .chosen-results{margin:0;padding:0}.chosen-container-multi .chosen-drop .result-selected{display:list-item;color:#ccc;cursor:default}.chosen-container-active .chosen-single{border:1px solid #5897fb;box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active.chosen-with-drop .chosen-single{border:1px solid #aaa;-moz-border-radius-bottomright:0;border-bottom-right-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#eee),color-stop(80%,#fff));background-image:-webkit-linear-gradient(#eee 20%,#fff 80%);background-image:-moz-linear-gradient(#eee 20%,#fff 80%);background-image:-o-linear-gradient(#eee 20%,#fff 80%);background-image:linear-gradient(#eee 20%,#fff 80%);box-shadow:0 1px 0 #fff inset}.chosen-container-active.chosen-with-drop .chosen-single div{border-left:0;background:transparent}.chosen-container-active.chosen-with-drop .chosen-single div b{background-position:-18px 2px}.chosen-container-active .chosen-choices{border:1px solid #5897fb;box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active .chosen-choices li.search-field input[type=text]{color:#111!important}.chosen-disabled{opacity:.5!important;cursor:default}.chosen-disabled .chosen-single{cursor:default}.chosen-disabled .chosen-choices .search-choice .search-choice-close{cursor:default}.chosen-rtl{text-align:right}.chosen-rtl .chosen-single{overflow:visible;padding:0 8px 0 0}.chosen-rtl .chosen-single span{margin-right:0;margin-left:26px;direction:rtl}.chosen-rtl .chosen-single-with-deselect span{margin-left:38px}.chosen-rtl .chosen-single div{right:auto;left:3px}.chosen-rtl .chosen-single abbr{right:auto;left:26px}.chosen-rtl .chosen-choices li{float:right}.chosen-rtl .chosen-choices li.search-field input[type=text]{direction:rtl}.chosen-rtl .chosen-choices li.search-choice{margin:3px 5px 3px 0;padding:3px 5px 3px 19px}.chosen-rtl .chosen-choices li.search-choice .search-choice-close{right:auto;left:4px}.chosen-rtl.chosen-container-single-nosearch .chosen-search,.chosen-rtl .chosen-drop{left:9999px}.chosen-rtl.chosen-container-single .chosen-results{margin:0 0 4px 4px;padding:0 4px 0 0}.chosen-rtl .chosen-results li.group-option{padding-right:15px;padding-left:0}.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div{border-right:0}.chosen-rtl .chosen-search input[type=text]{padding:4px 5px 4px 20px;background:#fff url(chosen-sprite.png) no-repeat -30px -20px;background:url(chosen-sprite.png) no-repeat -30px -20px;direction:rtl}.chosen-rtl.chosen-container-single .chosen-single div b{background-position:6px 2px}.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b{background-position:-12px 2px}@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-resolution:144dpi){.chosen-rtl .chosen-search input[type=text],.chosen-container-single .chosen-single abbr,.chosen-container-single .chosen-single div b,.chosen-container-single .chosen-search input[type=text],.chosen-container-multi .chosen-choices .search-choice .search-choice-close,.chosen-container .chosen-results-scroll-down span,.chosen-container .chosen-results-scroll-up span{background-image:url(chosen-sprite@2x.png)!important;background-size:52px 37px!important;background-repeat:no-repeat!important}}
@@ -0,0 +1,129 @@
1
+ /* Global */
2
+
3
+ html, body {
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ body {
9
+ font-family: Helvetica Neue, Arial, san-serif;
10
+ font-size: 0.9em;
11
+ color: #444;
12
+
13
+ text-rendering: optimizeLegibility;
14
+ -webkit-font-smoothing: antialiased;
15
+ }
16
+
17
+ body a {
18
+ color: #5F8EB3;
19
+ font-weight: bold;
20
+ text-decoration: none;
21
+ }
22
+
23
+ body a:hover {
24
+ text-decoration: underline;
25
+ }
26
+
27
+ /* Layout */
28
+
29
+ header {
30
+ background: #333;
31
+ color: #fff;
32
+ padding: 10px 30px;
33
+ }
34
+
35
+ header h1 {
36
+ -webkit-margin-before: 0;
37
+ -webkit-margin-after: 0;
38
+ }
39
+
40
+ section.content {
41
+ padding: 30px;
42
+ }
43
+
44
+ footer {
45
+ text-align: center;
46
+ padding-bottom: 25px;
47
+ }
48
+
49
+ /* TODO Lines */
50
+
51
+ section div.controls {
52
+ padding-bottom: 20px;
53
+ }
54
+
55
+ section div.controls div.filter {
56
+ display: inline-block;
57
+ padding-left: 10px;
58
+ }
59
+
60
+ section div.controls div.filter select {
61
+ width: 150px;
62
+ }
63
+
64
+ section div.line {
65
+ width: 100%;
66
+ }
67
+
68
+ section div.line > div {
69
+ display: inline-block;
70
+ height: 38px;
71
+ line-height: 20px;
72
+
73
+ margin: 2px 0;
74
+
75
+ border-style: solid;
76
+ border-width: 1px 1px 5px;
77
+ }
78
+
79
+ section div.line div div.padding {
80
+ padding: 10px 10px;
81
+
82
+ white-space: nowrap;
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ }
86
+
87
+ section div.line div.meta {
88
+ background: #B1D2E3;
89
+ border-color: #A1C3D6;
90
+ border-radius: 2px 0px 0px 2px;
91
+
92
+ font-weight: bold;
93
+
94
+ width: 35%;
95
+ }
96
+
97
+ section div.line div.content {
98
+ background: #f6f6f6;
99
+ border-color: #eee;
100
+ border-radius: 0 2px 3px 0;
101
+
102
+ font-family: 'Monaco', serif;
103
+ font-size: 0.8em;
104
+
105
+ position: relative;
106
+ left: -5px;
107
+ z-index: -1;
108
+ padding-left: 15px;
109
+
110
+ width: 60%;
111
+ }
112
+
113
+ section div.line:hover div.meta {
114
+ background: #A1C3D6;
115
+ border-color: #95B9CC;
116
+ }
117
+
118
+ section div.line:hover div.content{
119
+ background: #eee;
120
+ border-color: #dedede;
121
+ }
122
+
123
+ section div.line div.meta span {
124
+ margin-right: 10px;
125
+ }
126
+
127
+ section div.line div.meta span:last-child {
128
+ margin-right: 0;
129
+ }
@@ -0,0 +1,5 @@
1
+ <script src='http://d3js.org/d3.v3.min.js'></script>
2
+ <script type='text/javascript'>
3
+ PRELOADED_DATA = <%= data.to_json %>;
4
+ </script>
5
+ <script src='js/todidnt-history.js'></script>