lively 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fe993ef2758a622d5da68c03094ea040678f62618b26bd8f888ab6127366300
4
- data.tar.gz: 4919d6e2ec9fd70f2388d838a98740a6f911875ee233e3d351e55e59f426fa46
3
+ metadata.gz: 5792cd3cb017762c79c0c8efcf99724db21dd444aa0e6e1f0c7f4f8e0e61774a
4
+ data.tar.gz: 0275c27630c5e0af1408771737e9fa87276ce3b6e2f06de788eaa9e45d60a6a5
5
5
  SHA512:
6
- metadata.gz: 1b8e0c1657776e309f5e164f79ad66e345aac9a455ef604bf2c326dfef9ce06e2a8f8370cd6052706a825bbf159ce5e5f60ade6b3cfd5f651c38aa2ee3c15bc6
7
- data.tar.gz: 2f2e3e3043956ae4ce65c5a82e82c62af462d1b750ef51d0aa4dc71c9ffc0f731ad80779517fe44263b5c9f57a1d187f84e2cb9d0a325c46855c799c9d404c3d
6
+ metadata.gz: 3c0f91c91d6d25d138aa78332cfb9c51d4f5793f5134c059bed8d1b6d1dd4b327a70f3d8e47106a49965534157d58913952b60f7508e903b461d3e15d48381bb
7
+ data.tar.gz: c4b7817e61109d157bb68c157e8c50590776db6c1eca1c4d7f65936339ba2d02da62f07dfc903cd939d8807231dc382e6b7f18702ef0096266b3bd458b25f4cd
checksums.yaml.gz.sig CHANGED
Binary file
data/bin/lively ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'async/service'
4
+ require_relative '../lib/lively/environment/application'
5
+
6
+ ARGV.each do |path|
7
+ require(path)
8
+ end
9
+
10
+ configuration = Async::Service::Configuration.build do
11
+ service "lively" do
12
+ include Lively::Environment::Application
13
+ end
14
+ end
15
+
16
+ Async::Service::Controller.run(configuration)
@@ -4,12 +4,32 @@
4
4
  # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
6
  require 'live'
7
+ require 'protocol/http/middleware'
7
8
  require 'async/websocket/adapters/http'
8
9
 
9
10
  require_relative 'pages/index'
11
+ require_relative 'hello_world'
10
12
 
11
13
  module Lively
12
14
  class Application < Protocol::HTTP::Middleware
15
+ def self.[](tag)
16
+ klass = Class.new(self)
17
+
18
+ klass.define_singleton_method(:resolver) do
19
+ Live::Resolver.allow(tag)
20
+ end
21
+
22
+ klass.define_method(:body) do
23
+ tag.new
24
+ end
25
+
26
+ return klass
27
+ end
28
+
29
+ def self.resolver
30
+ Live::Resolver.allow(HelloWorld)
31
+ end
32
+
13
33
  def initialize(delegate, resolver: self.class.resolver)
14
34
  super(delegate)
15
35
 
@@ -24,19 +44,23 @@ module Lively
24
44
  self.class.name
25
45
  end
26
46
 
27
- def body
28
- "Hello World"
47
+ def body(...)
48
+ HelloWorld.new(...)
49
+ end
50
+
51
+ def index(...)
52
+ Pages::Index.new(title: self.title, body: self.body(...))
29
53
  end
30
54
 
31
- def index
32
- Pages::Index.new(title: self.title, body: self.body)
55
+ def handle(request, ...)
56
+ return Protocol::HTTP::Response[200, [], [self.index(...).call]]
33
57
  end
34
58
 
35
59
  def call(request)
36
60
  if request.path == '/live'
37
61
  return Async::WebSocket::Adapters::HTTP.open(request, &self.method(:live)) || Protocol::HTTP::Response[400]
38
62
  else
39
- return Protocol::HTTP::Response[200, [], [self.index.call]]
63
+ return handle(request)
40
64
  end
41
65
  end
42
66
  end
@@ -3,13 +3,18 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
+ require_relative '../application'
7
+ require_relative '../assets'
8
+
9
+ require 'falcon/environment/server'
10
+
6
11
  module Lively
7
12
  module Environment
8
13
  module Application
