vines 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. data/README +2 -2
  2. data/Rakefile +63 -8
  3. data/bin/vines +0 -1
  4. data/conf/config.rb +16 -7
  5. data/lib/vines.rb +21 -16
  6. data/lib/vines/command/init.rb +5 -3
  7. data/lib/vines/config.rb +34 -0
  8. data/lib/vines/contact.rb +14 -0
  9. data/lib/vines/stanza.rb +26 -0
  10. data/lib/vines/stanza/iq.rb +1 -1
  11. data/lib/vines/stanza/iq/disco_info.rb +3 -0
  12. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  13. data/lib/vines/stanza/iq/roster.rb +26 -30
  14. data/lib/vines/stanza/presence.rb +0 -12
  15. data/lib/vines/stanza/presence/subscribe.rb +3 -20
  16. data/lib/vines/stanza/presence/subscribed.rb +9 -10
  17. data/lib/vines/stanza/presence/unsubscribe.rb +8 -15
  18. data/lib/vines/stanza/presence/unsubscribed.rb +8 -8
  19. data/lib/vines/storage.rb +28 -0
  20. data/lib/vines/storage/couchdb.rb +29 -0
  21. data/lib/vines/storage/local.rb +22 -0
  22. data/lib/vines/storage/redis.rb +26 -0
  23. data/lib/vines/storage/sql.rb +48 -5
  24. data/lib/vines/stream/client.rb +6 -8
  25. data/lib/vines/stream/http.rb +23 -21
  26. data/lib/vines/stream/http/auth.rb +1 -1
  27. data/lib/vines/stream/http/bind.rb +1 -1
  28. data/lib/vines/stream/http/bind_restart.rb +4 -3
  29. data/lib/vines/stream/http/ready.rb +1 -1
  30. data/lib/vines/stream/http/request.rb +94 -5
  31. data/lib/vines/stream/http/session.rb +8 -6
  32. data/lib/vines/version.rb +1 -1
  33. data/test/config_test.rb +12 -0
  34. data/test/contact_test.rb +40 -0
  35. data/test/rake_test_loader.rb +11 -3
  36. data/test/stanza/iq/private_storage_test.rb +177 -0
  37. data/test/stanza/iq/roster_test.rb +1 -1
  38. data/test/stanza/iq_test.rb +63 -0
  39. data/test/storage/couchdb_test.rb +7 -1
  40. data/test/storage/local_test.rb +8 -2
  41. data/test/storage/redis_test.rb +16 -7
  42. data/test/storage/sql_test.rb +8 -1
  43. data/test/storage/storage_tests.rb +50 -0
  44. data/test/stream/http/auth_test.rb +3 -0
  45. data/test/stream/http/ready_test.rb +3 -0
  46. data/test/stream/http/request_test.rb +86 -0
  47. data/test/stream/parser_test.rb +2 -0
  48. data/web/404.html +43 -0
  49. data/web/apple-touch-icon.png +0 -0
  50. data/web/chat/coffeescripts/chat.coffee +385 -0
  51. data/web/chat/coffeescripts/init.coffee +15 -0
  52. data/web/chat/coffeescripts/logout.coffee +5 -0
  53. data/web/chat/index.html +17 -0
  54. data/web/chat/javascripts/app.js +1 -0
  55. data/web/chat/javascripts/chat.js +436 -0
  56. data/web/chat/javascripts/init.js +21 -0
  57. data/web/chat/javascripts/logout.js +11 -0
  58. data/web/chat/stylesheets/chat.css +290 -0
  59. data/web/favicon.png +0 -0
  60. data/web/lib/coffeescripts/contact.coffee +32 -0
  61. data/web/lib/coffeescripts/layout.coffee +30 -0
  62. data/web/lib/coffeescripts/login.coffee +52 -0
  63. data/web/lib/coffeescripts/navbar.coffee +84 -0
  64. data/web/lib/coffeescripts/router.coffee +40 -0
  65. data/web/lib/coffeescripts/session.coffee +211 -0
  66. data/web/lib/images/default-user.png +0 -0
  67. data/web/lib/images/logo-large.png +0 -0
  68. data/web/lib/images/logo-small.png +0 -0
  69. data/web/lib/javascripts/base.js +9 -0
  70. data/web/lib/javascripts/contact.js +94 -0
  71. data/web/lib/javascripts/icons.js +101 -0
  72. data/web/lib/javascripts/jquery.cookie.js +91 -0
  73. data/web/lib/javascripts/jquery.js +18 -0
  74. data/web/lib/javascripts/layout.js +48 -0
  75. data/web/lib/javascripts/login.js +61 -0
  76. data/web/lib/javascripts/navbar.js +69 -0
  77. data/web/lib/javascripts/raphael.js +8 -0
  78. data/web/lib/javascripts/router.js +105 -0
  79. data/web/lib/javascripts/session.js +322 -0
  80. data/web/lib/javascripts/strophe.js +1 -0
  81. data/web/lib/stylesheets/base.css +223 -0
  82. data/web/lib/stylesheets/login.css +63 -0
  83. metadata +51 -9
