trmnl_preview 0.5.2 → 0.5.3

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: ff4e3150ccec1606d5368198055556d8650b129a682ae398fa7ff29e30e42009
4
- data.tar.gz: d65492ec3e770e5203bbfd736651e56b052aa726dd1ead38bff93eccb2cbf023
3
+ metadata.gz: c67d7a534ea6adba08ab5b46d40f60ec8e505123136916ba98e43991c6877f58
4
+ data.tar.gz: 5a22288cb32a76b2b8cb8d8163a38c964736e6fd7c067da6f76f9fe8c7401ec1
5
5
  SHA512:
6
- metadata.gz: b7157fc2656c9228482b11b4806280a29b9ce6ae3017af3f57a79e70b3f8f00ee125671476542808fc8411a8381e0569a4cfd19e6057ed4c7fd6a21a8ebee016
7
- data.tar.gz: c03598652d732f5922fbe7ea9b5b24c5d09e22ac2b1f793c511d59ca0739648fced9ee6c8579794298541669ffb42c5f6a37338ba523ad8b1dd193aac721a5ba
6
+ metadata.gz: 9df016907e67d1cdc0b771299162b769c83b2bb2c7d9ab4b0e5bd77cf8db3f6a8f15db93ad4a00810e0fd224cb2e3dbee1876c9fe416854f26f239704a28537a
7
+ data.tar.gz: bc4e3d5c46fc64d25a3791c77c0e43af5a84875c2ad2853742e3ba31cf7a5bbdef5a548d92a4bab4b27b048e089eae4e712cd99eec43ad1ee0902f27e39a364c
data/README.md CHANGED
@@ -22,6 +22,7 @@ This is the structure of a plugin project:
22
22
  ├── half_horizontal.liquid
23
23
  ├── half_vertical.liquid
24
24
  ├── quadrant.liquid
25
+ ├── shared.liquid
25
26
  └── settings.yml
