todidnt 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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>