9
14
  include Falcon::Environment::Server
10
15
 
11
16
  def application
12
- if Object.const_defined?(:Application, false)
17
+ if Object.const_defined?(:Application)
13
18
  ::Application
14
19
  else
15
20
  Console.warn(self, "No Application class defined, using default.")
@@ -0,0 +1,59 @@
1
+ module Lively
2
+ class HelloWorld < Live::View
3
+ def initialize(...)
4
+ super
5
+
6
+ @clock = nil
7
+ end
8
+
9
+ def bind(page)
10
+ super
11
+
12
+ @clock ||= Async do
13
+ while true
14
+ self.update!
15
+
16
+ sleep 1
17
+ end
18
+ end
19
+ end
20
+
21
+ def close
22
+ @clock&.stop
23
+
24
+ super
25
+ end
26
+
27
+ def render(builder)
28
+ builder.tag(:h1) do
29
+ builder.text("Hello, I'm Lively!")
30
+ end
31
+
32
+ builder.tag(:p) do
33
+ builder.text("The time is #{Time.now}.")
34
+ end
35
+
36
+ builder.tag(:p) do
37
+ builder.text(<<~TEXT)
38
+ Lively is a simple client-server SPA framework. It is designed to be easy to use and understand, while providing a solid foundation for building interactive web applications. Create an `application.rb` file and define your own `Application` class to get started.
39
+ TEXT
40
+ end
41
+
42
+ builder.inline_tag(:pre) do
43
+ builder.text(<<~TEXT)
44
+ #!/usr/bin/env lively
45
+
46
+ class Application < Lively::Application
47
+ def body
48
+ Lively::HelloWorld.new
49
+ end
50
+ end
51
+ TEXT
52
+ end
53
+
54
+ builder.tag(:p) do
55
+ builder.text("Check the `examples/` directory for... you guessed it... more examples.")
56
+ end
57
+ end
58
+ end
59
+ end
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
6
  module Lively
7
- VERSION = "0.5.0"
7
+ VERSION = "0.6.0"
8
8
  end
@@ -1,6 +1,13 @@
1
1
  import morphdom from 'morphdom';
2
2
 