@@ -0,0 +1,21 @@
1
+ $(function() {
2
+ var buttons, icon, label, nav, pages, session;
3
+ session = new Session();
4
+ nav = new NavBar(session);
5
+ nav.draw();
6
+ buttons = {
7
+ Messages: ICONS.chat,
8
+ Logout: ICONS.power
9
+ };
10
+ for (label in buttons) {
11
+ icon = buttons[label];
12
+ nav.addButton(label, icon);
13
+ }
14
+ pages = {
15
+ '/messages': new ChatPage(session),
16
+ '/logout': new LogoutPage(session),
17
+ 'default': new LoginPage(session, '/messages/')
18
+ };
19
+ new Router(pages).draw();
20
+ return nav.select($('#nav-link-messages').parent());
21
+ });
@@ -0,0 +1,11 @@
1
+ var LogoutPage;
2
+ LogoutPage = (function() {
3
+ function LogoutPage(session) {
4
+ this.session = session;
5
+ }
6
+ LogoutPage.prototype.draw = function() {
7
+ window.location.hash = '';
8
+ return window.location.reload();
9
+ };
10
+ return LogoutPage;
11
+ })();
@@ -0,0 +1,290 @@
1
+ #chat-page #container {
2
+ height: 100%;
3
+ }
4
+ #chat-page #alpha,
5
+ #chat-page #beta,
6
+ #chat-page #charlie {
7
+ height: 100%;
8
+ position: absolute;
9
+ }
10
+ #chat-page #alpha {
11
+ background: #f8f8f8;
12
+ -webkit-box-shadow: 0 0 40px rgba(0, 0, 0, 0.1) inset;
13
+ box-shadow: 0 0 40px rgba(0, 0, 0, 0.1) inset;
14
+ width: 260px;
15
+ }
16
+ #chat-page #alpha h2,
17
+ #chat-page #beta h2,
18
+ #chat-page #charlie h2 {
19
+ border-bottom: 1px solid #ddd;
20
+ font-size: 10pt;
21
+ padding-left: 10px;
22
+ position: relative;
23
+ text-shadow: 0 -1px 1px #fff;
24
+ }
25
+ #chat-page #beta {
26
+ -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.5), 0 0 40px rgba(0, 0, 0, 0.1) inset;
27
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5), 0 0 40px rgba(0, 0, 0, 0.1) inset;
28
+ width: 460px;
29
+ left: 260px;
30
+ z-index: 1;
31
+ }
32
+ #chat-page #charlie {
33
+ background: #f8f8f8;
34
+ -webkit-box-shadow: 0 0 40px rgba(0, 0, 0, 0.1) inset;
35
+ box-shadow: 0 0 40px rgba(0, 0, 0, 0.1) inset;
36
+ width: 260px;
37
+ left: 720px;
38
+ }
39
+ #chat-page #chat-title {
40
+ background: #f8f8f8;
41
+ }
42
+ #chat-page #messages {
43
+ background: #fff;
44
+ height: 100%;
45
+ list-style: none;
46
+ overflow-y: auto;
47
+ text-shadow: 0 1px 1px #ddd;
48
+ width: 100%;
49
+ }
50
+ #chat-page #messages::-webkit-scrollbar,
51
+ #chat-page #roster::-webkit-scrollbar,
52
+ #chat-page #notifications::-webkit-scrollbar {
53
+ width: 6px;
54
+ }
55
+ #chat-page #messages::-webkit-scrollbar-thumb,
56
+ #chat-page #roster::-webkit-scrollbar-thumb,
57
+ #chat-page #notifications::-webkit-scrollbar-thumb {
58
+ border-radius: 10px;
59
+ }
60
+ #chat-page #messages::-webkit-scrollbar-thumb:vertical,
61
+ #chat-page #roster::-webkit-scrollbar-thumb:vertical,
62
+ #chat-page #notifications::-webkit-scrollbar-thumb:vertical {
63
+ background: rgba(0, 0, 0, .2);
64
+ }
65
+ #chat-page #messages li {
66
+ border-bottom: 1px solid #f0f0f0;
67
+ min-height: 40px;
68
+ padding: 10px;
69
+ position: relative;
70
+ }
71
+ #chat-page #messages li:hover > span .time {
72
+ opacity: 0.3;
73
+ }
74
+ #chat-page #messages li img {
75
+ border: 3px solid #fff;
76
+ -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 40px rgba(0, 0, 0, 0.06) inset;
77
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 40px rgba(0, 0, 0, 0.06) inset;
78
+ position: absolute;
79
+ top: 7px;
80
+ right: 7px;
81
+ height: 40px;
82
+ width: 40px;
83
+ }
84
+ #chat-page #messages li p {
85
+ line-height: 1.5;
86
+ width: 90%;
87
+ }
88
+ #chat-page #messages li footer {
89
+ font-size: 9pt;
90
+ padding-right: 50px;
91
+ text-align: right;
92
+ }
93
+ #chat-page #messages li footer span {
94
+ color: #d8d8d8;
95
+ margin-right: 0.5em;
96
+ text-shadow: none;
97
+ }
98
+ #chat-page #messages li footer .author::before {
99
+ content: '\2014 ';
100
+ }
101
+ #chat-page #message-form {
102
+ background: #f8f8f8;
103
+ border-top: 1px solid #dfdfdf;
104
+ height: 50px;
105
+ position: absolute;
106
+ bottom: 0;
107
+ width: 100%;
108
+ }
109
+ #chat-page input[type="text"]:focus,
110
+ #chat-page input[type="email"]:focus {
111
+ -webkit-box-shadow: inset 1px 1px 2px rgba(0, 0, 0, 0.6);
112
+ box-shadow: inset 1px 1px 2px rgba(0, 0, 0, 0.6);
113
+ }
114
+ #chat-page #message {
115
+ display: block;
116
+ position: relative;
117
+ left: 10px;
118
+ top: 10px;
119
+ width: 428px;
120
+ }
121
+ #chat-page #roster,
122
+ #chat-page #notifications {
123
+ height: 100%;
124
+ list-style: none;
125
+ overflow-y: auto;
126
+ text-shadow: 0 1px 1px #fff;
127
+ width: 260px;
128
+ }
129
+ #chat-page #roster li,
130
+ #chat-page #notifications li {
131
+ cursor: pointer;
132
+ border-bottom: 1px solid #ddd;
133
+ font-weight: bold;
134
+ min-height: 42px;
135
+ padding: 0 10px;
136
+ position: relative;
137
+ -moz-transition: background 0.3s;
138
+ -o-transition: background 0.3s;
139
+ -webkit-transition: background 0.3s;
140
+ transition: background 0.3s;
141
+ }
142
+ #chat-page #notifications li {
143
+ font-weight: normal;
144
+ padding: 10px 0 0 0;
145
+ }
146
+ #chat-page #roster li:hover,
147
+ #chat-page #notifications li:hover {
148
+ background: rgba(255, 255, 255, 1.0);
149
+ }
150
+ #chat-page #roster li.offline > * {
151
+ opacity: 0.4;
152
+ }
153
+ #chat-page #roster li.selected > * {
154
+ opacity: 1.0;
155
+ }
156
+ #chat-page #roster li.selected {
157
+ background: #4693FF;
158
+ background: -moz-linear-gradient(#4693FF, #015de6);
159
+ background: -o-linear-gradient(#4693FF, #015de6);
160
+ background: -webkit-gradient(linear, left top, left bottom, from(#4693FF), to(#015de6));
161
+ border-bottom: 1px solid #fff;
162
+ color: #fff;
163
+ text-shadow: 0 -1px 1px #1b3a65;
164
+ }
165
+ #chat-page #roster li.selected .status-msg {
166
+ color: rgba(255, 255, 255, 0.85);
167
+ }
168
+ #chat-page #roster .status-msg {
169
+ display: block;
170
+ font-size: 11px;
171
+ font-weight: normal;
172
+ line-height: 11px;
173
+ }
174
+ #chat-page #roster .unread {
175
+ background: #4693FF;
176
+ background: -moz-linear-gradient(#4693FF, #015de6);
177
+ background: -o-linear-gradient(#4693FF, #015de6);
178
+ background: -webkit-gradient(linear, left top, left bottom, from(#4693FF), to(#015de6));
179
+ border-radius: 30px;
180
+ color: #fff;
181
+ display: inline-block;
182
+ font-size: 11px;
183
+ font-weight: normal;
184
+ line-height: 15px;
185
+ padding: 0px 6px;
186
+ position: absolute;
187
+ right: 50px;
188
+ top: 14px;
189
+ text-shadow: none;
190
+ }
191
+ #chat-page #roster .vcard-img {
192
+ background: #fff;
193
+ border: 1px solid #fff;
194
+ -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 40px rgba(0, 0, 0, 0.06) inset;
195
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 40px rgba(0, 0, 0, 0.06) inset;
196
+ height: 32px;
197
+ width: 32px;
198
+ position: absolute;
199
+ top: 4px;
200
+ right: 10px;
201
+ }
202
+ #chat-page #roster-controls,
203
+ #chat-page #notification-controls {
204
+ background: rgba(255, 255, 255, 0.05);
205
+ border-top: 1px solid #ddd;
206
+ height: 50px;
207
+ position: absolute;
208
+ bottom: 0;
209
+ width: 260px;
210
+ }
211
+ #chat-page #notification-controls {
212
+ text-align: right;
213
+ }
214
+ #chat-page #roster-controls > div,
215
+ #chat-page #notification-controls > div {
216
+ cursor: pointer;
217
+ display: inline-block;
218
+ height: 27px;
219
+ margin: 0 10px;
220
+ position: relative;
221
+ top: 10px;
222
+ width: 27px;
223
+ }
224
+ #chat-page #roster-controls > div > svg,
225
+ #chat-page #notification-controls > div > svg {
226
+ height: 27px;
227
+ width: 27px;
228
+ }
229
+ #chat-page .contact-form {
230
+ background: inherit;
231
+ border-bottom: 1px solid #ddd;
232
+ border-top: 1px solid #fff;
233
+ -webkit-box-shadow: 0px -3px 5px rgba(0, 0, 0, 0.1);
234
+ box-shadow: 0px -3px 5px rgba(0, 0, 0, 0.1);
235
+ padding-top: 10px;
236
+ position: absolute;
237
+ bottom: 50px;
238
+ left: 0;
239
+ width: 260px;
240
+ }
241
+ #chat-page .contact-form h2,
242
+ #chat-page .notify-form h2 {
243
+ border: none !important;
244
+ line-height: 1;
245
+ margin-bottom: 10px;
246
+ }
247
+ #chat-page .contact-form p,
248
+ #chat-page .notify-form p {
249
+ line-height: 1.5;
250
+ margin: 0 10px 10px 10px;
251
+ text-shadow: 0 1px 1px #fff;
252
+ }
253
+ #chat-page .notify-form p {
254
+ margin-top: -5px;
255
+ }
256
+ #chat-page .contact-form .buttons,
257
+ #chat-page .notify-form .buttons {
258
+ padding-right: 10px;
259
+ text-align: right;
260
+ }
261
+ #chat-page .contact-form input[type="text"],
262
+ #chat-page .contact-form input[type="email"] {
263
+ margin-bottom: 10px;
264
+ width: 228px;
265
+ position: relative;
266
+ left: 10px;
267
+ }
268
+ #chat-page #edit-contact-jid {
269
+ color: #444;
270
+ margin-top: -5px;
271
+ }
272
+ #chat-page #search-roster {
273
+ cursor: pointer;
274
+ display: inline-block;
275
+ line-height: 1;
276
+ position: absolute;
277
+ right: 10px;
278
+ top: 6px;
279
+ }
280
+ #chat-page #search-roster svg {
281
+ height: 16px;
282
+ width: 16px;
283
+ }
284
+ #chat-page #search-roster-form {
285
+ border-bottom: 1px solid #ddd;
286
+ padding: 5px 10px;
287
+ }
288
+ #chat-page #search-roster-text {
289
+ width: 100%;
290
+ }
Binary file
@@ -0,0 +1,32 @@
1
+ class Contact
2
+ constructor: (node) ->
3
+ node = $(node)
4
+ @jid = node.attr 'jid'
5
+ @name = node.attr 'name'
6
+ @ask = node.attr 'ask'
7
+ @subscription = node.attr 'subscription'
8
+ @groups = $('group', node).map(-> $(this).text()).get()
9
+ @presence = []
10
+
11
+ online: ->
12
+ @presence.length > 0
13
+
14
+ offline: ->
15
+ @presence.length == 0
16
+
17
+ available: ->
18
+ this.online() && (p for p in @presence when !p.away).length > 0
19
+
20
+ away: -> !this.available()
21
+
22
+ status: ->
23
+ available = (p.status for p in @presence when p.status && !p.away)[0] || 'Available'
24
+ away = (p.status for p in @presence when p.status && p.away)[0] || 'Away'
25
+ if this.offline() then 'Offline'
26
+ else if this.away() then away
27
+ else available
28
+
29
+ update: (presence) ->
30
+ @presence = (p for p in @presence when p.from != presence.from)
31
+ @presence.push presence unless presence.type
32
+ @presence = [] if presence.type == 'unsubscribed'
@@ -0,0 +1,30 @@
1
+ class Layout
2
+ constructor: (@fn) ->
3
+ this.resize()
4
+ this.listen()
5
+ setTimeout (=> this.resize()), 250
6
+
7
+ resize: ->
8
+ this.fill '.x-fill', 'outerWidth', 'width'
9
+ this.fill '.y-fill', 'outerHeight', 'height'
10
+ this.fn()
11
+
12
+ fill: (selector, get, set) ->
13
+ $(selector).each (ix, node) =>
14
+ node = $(node)
15
+ getter = node[get]
16
+ parent = getter.call node.parent()
17
+ fixed = this.fixed node, selector, (n) -> getter.call n
18
+ node[set].call node, parent - fixed
19
+
20
+ fixed: (node, selector, fn) ->
21
+ node.siblings().not(selector).not('.float').filter(':visible')
22
+ .map(-> fn $ this).get()
23
+ .reduce ((sum, num) -> sum + num), 0
24
+
25
+ listen: ->
26
+ id = null
27
+ $(window).resize =>
28
+ clearTimeout id
29
+ id = setTimeout (=> this.resize()), 10
30
+ this.resize()
@@ -0,0 +1,52 @@
1
+ class LoginPage
2
+ constructor: (@session, @startPage) ->
3
+
4
+ start: ->
5
+ $('#error').hide()
6
+ callback = (success) =>
7
+ unless success
8
+ @session.disconnect()
9
+ $('#error').show()
10
+ $('#password').val('').focus()
11
+ return
12
+
13
+ localStorage['jid'] = $('#jid').val()
14
+ $('#current-user-name').text @session.bareJid()
15
+ $('#current-user-avatar').attr 'src', @session.avatar(@session.jid())
16
+ $('#current-user-avatar').attr 'alt', @session.bareJid()
17
+ $('#container').fadeOut 200, =>
18
+ $('#navbar').show()
19
+ window.location.hash = @startPage
20
+
21
+ @session.connect $('#jid').val(), $('#password').val(), callback
22
+ false
23
+
24
+ draw: ->
25
+ @session.disconnect()
26
+ jid = localStorage['jid'] || ''
27
+ $('#navbar').hide()
28
+ $('body').attr 'id', 'login-page'
29
+ $('#container').hide().empty()
30
+ $("""
31
+ <form id="login-form">
32
+ <h1>vines&gt;</h1>
33
+ <fieldset id="login-form-controls">
34
+ <input id="jid" name="jid" type="email" maxlength="1024" value="#{jid}" placeholder="Your user name"/>
35
+ <input id="password" name="password" type="password" maxlength="1024" placeholder="Your password"/>
36
+ <input id="start" type="submit" value="Sign in"/>
37
+ </fieldset>
38
+ <p id="error" style="display:none;">User name and password not found.</p>
39
+ </form>
40
+ """).appendTo '#container'
41
+ $('#login-form').submit => this.start()
42
+ $('#container').fadeIn 1000
43
+ $('#jid').keydown -> $('#error').fadeOut()
44
+ $('#password').keydown -> $('#error').fadeOut()
45
+ this.resize()
46
+
47
+ resize: ->
48
+ win = $ window
49
+ form = $ '#login-form'
50
+ sizer = -> form.css 'top', win.height() / 2 - form.height() / 2
51
+ win.resize sizer
52
+ sizer()
@@ -0,0 +1,84 @@
1
+ class NavBar
2
+ constructor: (@session) ->
3
+ @session.onCard (card) =>
4
+ if card.jid == @session.bareJid()
5
+ $('#current-user-avatar').attr 'src', @session.avatar card.jid
6
+
7
+ draw: ->
8
+ $("""
9
+ <header id="navbar" class="x-fill">
10
+ <h1 id="logo">vines&gt;</h1>
11
+ <div id="current-user">
12
+ <img id="current-user-avatar" alt="#{@session.bareJid()}" src="#{@session.avatar(@session.jid())}"/>
13
+ <div id="current-user-info">
14
+ <h1 id="current-user-name">#{@session.bareJid()}</h1>
15
+ <form id="current-user-presence-form">
16
+ <span class="select">
17
+ <span class="text">Available</span>
18
+ <select id="current-user-presence">
19
+ <optgroup label="Available">
20
+ <option>Available</option>
21
+ <option>Surfing the web</option>
22
+ <option>Reading email</option>
23
+ </optgroup>
24
+ <optgroup label="Away">
25
+ <option value="xa">Away</option>
26
+ <option value="xa">Out to lunch</option>
27
+ <option value="xa">On the phone</option>
28
+ <option value="xa">In a meeting</option>
29
+ </optgroup>
30
+ </select>
31
+ </span>
32
+ </form>
33
+ </div>
34
+ </div>
35
+ <nav id="app-nav" class="x-fill">
36
+ <ul id="nav-links"></ul>
37
+ </nav>
38
+ </header>
39
+ """).appendTo 'body'
40
+ $('<div id="container" class="x-fill y-fill"></div>').appendTo 'body'
41
+
42
+ $('#current-user-presence').change (event) =>
43
+ selected = $ 'option:selected', event.currentTarget
44
+ $('#current-user-presence-form .text').text selected.text()
45
+ @session.sendPresence selected.val() == 'xa', selected.text()
46
+
47
+ addButton: (label, icon) ->
48
+ id = "nav-link-#{label.toLowerCase()}"
49
+ node = $("""
50
+ <li>
51
+ <a id="#{id}" href="#/#{label.toLowerCase()}">
52
+ <span>#{label}</span>
53
+ </a>
54
+ </li>
55
+ """).appendTo '#nav-links'
56
+ this.button(id, icon)
57
+ node.click (event) => this.select(event.currentTarget)
58
+
59
+ select: (button) ->
60
+ button = $(button)
61
+ $('#nav-links li').removeClass('selected')
62
+ $('#nav-links li a').removeClass('selected')
63
+ button.addClass('selected')
64
+ $('a', button).addClass('selected')
65
+ dark = $('#nav-links svg path')
66
+ dark.attr 'opacity', '0.6'
67
+ dark.css 'opacity', '0.6'
68
+ light = $('svg path', button)
69
+ light.attr 'opacity', '1.0'
70
+ light.css 'opacity', '1.0'
71
+
72
+ button: (id, path) ->
73
+ paper = Raphael(id)
74
+ icon = paper.path(path).attr
75
+ fill: '#fff'
76
+ stroke: '#000'
77
+ 'stroke-width': 0.3
78
+ opacity: 0.6
79
+
80
+ node = $('#' + id)
81
+ node.hover(
82
+ -> icon.animate(opacity: 1.0, 200),
83
+ -> icon.animate(opacity: 0.6, 200) unless node.hasClass('selected'))
84
+ node.get 0