trmnl_preview 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d511eb133cb9ee46d6d343f9ed1bef3c11c3e581ae193310d2d6f50d900c4a4
4
- data.tar.gz: ed60374a348285201cd0f49396cd2995c5b87301866aec421cd3c21131c6fe98
3
+ metadata.gz: 3a39d955b3dbcc869cde5e0210671a94dfa7af0ea1403fb71c883534d85b6eac
4
+ data.tar.gz: f0159db40a383ce3149071fd4826bdb9035abab379d442e8a93012a35f6cce30
5
5
  SHA512:
6
- metadata.gz: 063e9d97310fdc7c20609e83573006e18df1a081455d8d60d1694fbbc21a43c82c99290395b54e0b815d498884ebf69f984f4d153c3d50131c34b090c6752749
7
- data.tar.gz: c44116ac437e7636c403481d6b5b773a5f0d0ccf425b6e2181bc987d272056bef407ce43919d967fa86da80b96b032534d423b43dd60ef71d56c96ec22e4b9ab
6
+ metadata.gz: cccd5ba2af16c057717ae4f0acb6843fad3cd9a7f305109817c5d195f278dd1102c39a20cbf21abbbae0e18395b41ac8e5b314f5c10ae763503cc3e877f1403b
7
+ data.tar.gz: 19eb007c4d077a3078cde135ca200873f5d050abe167306b878b3f7374e052c282d3cb348e806486ce62fa0d6c0913efb1b4feaf100e213ad16d6e5167426ed9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Add poll button
6
+ - Add case image overlays
7
+ - Add `trmnlp build` command
8
+ - Add support for `url` pointing to a local JSON data file
9
+
3
10
  ## 0.2.0
4
11
 
5
12
  - Add "commands" concept to `trmnlp` executable