26
27
  ```
27
28
 
@@ -0,0 +1,36 @@
1
+ require 'action_view'
2
+ require 'singleton'
3
+
4
+ module Markup
5
+ module CustomLiquidFilters
6
+ class ActionViewHelpers
7
+ include Singleton
8
+ include ActionView::Helpers
9
+ end
10
+
11
+ def number_with_delimiter(number, delimiter = ',', separator = ',')
12
+ ActionViewHelpers.instance.number_with_delimiter(number, delimiter:, separator:)
13
+ end
14
+
15
+ def number_to_currency(number, unit = '$', delimiter = ',', separator = '.')
16
+ ActionViewHelpers.instance.number_to_currency(number, unit: unit, delimiter:, separator:)
17
+ end
18
+
19
+ def l_word(word, locale)
20
+ I18n.t("custom_plugins.#{word}", locale: locale)
21
+ end
22
+
23
+ def l_date(date, format, locale = 'en')
24
+ format = format.to_sym unless format.include?('%')
25
+ I18n.l(date.to_datetime, :format => format, locale: locale)
26
+ end
27
+
28
+ def pluralize(singular, count)
29
+ ActionViewHelpers.instance.pluralize(count, singular)
30
+ end
31
+
32
+ def json(obj)
33
+ JSON.generate(obj)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ module Markup
2
+ # This in-memory "file system" is the backing storage for custom templates defined {% template [name] %} tags.
3
+ class InlineTemplatesFileSystem < Liquid::BlankFileSystem
4
+ def initialize
5
+ super
6
+ @templates = {}
7
+ end
8
+
9
+ # called by Markup::LiquidTemplateTag to save users' custom shared templates via our custom {% template %} tag
10
+ def register(name, body)
11
+ @templates[name] = body
12
+ end
13
+
14
+ # called by Liquid::Template for {% render 'foo' %} when rendering screen markup
15
+ def read_template_file(name)
16
+ @templates[name] || raise(Liquid::FileSystemError, "Template not found: #{name}")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # get all files in the current directory as this file
2
+ Pathname.new(__dir__).glob('*.rb').each { |file| require file }
3
+
4
+ module Markup
5
+ # A very thin wrapper around Liquid::Template with TRMNL-specific functionality.
6
+ class Template < Liquid::Template
7
+ def self.parse(*)
8
+ template = super
9
+
10
+ # set up a temporary in-memory file system for custom user templates, via the magic :file_system register
11
+ # which will override the default file system
12
+ template.registers[:file_system] = InlineTemplatesFileSystem.new
13
+
14
+ template
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ module Markup
2
+ # The {% template [name] %} tag block is used in conjunction with InlineTemplatesFileSystem to allow users to define
3
+ # custom templates within the context of the current Liquid template. Generally speaking, they will define their own
4
+ # templates in the "shared" markup content, which is prepended to the individual screen templates before rendering.
5
+ class TemplateTag < Liquid::Block
6
+ NAME_REGEX = %r{\A[a-zA-Z0-9_/]+\z}
7
+
8
+ def initialize(tag_name, markup, options)
9
+ super
10
+ @name = markup.strip
11
+ end
12
+
13
+ def parse(tokens)
14
+ @body = ""
15
+ while (token = tokens.shift)
16
+ break if token.strip == "{% endtemplate %}"
17
+
18
+ @body << token
19
+ end
20
+ end
21
+
22
+ def render(context)
23
+ unless @name =~ NAME_REGEX
24
+ return "Liquid error: invalid template name #{@name.inspect} - template names must contain only letters, numbers, underscores, and slashes"
25
+ end
26
+
27
+ context.registers[:file_system].register(@name, @body.strip)
28
+ ''
29
+ end
30
+ end
31
+ end
@@ -13,6 +13,7 @@ module TRMNLP
13
13
 
14
14
  api_key = prompt("API Key: ")
15
15
  raise Error, "API key cannot be empty" if api_key.empty?
16
+ raise Error, "Invalid API key; did you copy it from the right place?" unless api_key.start_with?("user_")
16
17
 
17
18
  config.app.api_key = api_key
18
19
  config.app.save
@@ -6,8 +6,8 @@ require 'json'
6
6
  require 'liquid'
7
7
 
8
8
  require_relative 'config'
9
- require_relative 'custom_filters'
10
9
  require_relative 'paths'
10
+ require_relative '../markup/template'
11
11
 
12
12
  module TRMNLP
13
13
  class Context
@@ -122,7 +122,14 @@ module TRMNLP
122
122
  template_path = paths.template(view)
123
123
  return "Missing template: #{template_path}" unless template_path.exist?
124
124
 
125
- user_template = Liquid::Template.parse(template_path.read, environment: liquid_environment)
125
+ shared_template_path = paths.shared_template
126
+ if shared_template_path.exist?
127
+ full_markup = shared_template_path.read + template_path.read
128
+ else
129
+ full_markup = template_path.read
130
+ end
131
+
132
+ user_template = Markup::Template.parse(full_markup, environment: liquid_environment)
126
133
  user_template.render(user_data)
127
134
  rescue StandardError => e
128
135
  e.message
@@ -200,7 +207,8 @@ module TRMNLP
200
207
 
201
208
  def liquid_environment
202
209
  @liquid_environment ||= Liquid::Environment.build do |env|
203
- env.register_filter(CustomFilters)
210
+ env.register_filter(Markup::CustomLiquidFilters)
211
+ env.register_tag('template', Markup::TemplateTag)
204
212
 
205
213
  config.project.user_filters.each do |module_name, relative_path|
206
214
  require paths.root_dir.join(relative_path)
data/lib/trmnlp/paths.rb CHANGED
@@ -36,6 +36,8 @@ module TRMNLP
36
36
  def plugin_config = src_dir.join('settings.yml')
37
37
 
38
38
  def template(view) = src_dir.join("#{view}.liquid")
39
+
40
+ def shared_template = template('shared')
39
41
 
40
42
  def app_config = app_config_dir.join('config.yml')
41
43
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = "0.5.2".freeze
4
+ VERSION = "0.5.3".freeze
5
5
  end
@@ -45,6 +45,7 @@ Gem::Specification.new do |spec|
45
45
  # HTML rendering
46
46
  spec.add_dependency "liquid", "~> 5.6"
47
47
  spec.add_dependency "activesupport", "~> 8.0"
48
+ spec.add_dependency "actionview", "~> 8.0"
48
49
 
49
50
  # BMP rendering
50
51
  spec.add_dependency "ferrum", "~> 0.16"
@@ -59,6 +60,7 @@ Gem::Specification.new do |spec|
59
60
  spec.add_dependency "rubyzip", "~> 2.3.0"
60
61
  spec.add_dependency "thor", "~> 1.3"
61
62
  spec.add_dependency "oj", "~> 3.16.9"
63
+ spec.add_dependency "tzinfo-data", "~> 1.2025"
62
64
 
63
65
  # For more information and examples about making a new gem, check out our
64
66
  # guide at: https://bundler.io/guides/creating_gem.html
data/web/public/index.css CHANGED
@@ -34,51 +34,6 @@ menu a.active {
34
34
  color: white;
35
35
  }
36
36
 
37
- .case {
38
- width: 1000px;
39
- height: 680px;
40
- position: relative;
41
- }
42
-
43
- iframe {
44
- position: absolute;
45
- border: none;
46
- left: 98px;
47
- top: 65px;
48
- filter: grayscale(100%);
49
- }
50
-
51
- .case .case-overlay {
52
- position: absolute;
53
- width: 100%;
54
- height: 100%;
55
- top: 0;
56
- left: 0;
57
- background-size: cover;
58
- mix-blend-mode: multiply;
59
- pointer-events: none;
60
- }
61
-
62
- .case--none .case-overlay {
63
- background: none;
64
- }
65
-
66
- .case--white .case-overlay {
67
- background-image: url("white-case.jpg");
68
- }
69
-
70
- .case--black .case-overlay {
71
- background-image: url("black-case.jpg");
72
- }
73
-
74
- .case--clear .case-overlay {
75
- background-image: url("clear-case.jpg");
76
- }
77
-
78
- .case--none iframe {
79
- border: 1px solid black;
80
- }
81
-
82
37
  .spinner {
83
38
  width: 22px;
84
39
  height: 22px;
data/web/public/index.js CHANGED
@@ -11,7 +11,7 @@ trmnlp.connectLiveRender = function () {
11
11
  const payload = JSON.parse(msg.data);
12
12
 
13
13
  if (payload.type === "reload") {
14
- trmnlp.setIframeSrc(trmnlp.iframe.src);
14
+ trmnlp.setFrameSrc(trmnlp.frame._src);
15
15
  trmnlp.userData.textContent = JSON.stringify(payload.user_data, null, 2);
16
16
  }
17
17
  };
@@ -22,9 +22,9 @@ trmnlp.connectLiveRender = function () {
22
22
  };
23
23
  };
24
24
 
25
- trmnlp.setCaseImage = function () {
25
+ trmnlp.setFrameColor = function () {
26
26
  const value = trmnlp.caseSelect.value;
27
- document.querySelector(".case").className = `case case--${value}`;
27
+ document.querySelector("trmnl-frame").setAttribute("color", value);
28
28
  localStorage.setItem("trmnlp-case", value);
29
29
  };
30
30
 
@@ -32,17 +32,17 @@ trmnlp.setPreviewFormat = function () {
32
32
  const value = trmnlp.formatSelect.value;
33
33
  localStorage.setItem("trmnlp-format", value);
34
34
 
35
- trmnlp.setIframeSrc(`/render/${trmnlp.view}.${value}`);
35
+ trmnlp.setFrameSrc(`/render/${trmnlp.view}.${value}`);
36
36
  };
37
37
 
38
- trmnlp.setIframeSrc = function (src) {
38
+ trmnlp.setFrameSrc = function (src) {
39
39
  document.querySelector(".spinner").style.display = "inline-block";
40
- trmnlp.iframe.src = src;
40
+ trmnlp.frame.setSrc(src);
41
41
  };
42
42
 
43
43
  document.addEventListener("DOMContentLoaded", function () {
44
44
  trmnlp.view = document.querySelector("meta[name='trmnl-view']").content;
45
- trmnlp.iframe = document.querySelector("iframe");
45
+ trmnlp.frame = document.querySelector("trmnl-frame");
46
46
  trmnlp.caseSelect = document.querySelector(".select-case");
47
47
  trmnlp.formatSelect = document.querySelector(".select-format");
48
48
  trmnlp.userData = document.getElementById("user-data");
@@ -58,7 +58,7 @@ document.addEventListener("DOMContentLoaded", function () {
58
58
 
59
59
  trmnlp.caseSelect.value = caseValue;
60
60
  trmnlp.caseSelect.addEventListener("change", () => {
61
- trmnlp.setCaseImage();
61
+ trmnlp.setFrameColor();
62
62
  });
63
63
 
64
64
  trmnlp.formatSelect.value = formatValue;
@@ -66,10 +66,15 @@ document.addEventListener("DOMContentLoaded", function () {
66
66
  trmnlp.setPreviewFormat();
67
67
  });
68
68
 
69
- trmnlp.iframe.addEventListener("load", () => {
69
+ trmnlp.frame._iframe.addEventListener("load", () => {
70
70
  document.querySelector(".spinner").style.display = "none";
71
+
72
+ // On page load, trmnl-frame loads "about:blank", so wait for that to load
73
+ // before updating the src to the preview.
74
+ if (trmnlp.frame._src === null) {
75
+ trmnlp.setPreviewFormat();
76
+ }
71
77
  });
72
78
 
73
- trmnlp.setCaseImage();
74
- trmnlp.setPreviewFormat();
79
+ trmnlp.setFrameColor();
75
80
  });
@@ -0,0 +1,782 @@
1
+ (function () {
2
+ // Define the HTML template as a string.
3
+ // We wrap the SVG in a container <div id="container"> to allow forwarding
4
+ // of the host element's "class" and "style" attributes.
5
+
6
+ let mainContentId = `main-content-${Math.random().toString(36).substring(2)}`;
7
+ let containerId = `container-${Math.random().toString(36).substring(2)}`;
8
+ let contentWrapperId = `container-wrapper-${Math.random()
9
+ .toString(36)
10
+ .substring(2)}`;
11
+ let trmnlComponentIframeId = `trmnl-component-iframe-${Math.random()
12
+ .toString(36)
13
+ .substring(2)}`;
14
+
15
+ const colors = {
16
+ white: {
17
+ start: "#F7F6F6",
18
+ end: "#E9E9E9",
19
+ logo: "#E9E9E9",
20
+ },
21
+ black: {
22
+ start: "#414141",
23
+ end: "#313131",
24
+ logo: "#595959",
25
+ },
26
+ mint: {
27
+ start: "#D9DED0",
28
+ end: "#CFD3C8",
29
+ logo: "#D8DCD1",
30
+ },
31
+ gray: {
32
+ start: "#696e74",
33
+ end: "#696e74",
34
+ logo: "#595959",
35
+ },
36
+ wood: {
37
+ start: "#f9e3cb",
38
+ end: "#ecd4bc",
39
+ logo: "#fdefd5",
40
+ },
41
+ };
42
+
43
+ const contentWrapperTemplate = `<!doctype html>
44
+ <html lang="en">
45
+ <head>
46
+ <link rel="stylesheet" href="https://usetrmnl.com/css/latest/plugins.css"/>
47
+ <script type="text/javascript" src="https://usetrmnl.com/js/latest/plugins.js"></script>
48
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
49
+ <meta charset="utf-8" />
50
+ <title>TRMNL</title>
51
+ </head>
52
+ <body class="trmnl" style="background-color: white !important;">
53
+ <div id="${mainContentId}">CONTENT_PLACEHOLDER</div>
54
+ </body>
55
+ </html>`;
56
+
57
+ const templateHTML = `
58
+ <style>
59
+ :host {
60
+ display: inline-block;
61
+ width: auto;
62
+ }
63
+ #${containerId} {
64
+ width: 100%;
65
+ height: auto;
66
+ }
67
+ svg {
68
+ width: 100%;
69
+ height: auto;
70
+ display: block;
71
+ }
72
+ #${mainContentId} {
73
+ width: 100%;
74
+ height: auto;
75
+ display: block;
76
+ background-opacity: 0;
77
+ }
78
+ #${trmnlComponentIframeId} {
79
+ border: none !important;
80
+ width: 800px;
81
+ height: 480px;
82
+ }
83
+ #${trmnlComponentIframeId}.dark-mode {
84
+ filter: invert(1) brightness(0.9) sepia(25%) contrast(0.75);
85
+ opacity:90%;
86
+ }
87
+ </style>
88
+ <div id="${containerId}">
89
+ <svg width="950px" height="639px" viewBox="0 0 950 639" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
90
+ <title>TRMNL</title>
91
+ <defs>
92
+ <linearGradient x1="50%" y1="0.295017483%" x2="50%" y2="98.7079327%" id="linearGradient-1">
93
+ <stop id="start" stop-color="#F7F6F6" offset="0%"></stop>
94
+ <stop id="end" stop-color="#E9E9E9" offset="100%"></stop>
95
+ </linearGradient>
96
+ <path d="M28.2027372,0 L826.111263,1.81366828e-15 C835.917969,-1.0537935e-15 839.474118,1.02108172 843.059303,2.93845974 C846.644488,4.85583776 849.458162,7.66951208 851.37554,11.2546973 C853.292918,14.8398824 854.314,18.3960311 854.314,28.2027372 L854.314,546.429263 C854.314,556.235969 853.292918,559.792118 851.37554,563.377303 C849.458162,566.962488 846.644488,569.776162 843.059303,571.69354 C839.474118,573.610918 835.917969,574.632 826.111263,574.632 L28.2027372,574.632 C18.3960311,574.632 14.8398824,573.610918 11.2546973,571.69354 C7.66951208,569.776162 4.85583776,566.962488 2.93845974,563.377303 C1.02108172,559.792118 1.79144895e-14,556.235969 -3.08323607e-14,546.429263 L0,28.2027372 C0,18.3960311 1.02108172,14.8398824 2.93845974,11.2546973 C4.85583776,7.66951208 7.66951208,4.85583776 11.2546973,2.93845974 C14.8398824,1.02108172 18.3960311,0 28.2027372,0 Z" id="path-2"></path>
97
+ <filter x="-1.3%" y="-1.7%" width="102.7%" height="104.0%" filterUnits="objectBoundingBox" id="filter-3">
98
+ <feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
99
+ <feGaussianBlur stdDeviation="3.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
100
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.342083698 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
101
+ </filter>
102
+ <path d="M38.5374834,27 L815.462517,27 C819.474351,27 820.929139,27.4177153 822.395806,28.2020972 C823.862472,28.9864791 825.013521,30.1375277 825.797903,31.6041943 C826.582285,33.070861 827,34.5256491 827,38.5374834 L827,495.462517 C827,499.474351 826.582285,500.929139 825.797903,502.395806 C825.013521,503.862472 823.862472,505.013521 822.395806,505.797903 C820.929139,506.582285 819.474351,507 815.462517,507 L38.5374834,507 C34.5256491,507 33.070861,506.582285 31.6041943,505.797903 C30.1375277,505.013521 28.9864791,503.862472 28.2020972,502.395806 C27.4177153,500.929139 27,499.474351 27,495.462517 L27,38.5374834 C27,34.5256491 27.4177153,33.070861 28.2020972,31.6041943 C28.9864791,30.1375277 30.1375277,28.9864791 31.6041943,28.2020972 C33.070861,27.4177153 34.5256491,27 38.5374834,27 Z" id="path-4"></path>
103
+ <filter x="-0.5%" y="-0.8%" width="101.0%" height="101.7%" filterUnits="objectBoundingBox" id="filter-5">
104
+ <feGaussianBlur stdDeviation="3" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
105
+ <feOffset dx="0" dy="2" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
106
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
107
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
108
+ </filter>
109
+ <polygon id="path-6" points="30.4432432 6.94287907 30.4432432 4.15135135 46.3567568 4.15135135 46.3567568 6.94287907 39.9198299 6.94287907 39.9198299 18.6810811 36.8990438 18.6810811 36.8990438 6.94287907"></polygon>
110
+ <filter x="-9.4%" y="-10.3%" width="118.9%" height="120.6%" filterUnits="objectBoundingBox" id="filter-7">
111
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
112
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
113
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
114
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
115
+ </filter>
116
+ <path d="M49.8162162,4.15135135 L49.8162162,18.6810811 L52.8523226,18.6810811 L52.8523226,13.5912453 L61.0721342,13.5912453 C61.6443807,13.5912453 62.0184369,13.7189233 62.2506098,13.9435637 C62.4783178,14.1639612 62.6184648,14.5256765 62.6184648,15.1152338 L62.6184648,18.6810811 L65.6357196,18.6810811 L65.6357196,14.7403994 C65.6357196,13.8468096 65.3447594,13.1148672 64.814929,12.600511 C64.5115665,12.3061097 64.1385024,12.0909964 63.7133483,11.9555874 C64.2208543,11.696457 64.6412955,11.3548891 64.9652461,10.932914 C65.4762248,10.2674527 65.7297297,9.42496011 65.7297297,8.44713669 C65.7297297,7.06980219 65.2916771,5.97468682 64.3582728,5.23449284 C63.4382632,4.50512742 62.0749919,4.15135135 60.2825977,4.15135135 L49.8162162,4.15135135 Z M59.75624,10.8786151 L52.8523226,10.8786151 L52.8523226,6.84422472 L59.75624,6.84422472 C60.760834,6.84422472 61.4784366,6.9832779 61.9403018,7.28832549 C62.3756259,7.57590683 62.6184648,8.03546952 62.6184648,8.8022403 C62.6184648,9.5814535 62.3733935,10.0741526 61.9328604,10.3878162 C61.4714913,10.7164471 60.7551289,10.8786151 59.75624,10.8786151 Z" id="path-8"></path>
117
+ <filter x="-9.4%" y="-10.3%" width="118.9%" height="120.6%" filterUnits="objectBoundingBox" id="filter-9">
118
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
119
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
120
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
121
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
122
+ </filter>
123
+ <polygon id="path-10" points="68.4972973 18.6810811 68.4972973 4.15135135 72.9082435 4.15135135 79.241009 15.0102802 85.554257 4.15135135 89.9459459 4.15135135 89.9459459 18.6810811 86.7802138 18.6810811 86.7802138 7.96393861 80.6210236 18.6810811 77.8417371 18.6810811 71.6825469 7.96393861 71.6825469 18.6810811"></polygon>
124
+ <filter x="-7.0%" y="-10.3%" width="114.0%" height="120.6%" filterUnits="objectBoundingBox" id="filter-11">
125
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
126
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
127
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
128
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
129
+ </filter>
130
+ <polygon id="path-12" points="97.435008 4.15135135 93.4054054 4.15135135 93.4054054 18.6810811 96.4068254 18.6810811 96.4068254 7.50994637 105.999845 18.6810811 110.010811 18.6810811 110.010811 4.15135135 107.028027 4.15135135 107.028027 15.3224861"></polygon>
131
+ <filter x="-9.0%" y="-10.3%" width="118.1%" height="120.6%" filterUnits="objectBoundingBox" id="filter-13">
132
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
133
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
134
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
135
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
136
+ </filter>
137
+ <polygon id="path-14" points="117.380019 4.15135135 114.162162 4.15135135 114.162162 18.6810811 128 18.6810811 128 15.8895534 117.380019 15.8895534"></polygon>
138
+ <filter x="-10.8%" y="-10.3%" width="121.7%" height="120.6%" filterUnits="objectBoundingBox" id="filter-15">
139
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
140
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
141
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
142
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
143
+ </filter>
144
+ <polygon id="path-16" points="6.1435832 0.691891892 13.1459459 3.37533695 11.845606 6.91891892 4.84324324 4.2354993"></polygon>
145
+ <filter x="-18.1%" y="-24.1%" width="136.1%" height="148.2%" filterUnits="objectBoundingBox" id="filter-17">
146
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
147
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
148
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
149
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
150
+ </filter>
151
+ <polygon id="path-18" points="17.5964512 0 20.0648649 7.15103824 16.3062515 8.3027027 13.8378378 1.15166697"></polygon>
152
+ <filter x="-24.1%" y="-18.1%" width="148.2%" height="136.1%" filterUnits="objectBoundingBox" id="filter-19">
153
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
154
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
155
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
156
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
157
+ </filter>
158
+ <polygon id="path-20" points="25.6 8.27152584 21.2525567 14.5297297 17.9891892 12.4852309 22.3366325 6.22702703"></polygon>
159
+ <filter x="-19.7%" y="-18.1%" width="139.4%" height="136.1%" filterUnits="objectBoundingBox" id="filter-21">
160
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
161
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
162
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
163
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
164
+ </filter>
165
+ <polygon id="path-22" points="22.8324324 20.0099988 15.5472692 20.7567568 15.2216216 16.6602715 22.5067605 15.9135135"></polygon>
166
+ <filter x="-19.7%" y="-31.0%" width="139.4%" height="161.9%" filterUnits="objectBoundingBox" id="filter-23">
167
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
168
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
169
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
170
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
171
+ </filter>
172
+ <polygon id="path-24" points="14.009405 24.9081081 8.99459459 19.2742777 11.590595 16.6054054 16.6054054 22.2392877"></polygon>
173
+ <filter x="-19.7%" y="-18.1%" width="139.4%" height="136.1%" filterUnits="objectBoundingBox" id="filter-25">
174
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
175
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
176
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
177
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
178
+ </filter>
179
+ <polygon id="path-26" points="2.76756757 20.9711149 3.9545589 13.8378378 8.3027027 14.3153716 7.11571137 21.4486486"></polygon>
180
+ <filter x="-27.1%" y="-19.7%" width="154.2%" height="139.4%" filterUnits="objectBoundingBox" id="filter-27">
181
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
182
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
183
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
184
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
185
+ </filter>
186
+ <polygon id="path-28" points="0 10.0113136 6.40614303 6.22702703 8.3027027 9.36165939 1.89655719 13.1459459"></polygon>
187
+ <filter x="-18.1%" y="-21.7%" width="136.1%" height="143.4%" filterUnits="objectBoundingBox" id="filter-29">
188
+ <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
189
+ <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
190
+ <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
191
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
192
+ </filter>
193
+ </defs>
194
+ <g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" >
195
+ <g id="Group" transform="translate(48, 32)">
196
+ <g id="Rectangle">
197
+ <use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"></use>
198
+ <use fill="url(#linearGradient-1)" fill-rule="evenodd" xlink:href="#path-2"></use>
199
+ </g>
200
+ <g id="Rectangle">
201
+ <use fill="#E4E2E1" fill-rule="evenodd" xlink:href="#path-4"></use>
202
+ <use fill="black" fill-opacity="1" filter="url(#filter-5)" xlink:href="#path-4"></use>
203
+ </g>
204
+
205
+
206
+
207
+ <foreignobject class="node" x="36" y="34" width="840" height="520"
208
+ style="transform:scale(0.98); position: relative; border-radius: 12px; opacity: 0.9; mix-blend-mode: darken;">
209
+ <div id="${contentWrapperId}"
210
+ style="position: static; width: 100%; height: 100%; max-width: 100%; max-height: 100%;">
211
+ <iframe id="${trmnlComponentIframeId}" src="about:blank"
212
+ style="display:block; position: static; top: 0; left: 0; width: 100%; height: 100%; border: none;"></iframe>
213
+ </div>
214
+ </foreignobject>
215
+
216
+ <g id="logo--brand@vector" transform="translate(363, 529)">
217
+ <g id="Path">
218
+ <use
219
+ class="logo__path"
220
+ fill="#E9E9E9"
221
+ fill-rule="evenodd"
222
+ xlink:href="#path-6"
223
+ ></use>
224
+ <use
225
+ fill="black"
226
+ fill-opacity="1"
227
+ filter="url(#filter-7)"
228
+ xlink:href="#path-6"
229
+ ></use>
230
+ </g>
231
+ <g id="Shape">
232
+ <use
233
+ class="logo__path"
234
+ fill="#E9E9E9"
235
+ fill-rule="evenodd"
236
+ xlink:href="#path-8"
237
+ ></use>
238
+ <use
239
+ fill="black"
240
+ fill-opacity="1"
241
+ filter="url(#filter-9)"
242
+ xlink:href="#path-8"
243
+ ></use>
244
+ </g>
245
+ <g id="Path">
246
+ <use
247
+ class="logo__path"
248
+ fill="#E9E9E9"
249
+ fill-rule="evenodd"
250
+ xlink:href="#path-10"
251
+ ></use>
252
+ <use
253
+ fill="black"
254
+ fill-opacity="1"
255
+ filter="url(#filter-11)"
256
+ xlink:href="#path-10"
257
+ ></use>
258
+ </g>
259
+ <g id="Path">
260
+ <use
261
+ class="logo__path"
262
+ fill="#E9E9E9"
263
+ fill-rule="evenodd"
264
+ xlink:href="#path-12"
265
+ ></use>
266
+ <use
267
+ fill="black"
268
+ fill-opacity="1"
269
+ filter="url(#filter-13)"
270
+ xlink:href="#path-12"
271
+ ></use>
272
+ </g>
273
+ <g id="Path">
274
+ <use
275
+ class="logo__path"
276
+ fill="#E9E9E9"
277
+ fill-rule="evenodd"
278
+ xlink:href="#path-14"
279
+ ></use>
280
+ <use
281
+ fill="black"
282
+ fill-opacity="1"
283
+ filter="url(#filter-15)"
284
+ xlink:href="#path-14"
285
+ ></use>
286
+ </g>
287
+ <g id="Path">
288
+ <use
289
+ class="logo__path"
290
+ fill="#E9E9E9"
291
+ fill-rule="evenodd"
292
+ xlink:href="#path-16"
293
+ ></use>
294
+ <use
295
+ fill="black"
296
+ fill-opacity="1"
297
+ filter="url(#filter-17)"
298
+ xlink:href="#path-16"
299
+ ></use>
300
+ </g>
301
+ <g id="Path">
302
+ <use
303
+ class="logo__path"
304
+ fill="#E9E9E9"
305
+ fill-rule="evenodd"
306
+ xlink:href="#path-18"
307
+ ></use>
308
+ <use
309
+ fill="black"
310
+ fill-opacity="1"
311
+ filter="url(#filter-19)"
312
+ xlink:href="#path-18"
313
+ ></use>
314
+ </g>
315
+ <g id="Path">
316
+ <use
317
+ class="logo__path"
318
+ fill="#E9E9E9"
319
+ fill-rule="evenodd"
320
+ xlink:href="#path-20"
321
+ ></use>
322
+ <use
323
+ fill="black"
324
+ fill-opacity="1"
325
+ filter="url(#filter-21)"
326
+ xlink:href="#path-20"
327
+ ></use>
328
+ </g>
329
+ <g id="Path">
330
+ <use
331
+ class="logo__path"
332
+ fill="#E9E9E9"
333
+ fill-rule="evenodd"
334
+ xlink:href="#path-22"
335
+ ></use>
336
+ <use
337
+ fill="black"
338
+ fill-opacity="1"
339
+ filter="url(#filter-23)"
340
+ xlink:href="#path-22"
341
+ ></use>
342
+ </g>
343
+ <g id="Path">
344
+ <use
345
+ class="logo__path"
346
+ fill="#E9E9E9"
347
+ fill-rule="evenodd"
348
+ xlink:href="#path-24"
349
+ ></use>
350
+ <use
351
+ fill="black"
352
+ fill-opacity="1"
353
+ filter="url(#filter-25)"
354
+ xlink:href="#path-24"
355
+ ></use>
356
+ </g>
357
+ <g id="Path">
358
+ <use
359
+ class="logo__path"
360
+ fill="#E9E9E9"
361
+ fill-rule="evenodd"
362
+ xlink:href="#path-26"
363
+ ></use>
364
+ <use
365
+ fill="black"
366
+ fill-opacity="1"
367
+ filter="url(#filter-27)"
368
+ xlink:href="#path-26"
369
+ ></use>
370
+ </g>
371
+ <g id="Path">
372
+ <use
373
+ class="logo__path"
374
+ fill="#E9E9E9"
375
+ fill-rule="evenodd"
376
+ xlink:href="#path-28"
377
+ ></use>
378
+ <use
379
+ fill="black"
380
+ fill-opacity="1"
381
+ filter="url(#filter-29)"
382
+ xlink:href="#path-28"
383
+ ></use>
384
+ </g>
385
+ </g>
386
+ </g>
387
+ </g>
388
+ </svg>
389
+ </div>
390
+ `;
391
+
392
+ class TRMNL extends HTMLElement {
393
+ constructor() {
394
+ super();
395
+ console.log("TRMNL element constructed.");
396
+ this._src = null; // Store the src attribute value
397
+ this._color = "white"; // Default color
398
+ this._contentMode = "external"; // Default to external src mode
399
+ this._htmlContent = ""; // Store HTML content
400
+ this._darkMode = null; // Store dark mode setting
401
+
402
+ // Attach a shadow root and append the template content
403
+ const shadow = this.attachShadow({ mode: "open" });
404
+ const template = document.createElement("template");
405
+ template.innerHTML = templateHTML;
406
+ shadow.appendChild(template.content.cloneNode(true));
407
+
408
+ // Get reference to the iframe for later use
409
+ this._iframe = this.shadowRoot.querySelector(
410
+ `#${trmnlComponentIframeId}`
411
+ );
412
+
413
+ // Setup iframe load handler once
414
+ if (this._iframe) {
415
+ this._iframe.onload = () => {
416
+ this._handleIframeLoaded();
417
+ };
418
+ }
419
+
420
+ // Process any slotted content
421
+ this._processSlottedContent();
422
+
423
+ // Initialize dark mode media query listener if needed
424
+ this._darkModeMediaQuery = window.matchMedia(
425
+ "(prefers-color-scheme: dark)"
426
+ );
427
+ this._darkModeMediaQuery.addEventListener("change", () => {
428
+ this._updateDarkMode();
429
+ });
430
+ }
431
+
432
+ _handleIframeLoaded() {
433
+ if (this._contentMode === "internal" && this._htmlContent) {
434
+ try {
435
+ const currentSrc = this._iframe.getAttribute("src");
436
+ if (currentSrc === "about:blank") {
437
+ const iframeDoc = this._iframe.contentWindow.document;
438
+
439
+ // Step 1: Write the base shell with placeholder container
440
+ iframeDoc.open();
441
+ iframeDoc.write(
442
+ contentWrapperTemplate.replace(
443
+ `<div id="${mainContentId}">CONTENT_PLACEHOLDER</div>`,
444
+ `<div id="${mainContentId}"></div>`
445
+ )
446
+ );
447
+ iframeDoc.close();
448
+
449
+ // Step 2: Wait for the contentWrapperTemplate to parse & render
450
+ this._iframe.contentWindow.addEventListener(
451
+ "DOMContentLoaded",
452
+ () => {
453
+ const container = iframeDoc.getElementById(mainContentId);
454
+ if (!container) {
455
+ console.warn(
456
+ `${mainContentId} container not found inside iframe`
457
+ );
458
+ return;
459
+ }
460
+
461
+ // Step 3: Apply the transition & inject content
462
+ // Check if not Safari and if startViewTransition is available
463
+ const isSafari = /^((?!chrome|android).)*safari/i.test(
464
+ navigator.userAgent
465
+ );
466
+ if (
467
+ !isSafari &&
468
+ typeof iframeDoc.startViewTransition === "function"
469
+ ) {
470
+ iframeDoc.startViewTransition(() => {
471
+ container.innerHTML = this._htmlContent;
472
+ });
473
+ } else {
474
+ container.innerHTML = this._htmlContent;
475
+ }
476
+
477
+ console.log(
478
+ "Dynamic content injected" +
479
+ (!isSafari ? " with View Transition" : "")
480
+ );
481
+ },
482
+ { once: true }
483
+ );
484
+
485
+ const isSafari = /^((?!chrome|android).)*safari/i.test(
486
+ navigator.userAgent
487
+ );
488
+ if (isSafari) {
489
+ console.log("Safari browser detected");
490
+ }
491
+ // Safari needs additional help to render content correctly
492
+ if (isSafari) {
493
+ const container = iframeDoc.getElementById(mainContentId);
494
+
495
+ setTimeout(() => {
496
+ if (container && this._htmlContent) {
497
+ container.innerHTML = this._htmlContent;
498
+ console.log(
499
+ "Applied additional Safari-specific content update"
500
+ );
501
+ }
502
+ }, 100);
503
+ }
504
+ }
505
+ } catch (e) {
506
+ console.error("Error injecting HTML with transition into iframe:", e);
507
+ }
508
+ }
509
+ }
510
+
511
+ static get observedAttributes() {
512
+ return ["src", "class", "style", "color", "dark"];
513
+ }
514
+
515
+ attributeChangedCallback(name, oldValue, newValue) {
516
+ if (name === "src") {
517
+ console.log(
518
+ `attributeChangedCallback: 'src' changed from ${oldValue} to ${newValue}`
519
+ );
520
+ this._src = newValue;
521
+ this._contentMode = "external";
522
+ this.updateIframe();
523
+ } else if (name === "class" || name === "style") {
524
+ this.updateContainerAttribute(name, newValue);
525
+ } else if (name === "color") {
526
+ this._color = newValue || "white"; // Default to white if not specified
527
+ this.updateColors();
528
+ } else if (name === "dark") {
529
+ this._darkMode = newValue;
530
+ this._updateDarkMode();
531
+ }
532
+ }
533
+
534
+ connectedCallback() {
535
+ console.log("TRMNL connectedCallback invoked.");
536
+
537
+ // Process any existing content from innerHTML
538
+ this._processSlottedContent();
539
+
540
+ if (this.hasAttribute("src")) {
541
+ const src = this.getAttribute("src");
542
+ console.log("connectedCallback found src attribute:", src);
543
+ this._src = src;
544
+ this._contentMode = "external";
545
+ this.updateIframe();
546
+ }
547
+
548
+ // Initialize container attributes for class and style
549
+ this.updateContainerAttribute("class", this.getAttribute("class"));
550
+ this.updateContainerAttribute("style", this.getAttribute("style"));
551
+
552
+ // Initialize color
553
+ if (this.hasAttribute("color")) {
554
+ this._color = this.getAttribute("color");
555
+ }
556
+ this.updateColors();
557
+
558
+ // Initialize dark mode
559
+ if (this.hasAttribute("dark")) {
560
+ this._darkMode = this.getAttribute("dark");
561
+ this._updateDarkMode();
562
+ }
563
+
564
+ // Set up the mutation observer to handle content changes
565
+ this._setupMutationObserver();
566
+ }
567
+
568
+ disconnectedCallback() {
569
+ // Clean up the mutation observer when element is removed
570
+ if (this._observer) {
571
+ this._observer.disconnect();
572
+ }
573
+
574
+ // Remove media query listener
575
+ if (this._darkModeMediaQuery) {
576
+ this._darkModeMediaQuery.removeEventListener("change", () => {
577
+ this._updateDarkMode();
578
+ });
579
+ }
580
+ }
581
+
582
+ _updateDarkMode() {
583
+ if (!this._iframe) return;
584
+
585
+ // Step 1: Remove the class
586
+ this._iframe.classList.remove("dark-mode");
587
+
588
+ // Step 2: Force reflow by reading offsetHeight
589
+ // This ensures the class removal is flushed
590
+ void this._iframe.offsetHeight;
591
+
592
+ // Step 3: Re-apply the class based on logic
593
+ const shouldEnableDark =
594
+ this._darkMode === "true" ||
595
+ (this._darkMode === "auto" && this._darkModeMediaQuery.matches);
596
+
597
+ if (shouldEnableDark) {
598
+ this._iframe.classList.add("dark-mode");
599
+ }
600
+ }
601
+
602
+ _setupMutationObserver() {
603
+ // Create a MutationObserver to detect content changes
604
+ this._observer = new MutationObserver((mutations) => {
605
+ mutations.forEach((mutation) => {
606
+ if (mutation.type === "childList") {
607
+ this._processSlottedContent();
608
+ }
609
+ });
610
+ });
611
+
612
+ // Observe changes to the element's child nodes
613
+ this._observer.observe(this, { childList: true });
614
+ }
615
+
616
+ _processSlottedContent() {
617
+ // Check if there's content inside the tags
618
+ if (this.innerHTML.trim()) {
619
+ // Set content mode to internal and store HTML
620
+ this._contentMode = "internal";
621
+ this._htmlContent = this.innerHTML;
622
+ this._prepareThenInjectHTML();
623
+ }
624
+ }
625
+
626
+ _prepareThenInjectHTML() {
627
+ if (!this._iframe) {
628
+ console.error("Iframe not found in shadow DOM");
629
+ return;
630
+ }
631
+
632
+ // Set iframe to about:blank to ensure same-origin
633
+ // This will trigger the onload handler which will inject the content
634
+ this._iframe.setAttribute("src", "about:blank");
635
+ }
636
+
637
+ updateIframe() {
638
+ // Delay updating the iframe until the next animation frame
639
+ requestAnimationFrame(() => {
640
+ if (this._iframe) {
641
+ if (this._contentMode === "external" && this._src) {
642
+ this._iframe.setAttribute("src", this._src);
643
+ console.log(`iframe src set to: ${this._src}`);
644
+ } else if (this._contentMode === "internal") {
645
+ // For internal content, ensure we're at about:blank
646
+ this._prepareThenInjectHTML();
647
+ }
648
+ } else {
649
+ console.error(
650
+ "iframe element not found in shadow DOM during updateIframe."
651
+ );
652
+ }
653
+ });
654
+ }
655
+
656
+ updateContainerAttribute(name, value) {
657
+ const container = this.shadowRoot.querySelector("#container");
658
+ if (container) {
659
+ if (value !== null) {
660
+ container.setAttribute(name, value);
661
+ } else {
662
+ container.removeAttribute(name);
663
+ }
664
+ }
665
+ }
666
+
667
+ updateColors() {
668
+ // Validate that the color exists in our colors object
669
+ if (!colors[this._color]) {
670
+ console.warn(`Color "${this._color}" not found, defaulting to white`);
671
+ this._color = "white";
672
+ }
673
+
674
+ const colorSet = colors[this._color];
675
+
676
+ // Update the gradient stops
677
+ const startStop = this.shadowRoot.querySelector("#start");
678
+ const endStop = this.shadowRoot.querySelector("#end");
679
+
680
+ if (startStop && endStop) {
681
+ startStop.setAttribute("stop-color", colorSet.start);
682
+ endStop.setAttribute("stop-color", colorSet.end);
683
+ console.log(`Updated gradient colors to ${this._color} theme`);
684
+ }
685
+
686
+ // Update all SVG elements in the logo that use the fill color
687
+ const logoGroup = this.shadowRoot.querySelector("#logo--brand\\@vector");
688
+ if (logoGroup) {
689
+ // Find all 'use' elements with fill="#E9E9E9" and update them
690
+ const logoElements = logoGroup.querySelectorAll(".logo__path");
691
+ logoElements.forEach((element) => {
692
+ element.setAttribute("fill", colorSet.logo);
693
+ });
694
+ }
695
+ }
696
+
697
+ // JavaScript API methods
698
+
699
+ /**
700
+ * Set the color theme: 'white', 'black', or 'mint'
701
+ * @param {string} color - The color theme to use
702
+ */
703
+ setColor(color) {
704
+ this._color = color;
705
+ this.updateColors();
706
+ return this; // For chaining
707
+ }
708
+
709
+ /**
710
+ * Set dark mode: 'true', 'false', or 'auto'
711
+ * @param {string} mode - The dark mode setting
712
+ */
713
+ setDark(mode) {
714
+ this._darkMode = mode;
715
+ this._updateDarkMode();
716
+ return this; // For chaining
717
+ }
718
+
719
+ /**
720
+ * Set HTML content directly, wrapping it in the template
721
+ * @param {string} html - HTML content to display
722
+ */
723
+ setHTML(html) {
724
+ this._contentMode = "internal";
725
+ this._htmlContent = html;
726
+ this._prepareThenInjectHTML();
727
+ return this; // For chaining
728
+ }
729
+
730
+ /**
731
+ * Clear all content
732
+ */
733
+ clearContent() {
734
+ if (this._iframe) {
735
+ this._htmlContent = "";
736
+
737
+ if (this._contentMode === "internal") {
738
+ // Reset to about:blank to clear content
739
+ this._iframe.setAttribute("src", "about:blank");
740
+ } else {
741
+ // For external mode, set src to empty
742
+ this._src = "";
743
+ this._iframe.setAttribute("src", "about:blank");
744
+ }
745
+ }
746
+ return this; // For chaining
747
+ }
748
+
749
+ /**
750
+ * Set iframe source to an external URL
751
+ * @param {string} src - URL to load in the iframe
752
+ */
753
+ setSrc(src) {
754
+ this._contentMode = "external";
755
+ this._src = src;
756
+ this.updateIframe();
757
+ return this; // For chaining
758
+ }
759
+
760
+ /**
761
+ * Get current HTML content (for internal content mode only)
762
+ * @returns {string} The current HTML content or empty string if in external mode
763
+ */
764
+ getHTML() {
765
+ // Simply return the stored HTML content
766
+ return this._contentMode === "internal" ? this._htmlContent : "";
767
+ }
768
+
769
+ /**
770
+ * Set dark mode: 'true', 'false', or 'auto'
771
+ * @param {string} mode - The dark mode setting
772
+ */
773
+ setDarkMode(mode) {
774
+ this._darkMode = mode;
775
+ this._updateDarkMode();
776
+ return this; // For chaining
777
+ }
778
+ }
779
+
780
+ // Register the custom element using a hyphenated tag name
781
+ customElements.define("trmnl-frame", TRMNL);
782
+ })();
data/web/views/index.erb CHANGED
@@ -9,6 +9,7 @@
9
9
  <title>TRMNL Preview</title>