3
3
  export class Live {
4
+ #window;
5
+ #document;
6
+ #server;
7
+ #events;
8
+ #failures;
9
+ #reconnectTimer;
10
+
4
11
  static start(options = {}) {
5
12
  let window = options.window || globalThis;
6
13
  let path = options.path || 'live'
@@ -13,35 +20,37 @@ export class Live {
13
20
  }
14
21
 
15
22
  constructor(window, url) {
16
- this.window = window;
17
- this.document = window.document;
23
+ this.#window = window;
24
+ this.#document = window.document;
18
25
 
19
26
  this.url = url;
20
- this.events = [];
27
+ this.#server = null;
28
+ this.#events = [];
21
29
 
22
- this.failures = 0;
23
- this.reconnectTimer = null;
30
+ this.#failures = 0;
31
+ this.#reconnectTimer = null;
24
32
 
25
33
  // Track visibility state and connect if required:
26
- this.document.addEventListener("visibilitychange", () => this.handleVisibilityChange());
27
- this.handleVisibilityChange();
34
+ this.#document.addEventListener("visibilitychange", () => this.#handleVisibilityChange());
35
+
36
+ this.#handleVisibilityChange();
28
37
 
29
- const elementNodeType = this.window.Node.ELEMENT_NODE;
38
+ const elementNodeType = this.#window.Node.ELEMENT_NODE;
30
39
 
31
40
  // Create a MutationObserver to watch for removed nodes
32
- this.observer = new this.window.MutationObserver((mutationsList, observer) => {
41
+ this.observer = new this.#window.MutationObserver((mutationsList, observer) => {
33
42
  for (let mutation of mutationsList) {
34
43
  if (mutation.type === 'childList') {
35
44
  for (let node of mutation.removedNodes) {
36
45
  if (node.nodeType !== elementNodeType) continue;
37
46
 
38
47
  if (node.classList?.contains('live')) {
39
- this.unbind(node);
48
+ this.#unbind(node);
40
49
  }
41
50
 
42
51
  // Unbind any child nodes:
43
52
  for (let child of node.getElementsByClassName('live')) {
44
- this.unbind(child);
53
+ this.#unbind(child);
45
54
  }
46
55
  }
47
56
 
@@ -49,65 +58,65 @@ export class Live {
49
58
  if (node.nodeType !== elementNodeType) continue;
50
59
 
51
60
  if (node.classList.contains('live')) {
52
- this.bind(node);
61
+ this.#bind(node);
53
62
  }
54
63
 
55
64
  // Bind any child nodes:
56
65
  for (let child of node.getElementsByClassName('live')) {
57
- this.bind(child);
66
+ this.#bind(child);
58
67
  }
59
68
  }
60
69
  }
61
70
  }
62
71
  });
63
72
 
64
- this.observer.observe(this.document.body, {childList: true, subtree: true});
73
+ this.observer.observe(this.#document.body, {childList: true, subtree: true});
65
74
  }
66
75
 
67
76
  // -- Connection Handling --
68
77
 
69
78
  connect() {
70
- if (this.server) {
71
- return this.server;
79
+ if (this.#server) {
80
+ return this.#server;
72
81
  }
73
82
 
74
- let server = this.server = new this.window.WebSocket(this.url);
83
+ let server = this.#server = new this.#window.WebSocket(this.url);
75
84
 
76
- if (this.reconnectTimer) {
77
- clearTimeout(this.reconnectTimer);
78
- this.reconnectTimer = null;
85
+ if (this.#reconnectTimer) {
86
+ clearTimeout(this.#reconnectTimer);
87
+ this.#reconnectTimer = null;
79
88
  }
80
89
 
81
90
  server.onopen = () => {
82
- this.failures = 0;
83
- this.flush();
84
- this.attach();
91
+ this.#failures = 0;
92
+ this.#flush();
93
+ this.#attach();
85
94
  };
86
95
 
87
96
  server.onmessage = (message) => {
88
- const [name, ..._arguments] = JSON.parse(message.data);
97
+ const [name, ...args] = JSON.parse(message.data);
89
98
 
90
- this[name](..._arguments);
99
+ this[name](...args);
91
100
  };
92
101
 
93
102
  // The remote end has disconnected:
94
103
  server.addEventListener('error', () => {
95
- this.failures += 1;
104
+ this.#failures += 1;
96
105
  });
97
106
 
98
107
  server.addEventListener('close', () => {
99
- // Explicit disconnect will clear `this.server`:
100
- if (this.server && !this.reconnectTimer) {
108
+ // Explicit disconnect will clear `this.#server`:
109
+ if (this.#server && !this.#reconnectTimer) {
101
110
  // We need a minimum delay otherwise this can end up immediately invoking the callback:
102
- const delay = Math.max(100 * (this.failures + 1) ** 2, 60000);
103
- this.reconnectTimer = setTimeout(() => {
104
- this.reconnectTimer = null;
111
+ const delay = Math.max(100 * (this.#failures + 1) ** 2, 60000);
112
+ this.#reconnectTimer = setTimeout(() => {
113
+ this.#reconnectTimer = null;
105
114
  this.connect();
106
115
  }, delay);
107
116
  }
108
117
 
109
- if (this.server === server) {
110
- this.server = null;
118
+ if (this.#server === server) {
119
+ this.#server = null;
111
120
  }
112
121
  });
113
122
 
@@ -115,133 +124,149 @@ export class Live {
115
124
  }
116
125
 
117
126
  disconnect() {
118
- if (this.server) {
119
- const server = this.server;
120
- this.server = null;
127
+ if (this.#server) {
128
+ const server = this.#server;
129
+ this.#server = null;
121
130
  server.close();
122
131
  }
123
132
 
124
- if (this.reconnectTimer) {
125
- clearTimeout(this.reconnectTimer);
126
- this.reconnectTimer = null;
133
+ if (this.#reconnectTimer) {
134
+ clearTimeout(this.#reconnectTimer);
135
+ this.#reconnectTimer = null;
127
136
  }
128
137
  }
129
138
 
130
- send(message) {
131
- if (this.server) {
139
+ #send(message) {
140
+ if (this.#server) {
132
141
  try {
133
- return this.server.send(message);
142
+ return this.#server.send(message);
134
143
  } catch (error) {
135
144
  // console.log("Live.send", "failed to send message to server", error);
136
145
  }
137
146
  }
138
147
 
139
- this.events.push(message);
148
+ this.#events.push(message);
140
149
  }
141
150
 
142
- flush() {
143
- if (this.events.length === 0) return;
151
+ #flush() {
152
+ if (this.#events.length === 0) return;
144
153
 
145
- let events = this.events;
146
- this.events = [];
154
+ let events = this.#events;
155
+ this.#events = [];
147
156
 
148
157
  for (var event of events) {
149
- this.send(event);
158
+ this.#send(event);
150
159
  }
151
160
  }
152
161
 
153
- handleVisibilityChange() {
154
- if (this.document.hidden) {
162
+ #handleVisibilityChange() {
163
+ if (this.#document.hidden) {
155
164
  this.disconnect();
156
165
  } else {
157
166
  this.connect();
158
167
  }
159
168
  }
160
169
 
161
- bind(element) {
170
+ #bind(element) {
162
171
  console.log("bind", element.id, element.dataset);
163
172
 
164
- this.send(JSON.stringify(['bind', element.id, element.dataset]));
173
+ this.#send(JSON.stringify(['bind', element.id, element.dataset]));
165
174
  }
166
175
 
167
- unbind(element) {
176
+ #unbind(element) {
168
177
  console.log("unbind", element.id, element.dataset);
169
178
 
170
- if (this.server) {
171
- this.send(JSON.stringify(['unbind', element.id]));
179
+ if (this.#server) {
180
+ this.#send(JSON.stringify(['unbind', element.id]));
172
181
  }
173
182
  }
174
183
 
175
- attach() {
176
- for (let node of this.document.getElementsByClassName('live')) {
177
- this.bind(node);
184
+ #attach() {
185
+ for (let node of this.#document.getElementsByClassName('live')) {
186
+ this.#bind(node);
178
187
  }
179
188
  }
180
189
 
181
- createDocumentFragment(html) {
182
- return this.document.createRange().createContextualFragment(html);
190
+ #createDocumentFragment(html) {
191
+ return this.#document.createRange().createContextualFragment(html);
183
192
  }
184
193
 
185
- reply(options) {
194
+ #reply(options, ...args) {
186
195
  if (options?.reply) {
187
- this.send(JSON.stringify(['reply', options.reply]));
196
+ this.#send(JSON.stringify(['reply', options.reply, ...args]));
188
197
  }
189
198
  }
190
199
 
191
200
  // -- RPC Methods --
192
201
 
202
+ script(id, code, options) {
203
+ let element = this.#document.getElementById(id);
204
+
205
+ try {
206
+ let result = this.#window.Function(code).call(element);
207
+
208
+ this.#reply(options, result);
209
+ } catch (error) {
210
+ this.#reply(options, null, {name: error.name, message: error.message, stack: error.stack});
211
+ }
212
+ }
213
+
193
214
  update(id, html, options) {
194
- let element = this.document.getElementById(id);
195
- let fragment = this.createDocumentFragment(html);
215
+ let element = this.#document.getElementById(id);
216
+ let fragment = this.#createDocumentFragment(html);
196
217
 
197
218
  morphdom(element, fragment);
198
219
 
199
- this.reply(options);
220
+ this.#reply(options);
200
221
  }
201
222
 
202
223
  replace(selector, html, options) {
203
- let elements = this.document.querySelectorAll(selector);
204
- let fragment = this.createDocumentFragment(html);
224
+ let elements = this.#document.querySelectorAll(selector);
225
+ let fragment = this.#createDocumentFragment(html);
205
226
 
206
227
  elements.forEach(element => morphdom(element, fragment.cloneNode(true)));
207
228
 
208
- this.reply(options);
229
+ this.#reply(options);
209
230
  }
210
231
 
211
232
  prepend(selector, html, options) {
212
- let elements = this.document.querySelectorAll(selector);
213
- let fragment = this.createDocumentFragment(html);
233
+ let elements = this.#document.querySelectorAll(selector);
234
+ let fragment = this.#createDocumentFragment(html);
214
235
 
215
236
  elements.forEach(element => element.prepend(fragment.cloneNode(true)));
216
237
 
217
- this.reply(options);
238
+ this.#reply(options);
218
239
  }
219
240
 
220
241
  append(selector, html, options) {
221
- let elements = this.document.querySelectorAll(selector);
222
- let fragment = this.createDocumentFragment(html);
242
+ let elements = this.#document.querySelectorAll(selector);
243
+ let fragment = this.#createDocumentFragment(html);
223
244
 
224
245
  elements.forEach(element => element.append(fragment.cloneNode(true)));
225
246
 
226
- this.reply(options);
247
+ this.#reply(options);
227
248
  }
228
249
 
229
250
  remove(selector, options) {
230
- let elements = this.document.querySelectorAll(selector);
251
+ let elements = this.#document.querySelectorAll(selector);
231
252
 
232
253
  elements.forEach(element => element.remove());
233
254
 
234
- this.reply(options);
255
+ this.#reply(options);
235
256
  }
236
257
 
237
258
  dispatchEvent(selector, type, options) {
238
- let elements = this.document.querySelectorAll(selector);
259
+ let elements = this.#document.querySelectorAll(selector);
239
260
 
240
261
  elements.forEach(element => element.dispatchEvent(
241
- new this.window.CustomEvent(type, options)
262
+ new this.#window.CustomEvent(type, options)
242
263
  ));
243
264
 
244
- this.reply(options);
265
+ this.#reply(options);
266
+ }
267
+
268
+ error(message) {
269
+ console.error("Live.error", ...arguments);
245
270
  }
246
271
 
247
272
  // -- Event Handling --
@@ -249,19 +274,19 @@ export class Live {
249
274
  forward(id, event) {
250
275
  this.connect();
251
276
 
252
- this.send(
277
+ this.#send(
253
278
  JSON.stringify(['event', id, event])
254
279
  );
255
280
  }
256
281
 
257
- forwardEvent(id, event, detail) {
258
- event.preventDefault();
282
+ forwardEvent(id, event, detail, preventDefault = false) {
283
+ if (preventDefault) event.preventDefault();
259
284
 
260
285
  this.forward(id, {type: event.type, detail: detail});
261
286
  }
262
287
 
263
- forwardFormEvent(id, event, detail) {
264
- event.preventDefault();
288
+ forwardFormEvent(id, event, detail, preventDefault = true) {
289
+ if (preventDefault) event.preventDefault();
265
290
 
266
291
  let form = event.form;
267
292
  let formData = new FormData(form);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@socketry/live",
3
3
  "type": "module",
4
- "version": "0.11.0",
4
+ "version": "0.13.0",
5
5
  "description": "Live HTML tags for Ruby.",
6
6
  "main": "Live.js",
7
7
  "repository": {
@@ -85,9 +85,9 @@ describe('Live', function () {
85
85
  const live = Live.start({window: dom.window, base: 'http://localhost/'});
86
86
  ok(live);
87
87
 
88
- strictEqual(live.window, dom.window);
89
- strictEqual(live.document, dom.window.document);
90
88
  strictEqual(live.url.href, 'ws://localhost/live');
89
+
90
+ live.disconnect();
91
91
  });
92
92
 
93
93
  it('should connect to the WebSocket server', function () {
@@ -102,26 +102,46 @@ describe('Live', function () {
102
102
  it('should handle visibility changes', async function () {
103
103
  const live = new Live(dom.window, webSocketServerURL);
104
104
 
105
- let hidden = false;
106
- Object.defineProperty(dom.window.document, "hidden", {
107
- get() {return hidden},
108
- });
105
+ // It's tricky to test the method directly.
106
+ // - Changing document.hidden is a hack.
107
+ // - Sending custom events seems to cause a hang.
108
+
109
+ live.connect();
110
+ deepStrictEqual(await messages.pop(), ['bind', 'my', {}]);
109
111
 
110
- // The document starts out hidden... we have defined a property to make it not hidden, let's propagate that change:
111
- live.handleVisibilityChange();
112
+ live.disconnect();
112
113
 
113
- // We should receive a bind message for the live element:
114
+ live.connect()
114
115
  deepStrictEqual(await messages.pop(), ['bind', 'my', {}]);
115
116
 
116
- hidden = true;
117
- live.handleVisibilityChange();
118
- ok(!live.server);
117
+ live.disconnect();
118
+ });
119
+
120
+ it('can execute scripts', async function () {
121
+ const live = new Live(dom.window, webSocketServerURL);
119
122
 
120
- hidden = false;
121
- live.handleVisibilityChange();
122
- ok(live.server);
123
+ live.connect();
123
124
 
124
- deepStrictEqual(await messages.pop(), ['bind', 'my', {}]);
125
+ const connected = new Promise(resolve => {
126
+ webSocketServer.on('connection', resolve);
127
+ });
128
+
129
+ let socket = await connected;
130
+
131
+ socket.send(
132
+ JSON.stringify(['script', 'my', 'return 1+2', {reply: true}])
133
+ );
134
+
135
+ let successReply = await messages.popUntil(message => message[0] == 'reply');
136
+ strictEqual(successReply[2], 3);
137
+
138
+ socket.send(
139
+ JSON.stringify(['script', 'my', 'throw new Error("Test Error")', {reply: true}])
140
+ );
141
+
142
+ let errorReply = await messages.popUntil(message => message[0] == 'reply');
143
+ strictEqual(errorReply[2], null);
144
+ console.log(errorReply);
125
145
 
126
146
  live.disconnect();
127
147
  });
@@ -178,7 +198,10 @@ describe('Live', function () {
178
198
 
179
199
  dom.window.document.getElementById('my').remove();
180
200
 
181
- let payload = await messages.popUntil(message => message[0] == 'unbind');
201
+ let payload = await messages.popUntil(message => {
202
+ return message[0] == 'unbind' && message[1] == 'my';
203
+ });
204
+
182
205
  deepStrictEqual(payload, ['unbind', 'my']);
183
206
 
184
207
  live.disconnect();
@@ -325,4 +348,10 @@ describe('Live', function () {
325
348
 
326
349
  live.disconnect();
327
350
  });
351
+
352
+ it ('can log errors', function () {
353
+ const live = new Live(dom.window, webSocketServerURL);
354
+
355
+ live.error('my', 'Test Error');
356
+ });
328
357
  });
Binary file
@@ -1 +1,5 @@
1
- /* Default Stylesheet */
1
+ /*
2
+
3
+ Create your own `public/static/index.css` to override the default stylesheet.
4
+
5
+ */
@@ -1,6 +1,6 @@
1
1
 
2
2
  html {
3
- font-family: Monaco, monospace;
3
+ font-family: Arial, sans-serif;
4
4
  font-size: 16px;
5
5
 
6
6
  /* Fix odd text-size in `display: flex` elements on Safari iOS */
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lively
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -59,14 +59,14 @@ dependencies:
59
59
  requirements:
60
60
  - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: '0.8'
62
+ version: '0.9'
63
63
  type: :runtime
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '0.8'
69
+ version: '0.9'
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: xrb
72
72
  requirement: !ruby/object:Gem::Requirement
@@ -83,14 +83,17 @@ dependencies:
83
83
  version: '0'
84
84
  description:
85
85
  email:
86
- executables: []
86
+ executables:
87
+ - lively
87
88
  extensions: []
88
89
  extra_rdoc_files: []
89
90
  files:
91
+ - bin/lively
90
92
  - lib/lively.rb
91
93
  - lib/lively/application.rb
92
94
  - lib/lively/assets.rb
93
95
  - lib/lively/environment/application.rb
96
+ - lib/lively/hello_world.rb
94
97
  - lib/lively/pages/index.rb
95
98
  - lib/lively/pages/index.xrb
96
99
  - lib/lively/version.rb
@@ -104,6 +107,7 @@ files:
104
107
  - public/_components/morphdom/morphdom-umd.js
105
108
  - public/_components/morphdom/morphdom-umd.min.js
106
109
  - public/_components/morphdom/morphdom.js
110
+ - public/_static/Falcon.png
107
111
  - public/_static/icon.png
108
112
  - public/_static/index.css
109
113
  - public/_static/site.css
metadata.gz.sig CHANGED
Binary file