data/README.md CHANGED
@@ -4,6 +4,8 @@ A basic self-hosted web server to ease the development and sharing of [TRMNL](ht
4
4
 
5
5
  [Liquid](https://shopify.github.io/liquid/) templates are rendered locally as HTML, leveraging the [TRMNL Design System](https://usetrmnl.com/framework). This server does NOT generate a rendered BMP file. Hence, this is just a _preview_ of the final rendered dashboard.
6
6
 
7
+ The server watches the filesystem for changes to the Liquid templates, seamlessly updating the preview without the need to refresh.
8
+
7
9
  ![Screenshot](docs/preview.png)
8
10
 
9
11
  ## Creating a Plugin
@@ -53,6 +55,7 @@ When the strategy is "webhook", payloads can be POSTed to the `/webhook` endpoin
53
55
 
54
56
  - `strategy` - Either "polling" or "webhook"
55
57
  - `url` - The URL from which to fetch JSON data (polling strategy only)
58
+ - `live_render` - Set to `false` to disable automatic rendering when Liquid templates change (default `true`)
56
59
  - `[polling_headers]` - A section of headers to append to the HTTP poll request (polling strategy only)
57
60
 
58
61
  ## Contributing
data/config.example.toml CHANGED
@@ -5,6 +5,9 @@ strategy = "polling"
5
5
  # Poll URL (required for polling strategy)
6
6
  url = "https://example.com/data.json"
7
7
 
8
+ # Automatically re-render the view when Liquid templates change (default: true)
9
+ live_render = true
10
+
8
11
  # Polling headers (optional, for polling strategy)
9
12
  [polling_headers]
10
13
  authorization = "bearer 123"
data/docs/preview.png CHANGED
Binary file
data/exe/trmnlp CHANGED
@@ -5,6 +5,8 @@ require_relative '../lib/trmnl_preview'
5
5
  case ARGV[0].to_s.downcase
6
6
  when 'serve'
7
7
  require_relative '../lib/trmnl_preview/cmd/serve'
8
+ when 'build'
9
+ require_relative '../lib/trmnl_preview/cmd/build'
8
10
  else
9
11
  require_relative '../lib/trmnl_preview/cmd/usage'
10
12
  end
@@ -1,97 +1,77 @@
1
- require 'json'
2
- require 'liquid'
3
- require 'open-uri'
1
+
2
+ require 'faye/websocket'
4
3
  require 'sinatra'
5
4
  require 'sinatra/base'
6
- require 'toml-rb'
7
5
 
8
- require_relative 'liquid_filters'
6
+ require_relative 'context'
9
7
 
10
- class TRMNLPreview::App < Sinatra::Base
11
- # Constants
12
- VIEWS = %w{full half_horizontal half_vertical quadrant}
8
+ module TRMNLPreview
9
+ class App < Sinatra::Base
10
+ # Sinatra settings
11
+ set :views, File.join(File.dirname(__FILE__), '..', '..', 'web', 'views')
12
+ set :public_folder, File.join(File.dirname(__FILE__), '..', '..', 'web', 'public')
13
+
14
+ def initialize(*args)
15
+ super
13
16
 
14
- # Sinatra settings
15
- set :views, File.join(File.dirname(__FILE__), '..', '..', 'views')
16
-
17
- def initialize(*args)
18
- super
17
+ begin
18
+ @context = Context.new(settings.user_dir)
19
+ rescue StandardError => e
20
+ puts e.message
21
+ exit 1
22
+ end
19
23
 
20
- @config_path = File.join(settings.user_dir, 'config.toml')
21
- @user_views_dir = File.join(settings.user_dir, 'views')
22
- @temp_dir = File.join(settings.user_dir, 'tmp')
23
- @data_json_path = File.join(@temp_dir, 'data.json')
24
+ @context.poll_data if @context.strategy == 'polling'
24
25
 
25
- unless File.exist?(@config_path)
26
- puts "No config.toml found in #{settings.user_dir}"
27
- exit 1
26
+ @live_render_clients = VIEWS.each_with_object({}) { |view, hash| hash[view] = [] }
27
+ @context.on_view_change do |view|
28
+ @live_render_clients[view].each do |ws|
29
+ ws.send(@context.render_template(view))
30
+ end
31
+ end
28
32
  end
29
-
30
- unless Dir.exist?(@user_views_dir)
31
- puts "No views found at #{@user_views_dir}"
32
- exit 1
33
+
34
+ post '/webhook' do
35
+ @context.set_data(request.body.read)
36
+ "OK"
37
+ end
38
+
39
+ get '/' do
40
+ redirect '/full'
33
41
  end
34
42
 
35
- FileUtils.mkdir_p(@temp_dir)
43
+ get '/live_render/:view' do
44
+ ws = Faye::WebSocket.new(request.env)
45
+ view = params['view']
36
46
 
37
- @config = TomlRB.load_file(@config_path)
38
- strategy = @config['strategy']
39
-
40
- unless ['polling', 'webhook'].include?(strategy)
41
- puts "Invalid strategy: #{strategy} (must be 'polling' or 'webhook')"
42
- exit 1
43
- end
44
-
45
- url = @config['url']
46
- polling_headers = @config['polling_headers'] || {}
47
-
48
- if strategy == 'polling'
49
- if url.nil?
50
- puts "URL is required for polling strategy"
51
- exit 1
47
+ ws.on(:open) do |event|
48
+ @live_render_clients[view] << ws
52
49
  end
53
50
 
54
- print "Fetching #{url}... "
55
- payload = URI.open(url, polling_headers).read
56
- File.write(@data_json_path, payload)
57
- puts "got #{payload.size} bytes"
58
- end
51
+ ws.on(:close) do |event|
52
+ @live_render_clients[view].delete(ws)
53
+ end
59
54
 
60
- @liquid_environment = Liquid::Environment.build do |env|
61
- env.register_filter(TRMNLPreview::LiquidFilters)
55
+ ws.rack_response
62
56
  end
63
- end
64
57
 
65
- post '/webhook' do
66
- body = request.body.read
67
- File.write(@data_json_path, body)
68
- "OK"
69
- end
70
-
71
- get '/' do
72
- redirect '/full'
73
- end
74
-
75
- VIEWS.each do |view|
76
- get "/#{view}" do
77
- @view = view
78
- erb :index
58
+ get '/poll' do
59
+ @context.poll_data
60
+ redirect back
79
61
  end
80
-
81
- get "/render/#{view}" do
82
- path = File.join(@user_views_dir, "#{view}.liquid")
83
- unless File.exist?(path)
84
- halt 404, "Plugin template not found: views/#{view}.liquid"
62
+
63
+ VIEWS.each do |view|
64
+ get "/#{view}" do
65
+ @view = view
66
+ erb :index
85
67
  end
86
68
 
87
- user_template = Liquid::Template.parse(File.read(path), environment: @liquid_environment)
88
-
89
- @view = view
90
- erb :render_view do
91
- data = JSON.parse(File.read(@data_json_path))
92
- data = { data: data } if data.is_a?(Array) # per TRMNL docs, bare array is wrapped in 'data' key
93
-
94
- user_template.render(data)
69
+ get "/render/#{view}" do
70
+ @view = view
71
+ @live_render = @context.live_render
72
+ erb :render_view do
73
+ @context.render_template(view)
74
+ end
95
75
  end
96
76
  end
97
77
  end
@@ -0,0 +1,25 @@
1
+ require 'optionparser'
2
+
3
+ require_relative '../context'
4
+
5
+ OptionParser.new do |opts|
6
+ opts.banner = "Usage: trmnlp build [directory]"
7
+ end.parse!
8
+
9
+ root = ARGV[1] || Dir.pwd
10
+ begin
11
+ context = TRMNLPreview::Context.new(root)
12
+ rescue StandardError => e
13
+ puts e.message
14
+ exit 1
15
+ end
16
+
17
+ context.poll_data
18
+
19
+ TRMNLPreview::VIEWS.each do |view|
20
+ output_path = File.join(context.temp_dir, "#{view}.html")
21
+ puts "Creating #{output_path}..."
22
+ File.write(output_path, context.render_full_page(view))
23
+ end
24
+
25
+ puts "Done!"
@@ -6,4 +6,5 @@ Usage:
6
6
  Commands (-h for command-specific help):
7
7
 
8
8
  serve Start the TRMNL Preview server
9
+ build Generate static HTML files
9
10
  USAGE
@@ -0,0 +1,129 @@
1
+ require 'erb'
2
+ require 'fileutils'
3
+ require 'filewatcher'
4
+ require 'json'
5
+ require 'liquid'
6
+ require 'open-uri'
7
+ require 'toml-rb'
8
+
9
+ require_relative 'liquid_filters'
10
+
11
+ module TRMNLPreview
12
+ class Context
13
+ attr_reader :strategy, :temp_dir, :live_render
14
+
15
+ def initialize(root)
16
+ config_path = File.join(root, 'config.toml')
17
+ @user_views_dir = File.join(root, 'views')
18
+ @temp_dir = File.join(root, 'tmp')
19
+ @data_json_path = File.join(@temp_dir, 'data.json')
20
+
21
+ @liquid_environment = Liquid::Environment.build do |env|
22
+ env.register_filter(LiquidFilters)
23
+ end
24
+
25
+ unless File.exist?(config_path)
26
+ raise "No config.toml found in #{root}"
27
+ end
28
+
29
+ unless Dir.exist?(@user_views_dir)
30
+ raise "No views found at #{@user_views_dir}"
31
+ end
32
+
33
+ config = TomlRB.load_file(config_path)
34
+ @strategy = config['strategy']
35
+ @url = config['url']
36
+ @polling_headers = config['polling_headers'] || {}
37
+ @live_render = config['live_render'] != false
38
+
39
+ unless ['polling', 'webhook'].include?(@strategy)
40
+ raise "Invalid strategy: #{strategy} (must be 'polling' or 'webhook')"
41
+ end
42
+
43
+ FileUtils.mkdir_p(@temp_dir)
44
+
45
+ start_filewatcher_thread if @live_render
46
+ end
47
+
48
+ def start_filewatcher_thread
49
+ Thread.new do
50
+ loop do
51
+ begin
52
+ Filewatcher.new(@user_views_dir).watch do |changes|
53
+ views = changes.map { |path, _change| File.basename(path, '.liquid') }
54
+ views.each do |view|
55
+ @view_change_callback.call(view) if @view_change_callback
56
+ end
57
+ end
58
+ rescue => e
59
+ puts "Error during live render: #{e}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def on_view_change(&block)
66
+ @view_change_callback = block
67
+ end
68
+
69
+ def user_data
70
+ data = JSON.parse(File.read(@data_json_path))
71
+ data = { data: data } if data.is_a?(Array) # per TRMNL docs, bare array is wrapped in 'data' key
72
+ data
73
+ end
74
+
75
+ def poll_data
76
+ if @url.nil?
77
+ raise "URL is required for polling strategy"
78
+ end
79
+
80
+ print "Fetching #{@url}... "
81
+
82
+ if @url.match?(/^https?:\/\//)
83
+ payload = URI.open(@url, @polling_headers).read
84
+ else
85
+ payload = File.read(@url)
86
+ end
87
+
88
+ File.write(@data_json_path, payload)
89
+ puts "got #{payload.size} bytes"
90
+
91
+ user_data
92
+ end
93
+
94
+ def set_data(payload)
95
+ File.write(@data_json_path, payload)
96
+ end
97
+
98
+ def view_path(view)
99
+ File.join(@user_views_dir, "#{view}.liquid")
100
+ end
101
+
102
+ def render_template(view)
103
+ path = view_path(view)
104
+ unless File.exist?(path)
105
+ return "Missing plugin template: views/#{view}.liquid"
106
+ end
107
+
108
+ user_template = Liquid::Template.parse(File.read(path), environment: @liquid_environment)
109
+ user_template.render(user_data)
110
+ rescue StandardError => e
111
+ e.message
112
+ end
113
+
114
+ def render_full_page(view)
115
+ page_erb_template = File.read(File.join(__dir__, '..', '..', 'web', 'views', 'render_view.erb'))
116
+
117
+ ERB.new(page_erb_template).result(ERBBinding.new(view).get_binding do
118
+ render_template(view)
119
+ end)
120
+ end
121
+
122
+ private
123
+
124
+ class ERBBinding
125
+ def initialize(view) = @view = view
126
+ def get_binding = binding
127
+ end
128
+ end
129
+ end
@@ -1,6 +1,8 @@
1
- module TRMNLPreview::LiquidFilters
2
- def number_with_delimiter(number)
3
- # TODO: Replace with ActiveSupport's number_with_delimiter
4
- number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
1
+ module TRMNLPreview
2
+ module LiquidFilters
3
+ def number_with_delimiter(number)
4
+ # TODO: Replace with ActiveSupport's number_with_delimiter
5
+ number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
6
+ end
5
7
  end
6
8
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLPreview
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/trmnl_preview.rb CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  module TRMNLPreview; end
4
4
 
5
- # require_relative "trmnl_preview/app"
5
+ require_relative "trmnl_preview/context"
6
6
  require_relative "trmnl_preview/version"
7
7
 
8
8
  module TRMNLPreview
9
- class Error < StandardError; end
10
- # Your code goes here...
11
- end
9
+ VIEWS = %w{full half_horizontal half_vertical quadrant}
10
+ end
@@ -37,6 +37,8 @@ Gem::Specification.new do |spec|
37
37
  spec.add_dependency "puma", "~> 6.5"
38
38
  spec.add_dependency "liquid", "~> 5.6"
39
39
  spec.add_dependency "toml-rb", "~> 3.0"
40
+ spec.add_dependency "filewatcher", "~> 2.1"
41
+ spec.add_dependency "faye-websocket", "~> 0.11.3"
40
42
 
41
43
  # For more information and examples about making a new gem, check out our
42
44
  # guide at: https://bundler.io/guides/creating_gem.html
Binary file
Binary file
@@ -0,0 +1,23 @@
1
+ live_render = {};
2
+
3
+ live_render.connect = function () {
4
+ const view = document.querySelector("meta[name='trmnl-view']").content;
5
+ const ws = new WebSocket("/live_render/" + view);
6
+
7
+ ws.onopen = function () {
8
+ console.log("Connected to live push server");
9
+ };
10
+
11
+ ws.onmessage = function (msg) {
12
+ document.querySelector(".view").innerHTML = msg.data;
13
+ };
14
+
15
+ ws.onclose = function () {
16
+ console.log("Reconnecting to live push server...");
17
+ setTimeout(live_render.connect, 1000);
18
+ };
19
+ };
20
+
21
+ document.addEventListener("DOMContentLoaded", function () {
22
+ live_render.connect();
23
+ });
Binary file
@@ -0,0 +1,113 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TRMNL Preview</title>
7
+ <style>
8
+ body {
9
+ font-family: sans-serif;
10
+ margin: 10px;
11
+ }
12
+ main {
13
+ display: flex;
14
+ flex-direction: column;
15
+ width: fit-content;
16
+ }
17
+ menu {
18
+ padding: 0;
19
+ margin: 0;
20
+ display: flex;
21
+ justify-content: space-between;
22
+ }
23
+ menu a {
24
+ padding: 0.5em 1em;
25
+ background: #ddd;
26
+ border-radius: 0.5em;
27
+ display: inline-block;
28
+ text-decoration: none;
29
+ color: black;
30
+ }
31
+ menu a:hover {
32
+ background: #ccc;
33
+ }
34
+ menu a.active {
35
+ background: #333;
36
+ color: white;
37
+ }
38
+ .case {
39
+ width: 1000px;
40
+ height: 680px;
41
+ position: relative;
42
+ }
43
+ iframe {
44
+ position: absolute;
45
+ border: none;
46
+ left: 98px;
47
+ top: 65px;
48
+ filter: grayscale(100%);
49
+ }
50
+ .case .case-overlay {
51
+ position: absolute;
52
+ width: 100%;
53
+ height: 100%;
54
+ top: 0;
55
+ left: 0;
56
+ background-size: cover;
57
+ mix-blend-mode: multiply;
58
+ pointer-events: none;
59
+ }
60
+ .case--none .case-overlay { background: none; }
61
+ .case--white .case-overlay { background-image: url('white-case.jpg') }
62
+ .case--black .case-overlay { background-image: url('black-case.jpg') }
63
+ .case--clear .case-overlay { background-image: url('clear-case.jpg') }
64
+ .case--none iframe {
65
+ border: 1px solid black;
66
+ }
67
+ </style>
68
+ <script>
69
+ function updateCase() {
70
+ const value = document.querySelector('.select-case').value;
71
+ document.querySelector('.case').className = `case case--${value}`;
72
+ localStorage.setItem('trmnlp-case', value);
73
+ }
74
+
75
+ document.addEventListener('DOMContentLoaded', () => {
76
+ const caseValue = localStorage.getItem('trmnlp-case') || 'black';
77
+
78
+ const select = document.querySelector('.select-case');
79
+ select.value = caseValue;
80
+ select.addEventListener('change', () => { updateCase() });
81
+
82
+ updateCase();
83
+ });
84
+ </script>
85
+ </head>
86
+ <body>
87
+ <main>
88
+ <menu>
89
+ <div>
90
+ <a class="<%= 'active' if @view == 'full' %>" href="/full">Full</a>
91
+ <a class="<%= 'active' if @view == 'half_horizontal' %>" href="/half_horizontal">Half Horizontal</a>
92
+ <a class="<%= 'active' if @view == 'half_vertical' %>" href="/half_vertical">Half Vertical</a>
93
+ <a class="<%= 'active' if @view == 'quadrant' %>" href="/quadrant">Quadrant</a>
94
+ </div>
95
+ <div>
96
+ <select class="select-case">
97
+ <option value="none">None</option>
98
+ <option value="white">White</option>
99
+ <option value="black" selected>Black</option>
100
+ <option value="clear">Clear</option>
101
+ </select>
102
+
103
+ <a href="/poll">Poll</a>
104
+ </div>
105
+ </menu>
106
+
107
+ <div class="case case--black">
108
+ <iframe src="/render/<%= @view %>" width="800" height="480"></iframe>
109
+ <div class="case-overlay"></div>
110
+ </div>
111
+ </main>
112
+ </body>
113
+ </html>
@@ -9,6 +9,11 @@
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
11
11
  <!-- End Inter font -->
12
+
13
+ <% if @live_render %>
14
+ <meta name="trmnl-view" content="<%= @view %>">
15
+ <script src="/live-render.js"></script>
16
+ <% end %>
12
17
  </head>
13
18
  <body class="environment trmnl">
14
19
  <div class="screen">
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trmnl_preview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
@@ -79,6 +79,34 @@ dependencies:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
81
  version: '3.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: filewatcher
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.1'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.1'
96
+ - !ruby/object:Gem::Dependency
97
+ name: faye-websocket
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 0.11.3
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 0.11.3
82
110
  description: Automatically rebuild and preview TRNML plugins in multiple views
83
111
  email:
84
112
  - rockwell@schrock.me
@@ -96,13 +124,19 @@ files:
96
124
  - exe/trmnlp
97
125
  - lib/trmnl_preview.rb
98
126
  - lib/trmnl_preview/app.rb
127
+ - lib/trmnl_preview/cmd/build.rb
99
128
  - lib/trmnl_preview/cmd/serve.rb
100
129
  - lib/trmnl_preview/cmd/usage.rb
130
+ - lib/trmnl_preview/context.rb
101
131
  - lib/trmnl_preview/liquid_filters.rb
102
132
  - lib/trmnl_preview/version.rb
103
133
  - trmnl_preview.gemspec
104
- - views/index.erb
105
- - views/render_view.erb
134
+ - web/public/black-case.jpg
135
+ - web/public/clear-case.jpg
136
+ - web/public/live-render.js
137
+ - web/public/white-case.jpg
138
+ - web/views/index.erb
139
+ - web/views/render_view.erb
106
140
  homepage: https://github.com/schrockwell/trmnl_preview
107
141
  licenses:
108
142
  - MIT
data/views/index.erb DELETED
@@ -1,42 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>TRMNL Preview</title>
7
- <style>
8
- body {
9
- font-family: sans-serif;
10
- background: #eee;
11
- }
12
- .menu {
13
- margin-bottom: 1em;
14
- }
15
- .menu a {
16
- padding: 0.5em 1em;
17
- background: #ddd;
18
- border-radius: 0.5em;
19
- display: inline-block;
20
- text-decoration: none;
21
- color: black;
22
- }
23
- .menu a:hover {
24
- background: #ccc;
25
- }
26
- .menu a.active {
27
- background: #333;
28
- color: white;
29
- }
30
- </style>
31
- </head>
32
- <body>
33
- <div class="menu">
34
- <a class="<%= 'active' if @view == 'full' %>" href="/full">Full</a>
35
- <a class="<%= 'active' if @view == 'half_horizontal' %>" href="/half_horizontal">Half Horizontal</a>
36
- <a class="<%= 'active' if @view == 'half_vertical' %>" href="/half_vertical">Half Vertical</a>
37
- <a class="<%= 'active' if @view == 'quadrant' %>" href="/quadrant">Quadrant</a>
38
- </div>
39
-
40
- <iframe src="/render/<%= @view %>" width="800" height="480"></iframe>
41
- </body>
42
- </html>