10
10
 
11
11
  <link rel="stylesheet" href="/index.css">
12
+ <script src="/trmnl-component.js" defer></script>
12
13
  <script src="/index.js"></script>
13
14
  </head>
14
15
  <body>
@@ -20,27 +21,25 @@
20
21
  <a class="<%= 'active' if @view == 'half_vertical' %>" href="/half_vertical">Half Vertical</a>
21
22
  <a class="<%= 'active' if @view == 'quadrant' %>" href="/quadrant">Quadrant</a>
22
23
  </div>
23
- <div style="display: flex; gap: 0.3em;">
24
+ <div style="display: flex; gap: 0.3em; align-items: center;">
24
25
  <div class="spinner" style="display: none;"></div>
25
26
  <select class="select-format">
26
27
  <option value="bmp">BMP</option>
27
28
  <option value="html" selected>HTML</option>
28
29
  </select>
29
30
  <select class="select-case">
30
- <option value="none">None</option>
31
31
  <option value="white">White</option>
32
- <option value="black" selected>Black</option>
33
- <option value="clear">Clear</option>
32
+ <option value="black">Black</option>
33
+ <option value="mint">Mint</option>
34
+ <option value="gray">Gray</option>
35
+ <option value="wood">Wood</option>
34
36
  </select>
35
37
 
36
38
  <a href="/poll">Poll</a>
37
39
  </div>
38
40
  </menu>
39
41
 
40
- <div class="case case--black">
41
- <iframe width="800" height="480"></iframe>
42
- <div class="case-overlay"></div>
43
- </div>
42
+ <trmnl-frame>Rendering…</trmnl-frame>
44
43
 
45
44
  <pre id="user-data"><%= @user_data %></pre>
46
45
  </main>
@@ -10,7 +10,7 @@
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
12
  </head>
13
- <body class="environment trmnl">
13
+ <body class="environment trmnl" style="background: white">
14
14
  <div class="<%= @screen_classes %>">
15
15
  <div class="view view--<%= @view %>">
16
16
  <%= yield %>
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trmnl_preview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-09 00:00:00.000000000 Z
10
+ date: 2025-05-22 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: sinatra
@@ -93,6 +93,20 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '8.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: actionview
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '8.0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '8.0'
96
110
  - !ruby/object:Gem::Dependency
97
111
  name: ferrum
98
112
  requirement: !ruby/object:Gem::Requirement
@@ -233,6 +247,20 @@ dependencies:
233
247
  - - "~>"
234
248
  - !ruby/object:Gem::Version
235
249
  version: 3.16.9
250
+ - !ruby/object:Gem::Dependency
251
+ name: tzinfo-data
252
+ requirement: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - "~>"
255
+ - !ruby/object:Gem::Version
256
+ version: '1.2025'
257
+ type: :runtime
258
+ prerelease: false
259
+ version_requirements: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - "~>"
262
+ - !ruby/object:Gem::Version
263
+ version: '1.2025'
236
264
  description: Automatically rebuild and preview TRNML plugins in multiple views
237
265
  email:
238
266
  - rockwell@schrock.me
@@ -245,6 +273,10 @@ files:
245
273
  - LICENSE.txt
246
274
  - README.md
247
275
  - bin/trmnlp
276
+ - lib/markup/custom_liquid_filters.rb
277
+ - lib/markup/inline_templates_file_system.rb
278
+ - lib/markup/template.rb
279
+ - lib/markup/template_tag.rb
248
280
  - lib/trmnlp.rb
249
281
  - lib/trmnlp/api_client.rb
250
282
  - lib/trmnlp/app.rb
@@ -263,7 +295,6 @@ files:
263
295
  - lib/trmnlp/config/plugin.rb
264
296
  - lib/trmnlp/config/project.rb
265
297
  - lib/trmnlp/context.rb
266
- - lib/trmnlp/custom_filters.rb
267
298
  - lib/trmnlp/paths.rb
268
299
  - lib/trmnlp/screen_generator.rb
269
300
  - lib/trmnlp/version.rb
@@ -279,6 +310,7 @@ files:
279
310
  - web/public/clear-case.jpg
280
311
  - web/public/index.css
281
312
  - web/public/index.js
313
+ - web/public/trmnl-component.js
282
314
  - web/public/white-case.jpg
283
315
  - web/views/index.erb
284
316
  - web/views/render_html.erb
@@ -1,14 +0,0 @@
1
- require 'active_support'
2
-
3
- module TRMNLP
4
- module CustomFilters
5
- # TODO: sync up with core
6
- def number_with_delimiter(*args)
7
- ActiveSupport::NumberHelper.number_to_delimited(*args)
8
- end
9
-
10
- def number_to_currency(*args)
11
- ActiveSupport::NumberHelper.number_to_currency(*args)
12
- end
13
- end
14
- end