postnhost 0.1.0 → 0.1.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +11 -9
  4. data/Rakefile +28 -0
  5. data/app/assets/builds/postnhost/application.css +1 -1
  6. data/app/assets/stylesheets/application.tailwind.css +5 -1
  7. data/app/helpers/postnhost/application_helper.rb +0 -11
  8. data/app/views/layouts/postnhost/application.html.erb +2 -2
  9. data/app/views/layouts/postnhost/articles.html.erb +3 -3
  10. data/app/views/layouts/postnhost/product.html.erb +3 -3
  11. data/app/views/layouts/postnhost/public/templates/default.html.erb +2 -2
  12. data/app/views/layouts/postnhost/public/templates/swiss-editorial.html.erb +2 -2
  13. data/app/views/layouts/postnhost/public/templates/workspace-journal.html.erb +2 -2
  14. data/app/views/postnhost/onboarding/_post_install_checklist.html.erb +8 -8
  15. data/app/views/postnhost/pages/index.html.erb +1 -1
  16. data/app/views/postnhost/templates/edit.html.erb +1 -1
  17. data/lib/generators/postnhost/install/install_generator.rb +11 -12
  18. data/lib/generators/postnhost/locale/locale_generator.rb +1 -1
  19. data/lib/generators/postnhost/static_pages/static_pages_generator.rb +1 -1
  20. data/lib/generators/postnhost/tailwindcss/install_generator.rb +28 -25
  21. data/lib/generators/postnhost/tailwindcss/templates/host.tailwind.css +0 -3
  22. data/lib/generators/postnhost/user/user_generator.rb +1 -1
  23. data/lib/generators/postnhost/views/views_generator.rb +5 -7
  24. data/lib/postnhost/css_build.rb +68 -0
  25. data/lib/postnhost/css_scope.rb +77 -0
  26. data/lib/postnhost/sample_data.rb +1 -1
  27. data/lib/postnhost/tailwind_compiler.rb +75 -0
  28. data/lib/postnhost/tailwind_input.rb +32 -0
  29. data/lib/postnhost/version.rb +1 -1
  30. data/lib/tasks/postnhost_tasks.rake +40 -65
  31. metadata +19 -2
  32. data/lib/generators/postnhost/tailwindcss/templates/tailwind.config.js +0 -8
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="<%= document_lang %>" class="[scrollbar-gutter:stable]">
2
+ <html lang="<%= document_lang %>" data-postnhost>
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
@@ -10,7 +10,7 @@
10
10
  <%= csrf_meta_tags %>
11
11
  <%= csp_meta_tag %>
12
12
 
13
- <%= stylesheet_link_tag(*postnhost_stylesheet_assets, "data-turbo-track": "reload") %>
13
+ <%= stylesheet_link_tag "postnhost/application", "data-turbo-track": "reload" %>
14
14
  <%= javascript_include_tag "postnhost/application", "data-turbo-track": "reload", type: "module" %>
15
15
 
16
16
  <%= render 'layouts/postnhost/favicon' %>
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="<%= document_lang %>" class="[scrollbar-gutter:stable]">
2
+ <html lang="<%= document_lang %>" data-postnhost>
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
@@ -10,7 +10,7 @@
10
10
  <%= csrf_meta_tags %>
11
11
  <%= csp_meta_tag %>
12
12
 
13
- <%= stylesheet_link_tag(*postnhost_stylesheet_assets, "data-turbo-track": "reload") %>
13
+ <%= stylesheet_link_tag "postnhost/application", "data-turbo-track": "reload" %>
14
14
  <%= javascript_include_tag "postnhost/application", "data-turbo-track": "reload", type: "module" %>
15
15
 
16
16
  <%= render "layouts/postnhost/favicon" %>
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="<%= document_lang %>" class="[scrollbar-gutter:stable]">
2
+ <html lang="<%= document_lang %>" data-postnhost>
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
@@ -10,7 +10,7 @@
10
10
  <%= csrf_meta_tags %>
11
11
  <%= csp_meta_tag %>
12
12
 
13
- <%= stylesheet_link_tag(*postnhost_stylesheet_assets, "data-turbo-track": "reload") %>
13
+ <%= stylesheet_link_tag "postnhost/application", "data-turbo-track": "reload" %>
14
14
  <%= javascript_include_tag "postnhost/application", "data-turbo-track": "reload", type: "module" %>
15
15
 
16
16
  <%= render "layouts/postnhost/favicon" %>
@@ -1,29 +1,29 @@
1
1
  <div class="space-y-5">
2
2
  <p class="text-sm text-gray-600">
3
- Final manual checks for host app integration. Most of these are inherited from the root app and can be customized there. You can find this checklist later in the README.
3
+ Review these host-app settings before deployment. Details are available in the Production checklist section of the README.
4
4
  </p>
5
5
 
6
6
  <div class="rounded-xl border border-gray-200 bg-gray-50 p-4">
7
7
  <ul class="space-y-3 text-sm text-gray-700">
8
8
  <li>
9
9
  <span class="font-medium text-gray-900">1. Favicon</span><br>
10
- Inherited from the root app. Update host favicon files/links if needed.
10
+ Replace the favicon and related icon files in the host app if needed.
11
11
  </li>
12
12
  <li>
13
- <span class="font-medium text-gray-900">2. 404 / 500 pages</span><br>
14
- Inherited from root app static error pages (`public/404.html` and `public/500.html`).
13
+ <span class="font-medium text-gray-900">2. 404 and 500 pages</span><br>
14
+ Review the host app&rsquo;s static error pages at <code>public/404.html</code> and <code>public/500.html</code>.
15
15
  </li>
16
16
  <li>
17
17
  <span class="font-medium text-gray-900">3. robots.txt</span><br>
18
- Inherited from root app (`public/robots.txt`). Confirm the sitemap URL matches your mount path.
18
+ Update the host app&rsquo;s <code>public/robots.txt</code> and confirm that the sitemap URL matches your mount path.
19
19
  </li>
20
20
  <li>
21
21
  <span class="font-medium text-gray-900">4. Public template styling</span><br>
22
- Customize by running <code>rails g postnhost:views --views_scope=minimal</code> (or <code>full</code>) and editing copied templates in the host app.
22
+ To customize the public site, run <code>bin/rails g postnhost:views --views-scope=minimal</code> or use <code>--views-scope=full</code>, then edit the copied templates.
23
23
  </li>
24
24
  <li>
25
- <span class="font-medium text-gray-900">5. S3 upload settings</span><br>
26
- Configure credentials/bucket/endpoint in host app credentials and initializer (`config/initializers/postnhost.rb`).
25
+ <span class="font-medium text-gray-900">5. Production uploads</span><br>
26
+ The generated CarrierWave initializer uses S3-compatible storage in production. Configure it through Rails credentials or <code>config/initializers/postnhost.rb</code>, or replace that storage configuration.
27
27
  </li>
28
28
  </ul>
29
29
  </div>
@@ -104,7 +104,7 @@
104
104
  </div>
105
105
  <% else %>
106
106
  <div class="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600">
107
- No code-defined static pages found. Add templates in <code>app/views/postnhost/static_pages</code> (for example, <code>about.html.erb</code> maps to <code>/about</code>), or run <code>rails g postnhost:static_pages about</code> to scaffold a starter template.
107
+ No code-defined static pages found. Add templates in <code>app/views/postnhost/static_pages</code> (for example, <code>about.html.erb</code> maps to <code>/about</code>), or run <code>bin/rails g postnhost:static_pages about</code> to scaffold a starter template.
108
108
  </div>
109
109
  <% end %>
110
110
  </div>
@@ -55,7 +55,7 @@
55
55
  <p class="text-sm leading-6 text-gray-600">If your changes add new Tailwind classes, install and run the optional host Tailwind pipeline:</p>
56
56
  <div class="space-y-1">
57
57
  <code class="block overflow-x-auto rounded-lg bg-gray-900 px-3 py-2 text-xs leading-5 text-white">bin/rails g postnhost:tailwindcss:install</code>
58
- <code class="block overflow-x-auto rounded-lg bg-gray-900 px-3 py-2 text-xs leading-5 text-white">bundle exec rails postnhost:tailwindcss:watch</code>
58
+ <code class="block overflow-x-auto rounded-lg bg-gray-900 px-3 py-2 text-xs leading-5 text-white">bin/rails postnhost:tailwindcss:watch</code>
59
59
  </div>
60
60
  </div>
61
61
  </div>
@@ -5,7 +5,7 @@ module Postnhost
5
5
 
6
6
  source_root File.expand_path("templates", __dir__)
7
7
 
8
- desc "Creates Postnhost initializer and mounts the engine"
8
+ desc "Creates the PostnHost initializer and mounts the engine"
9
9
 
10
10
  def create_initializer
11
11
  template "postnhost.rb", "config/initializers/postnhost.rb"
@@ -24,7 +24,7 @@ module Postnhost
24
24
  def show_post_install
25
25
  say ""
26
26
  say "=" * 60
27
- say "Postnhost has been installed!", :green
27
+ say "PostnHost has been installed!", :green
28
28
  say "=" * 60
29
29
  say ""
30
30
  say "Next steps:"
@@ -32,23 +32,22 @@ module Postnhost
32
32
  say " bin/rails postnhost:install:migrations"
33
33
  say " bin/rails db:migrate"
34
34
  say ""
35
- say " 2. Open first-time setup in the browser:"
36
- say " http://localhost:3000#{DEFAULT_MOUNT_PATH}/onboarding"
35
+ say " 2. Open first-time setup in your app:"
36
+ say " #{DEFAULT_MOUNT_PATH}/onboarding"
37
37
  say " Or create a CMS user interactively: bin/rails g postnhost:user"
38
38
  say ""
39
- say " 3. Update config/initializers/postnhost.rb with your site settings."
39
+ say " 3. Review config/initializers/postnhost.rb for optional defaults, production uploads, and AI translations."
40
40
  say ""
41
41
  say " 4. (Optional) Customize public views:"
42
- say " rails g postnhost:views --views-scope=minimal"
43
- say " # or"
44
- say " rails g postnhost:views --views-scope=full"
42
+ say " bin/rails g postnhost:views --views-scope=minimal"
43
+ say " or"
44
+ say " bin/rails g postnhost:views --views-scope=full"
45
45
  say ""
46
- say " 5. (Optional) Enable host Tailwind pipeline for custom Postnhost classes:"
47
- say " rails g postnhost:tailwindcss:install"
48
- say " bundle exec rails postnhost:tailwindcss:watch"
46
+ say " 5. (Optional) Enable host Tailwind support for custom PostnHost classes:"
47
+ say " bin/rails g postnhost:tailwindcss:install"
49
48
  say ""
50
49
  say " 6. (Optional) Add a new public locale (copy English strings into your app):"
51
- say " rails g postnhost:locale sv"
50
+ say " bin/rails g postnhost:locale sv"
52
51
  say ""
53
52
  end
54
53
  end
@@ -5,7 +5,7 @@ module Postnhost
5
5
  class LocaleGenerator < Rails::Generators::Base
6
6
  argument :locale, type: :string, desc: "Locale code to match Language#html_lang (e.g. sv, pt-BR)"
7
7
 
8
- desc "Copies Postnhost English locale (en.yml) to config/locales/<locale>.yml in your app for translation."
8
+ desc "Copies the PostnHost English locale (en.yml) to config/locales/<locale>.yml in your app for translation."
9
9
 
10
10
  def copy_postnhost_locale
11
11
  code = locale.to_s.strip
@@ -7,7 +7,7 @@ module Postnhost
7
7
 
8
8
  def create_static_pages
9
9
  if slugs.empty?
10
- say("Please provide at least one page slug, e.g. rails g postnhost:static_pages terms privacy", :red)
10
+ say("Please provide at least one page slug, e.g. bin/rails g postnhost:static_pages terms privacy", :red)
11
11
  return
12
12
  end
13
13
 
@@ -7,19 +7,12 @@ module Postnhost
7
7
  source_root File.expand_path("templates", __dir__)
8
8
 
9
9
  namespace "postnhost:tailwindcss:install"
10
- desc "Install host Tailwind pipeline for Postnhost view overrides."
10
+ desc "Install host Tailwind support for PostnHost view overrides."
11
11
 
12
12
  def create_stylesheet_directory
13
13
  empty_directory "app/assets/stylesheets/postnhost"
14
14
  end
15
15
 
16
- def create_tailwind_config
17
- path = Rails.root.join("postnhost.tailwind.config.js")
18
- return if path.exist?
19
-
20
- copy_file "tailwind.config.js", path
21
- end
22
-
23
16
  def create_host_stylesheet
24
17
  path = Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css")
25
18
  return if path.exist?
@@ -31,12 +24,14 @@ module Postnhost
31
24
  path = Rails.root.join("package.json")
32
25
  return say_package_json_instructions unless path.exist?
33
26
 
27
+ @package_json_present = true
28
+
34
29
  json_data = JSON.parse(File.read(path))
35
30
  json_data["scripts"] ||= {}
36
31
  json_data["devDependencies"] ||= {}
37
32
 
38
- json_data["scripts"]["postnhost:tailwindcss"] = tailwind_build_command
39
- json_data["scripts"]["postnhost:tailwindcss:watch"] = "#{tailwind_build_command} --watch"
33
+ json_data["scripts"]["postnhost:tailwindcss"] = "bundle exec rails postnhost:tailwindcss:build"
34
+ json_data["scripts"]["postnhost:tailwindcss:watch"] = "bundle exec rails postnhost:tailwindcss:watch"
40
35
 
41
36
  ensure_dependency(json_data, "tailwindcss", "^4.1.12")
42
37
  ensure_dependency(json_data, "@tailwindcss/cli", "^4.1.12")
@@ -44,8 +39,7 @@ module Postnhost
44
39
 
45
40
  File.write(path, "#{JSON.pretty_generate(json_data)}\n")
46
41
 
47
- say "Updated package.json script:", :green
48
- say " yarn postnhost:tailwindcss:watch"
42
+ say "Updated package.json with PostnHost Tailwind scripts and dependencies.", :green
49
43
  end
50
44
 
51
45
  def wire_procfile_dev
@@ -54,32 +48,41 @@ module Postnhost
54
48
 
55
49
  if path.exist?
56
50
  if File.read(path).include?("postnhost:tailwindcss:watch")
57
- say "Procfile.dev already includes Postnhost watcher.", :green
51
+ say "Procfile.dev already includes the PostnHost watcher.", :green
58
52
  else
59
53
  append_to_file path, "\n#{entry}\n"
60
- say "Added Postnhost watcher to Procfile.dev.", :green
54
+ say "Added the PostnHost watcher to Procfile.dev.", :green
61
55
  end
62
56
  else
63
57
  create_file "Procfile.dev", "#{entry}\n"
64
- say "Created Procfile.dev with Postnhost watcher.", :green
58
+ say "Created Procfile.dev with the PostnHost watcher.", :green
65
59
  end
66
60
  end
67
61
 
68
62
  def print_next_steps
69
63
  say ""
70
- say "Postnhost host-tailwind mode installed.", :green
64
+ say "PostnHost host Tailwind support installed.", :green
71
65
  say "Next steps:"
72
- say " 1. Run bin/dev (Postnhost watcher is wired in Procfile.dev)."
73
- say " 2. Keep default layout tags; Postnhost auto-loads postnhost/host when config is present."
66
+ step = 1
67
+
68
+ if @package_json_present
69
+ say " #{step}. Install dependencies with the JavaScript package manager used by this app."
70
+ step += 1
71
+ end
72
+
73
+ if Rails.root.join("bin/dev").exist?
74
+ say " #{step}. Run bin/dev; the PostnHost watcher is wired in Procfile.dev."
75
+ else
76
+ say " #{step}. Run bin/rails postnhost:tailwindcss:watch alongside your Rails server."
77
+ end
78
+
79
+ say " #{step + 1}. Use normal Tailwind classes in copied PostnHost views."
80
+ say " #{step + 2}. Keep the default layout tags; the combined build overrides postnhost/application automatically."
74
81
  say ""
75
82
  end
76
83
 
77
84
  private
78
85
 
79
- def tailwind_build_command
80
- "./node_modules/.bin/tailwindcss -i ./app/assets/stylesheets/postnhost/host.tailwind.css -o ./app/assets/builds/postnhost/host.css -c ./postnhost.tailwind.config.js --minify"
81
- end
82
-
83
86
  def ensure_dependency(json_data, package_name, version)
84
87
  return if json_data["dependencies"]&.key?(package_name) || json_data["devDependencies"].key?(package_name)
85
88
 
@@ -87,9 +90,9 @@ module Postnhost
87
90
  end
88
91
 
89
92
  def say_package_json_instructions
90
- say "package.json not found. Add these scripts manually:", :yellow
91
- say " \"postnhost:tailwindcss\": \"#{tailwind_build_command}\""
92
- say " \"postnhost:tailwindcss:watch\": \"#{tailwind_build_command} --watch\""
93
+ say "package.json not found. Add a Tailwind CSS 4 compiler to the host Gemfile:", :yellow
94
+ say ' gem "tailwindcss-ruby", "~> 4.0"'
95
+ say "Then run bundle install."
93
96
  end
94
97
  end
95
98
  end
@@ -1,6 +1,3 @@
1
- @import "tailwindcss";
2
- @plugin "@tailwindcss/typography";
3
-
4
1
  @source "../../../views/postnhost/**/*.erb";
5
2
  @source "../../../views/layouts/postnhost/**/*.erb";
6
3
  @source "../../../helpers/**/*.rb";
@@ -1,7 +1,7 @@
1
1
  module Postnhost
2
2
  module Generators
3
3
  class UserGenerator < Rails::Generators::Base
4
- desc "Interactively creates a Postnhost CMS user"
4
+ desc "Interactively creates a PostnHost CMS user"
5
5
 
6
6
  def create_user
7
7
  attributes = prompt_for_user_attributes
@@ -1,7 +1,7 @@
1
1
  module Postnhost
2
2
  module Generators
3
3
  class ViewsGenerator < Rails::Generators::Base
4
- desc "Copies public Postnhost views to your application for customization"
4
+ desc "Copies public PostnHost views to your application for customization"
5
5
  class_option :views_scope,
6
6
  type: :string,
7
7
  required: true,
@@ -28,7 +28,7 @@ module Postnhost
28
28
  copy_file "layouts/postnhost/public_header/_dashboard_sign_in.html.erb", "app/views/layouts/postnhost/public_header/_dashboard_sign_in.html.erb"
29
29
  say ""
30
30
  warn_if_host_tailwind_not_installed
31
- say "Public views copied (#{views_scope})! Full scope includes every public template and layout under app/views/postnhost/public/templates/ and app/views/layouts/postnhost/public/templates/.", :green
31
+ say views_scope == "full" ? "All public templates and layouts were copied." : "The minimal public view set was copied.", :green
32
32
  end
33
33
 
34
34
  def engine_views_path
@@ -123,15 +123,13 @@ module Postnhost
123
123
  def warn_if_host_tailwind_not_installed
124
124
  return if host_tailwind_enabled?
125
125
 
126
- say "Tip: if you add new Tailwind classes in copied views, install host pipeline:", :yellow
127
- say " rails g postnhost:tailwindcss:install"
128
- say " bundle exec rails postnhost:tailwindcss:watch"
126
+ say "Tip: if you add new Tailwind classes in copied views, install host Tailwind support:", :yellow
127
+ say " bin/rails g postnhost:tailwindcss:install"
129
128
  say ""
130
129
  end
131
130
 
132
131
  def host_tailwind_enabled?
133
- Rails.root.join("postnhost.tailwind.config.js").exist? &&
134
- Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css").exist?
132
+ Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css").exist?
135
133
  end
136
134
 
137
135
  def source_paths
@@ -0,0 +1,68 @@
1
+ require "English"
2
+ require "fileutils"
3
+
4
+ require_relative "css_scope"
5
+
6
+ module Postnhost
7
+ class CssBuild
8
+ POLL_INTERVAL = 0.1
9
+
10
+ class << self
11
+ def run(command:, unscoped_path:, output_path:, watch: false)
12
+ FileUtils.rm_f(unscoped_path)
13
+
14
+ if watch
15
+ watch(command:, unscoped_path:, output_path:)
16
+ else
17
+ system(*command, exception: true)
18
+ CssScope.scope_file(unscoped_path, output_path)
19
+ end
20
+ ensure
21
+ FileUtils.rm_f(unscoped_path)
22
+ end
23
+
24
+ private
25
+
26
+ def watch(command:, unscoped_path:, output_path:)
27
+ process_id = Process.spawn(*command)
28
+ applied_signature = nil
29
+ pending_signature = nil
30
+
31
+ loop do
32
+ process_status = Process.waitpid(process_id, Process::WNOHANG)
33
+ signature = file_signature(unscoped_path)
34
+
35
+ if signature && signature == pending_signature && signature != applied_signature
36
+ CssScope.scope_file(unscoped_path, output_path)
37
+ applied_signature = signature
38
+ end
39
+
40
+ pending_signature = signature
41
+ break if process_status
42
+
43
+ sleep POLL_INTERVAL
44
+ end
45
+
46
+ raise "Tailwind CSS watcher exited unsuccessfully" unless $CHILD_STATUS.success?
47
+ ensure
48
+ terminate(process_id)
49
+ end
50
+
51
+ def file_signature(path)
52
+ return unless File.exist?(path)
53
+
54
+ stat = File.stat(path)
55
+ [stat.mtime.to_f, stat.size]
56
+ end
57
+
58
+ def terminate(process_id)
59
+ return unless process_id
60
+
61
+ Process.kill("TERM", process_id)
62
+ Process.wait(process_id)
63
+ rescue Errno::ESRCH, Errno::ECHILD
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,77 @@
1
+ require "crass"
2
+ require "fileutils"
3
+
4
+ module Postnhost
5
+ class CssScope
6
+ ROOT_SELECTOR = "html[data-postnhost]"
7
+ ROOT_OR_DESCENDANT_SELECTOR = ":is(#{ROOT_SELECTOR}, #{ROOT_SELECTOR} *)"
8
+ GROUP_AT_RULES = %w[container layer media starting-style supports].freeze
9
+
10
+ class << self
11
+ def call(css)
12
+ rules = Crass::Parser.parse_stylesheet(css, preserve_comments: true)
13
+ scope_rules(rules)
14
+ Crass::Parser.stringify(rules)
15
+ end
16
+
17
+ def scope_file(input_path, output_path = input_path)
18
+ output_path = File.expand_path(output_path)
19
+ FileUtils.mkdir_p(File.dirname(output_path))
20
+ temporary_path = "#{output_path}.tmp"
21
+ File.write(temporary_path, call(File.read(input_path)))
22
+ FileUtils.mv(temporary_path, output_path)
23
+ ensure
24
+ FileUtils.rm_f(temporary_path) if temporary_path
25
+ end
26
+
27
+ private
28
+
29
+ def scope_rules(rules)
30
+ rules.each do |rule|
31
+ case rule[:node]
32
+ when :style_rule
33
+ scope_style_rule(rule)
34
+ when :at_rule
35
+ scope_at_rule(rule)
36
+ end
37
+ end
38
+ end
39
+
40
+ def scope_at_rule(rule)
41
+ return unless rule[:block] && GROUP_AT_RULES.include?(rule[:name])
42
+
43
+ nested_rules = Crass::Parser.parse_rules(rule[:block], preserve_comments: true)
44
+ scope_rules(nested_rules)
45
+ rule[:block] = nested_rules
46
+ end
47
+
48
+ def scope_style_rule(rule)
49
+ selectors = split_selectors(rule.dig(:selector, :tokens)).map { |selector| scope_selector(selector) }.uniq
50
+ scoped_rule = Crass::Parser.parse_rules("#{selectors.join(',')}{}").first
51
+ rule[:selector] = scoped_rule[:selector]
52
+ end
53
+
54
+ def split_selectors(tokens)
55
+ selectors = tokens.each_with_object([[]]) do |token, groups|
56
+ if token[:node] == :comma
57
+ groups << []
58
+ else
59
+ groups.last << token
60
+ end
61
+ end
62
+
63
+ selectors.map { |selector_tokens| Crass::Parser.stringify(selector_tokens).strip }
64
+ end
65
+
66
+ def scope_selector(selector)
67
+ return ROOT_SELECTOR if selector.match?(/\A(?::root|:host)\z/)
68
+ return selector.sub(/\A:root/, ROOT_SELECTOR) if selector.match?(/\A:root(?=$|[\s.#\[:>+~])/)
69
+ return selector.sub(/\Ahtml/, ROOT_SELECTOR) if selector.match?(/\Ahtml(?=$|[\s.#\[:>+~])/)
70
+ return selector.sub(/\A\*/, ROOT_OR_DESCENDANT_SELECTOR) if selector.start_with?("*")
71
+ return "#{ROOT_OR_DESCENDANT_SELECTOR}#{selector}" if selector.match?(/\A[.#\[:]/)
72
+
73
+ "#{ROOT_SELECTOR} #{selector}"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -785,7 +785,7 @@ module Postnhost
785
785
  <li>⏰ <strong>Post scheduling</strong> — Schedule publication through your application’s Active Job adapter</li>
786
786
  <li>🌐 <strong>Localized SEO</strong> — Localized routes, language switcher, sitemap, and hreflang tags</li>
787
787
  <li>⚙️ <strong>Settings</strong> — Update key copy and assets from the CMS</li>
788
- <li>🎨 <strong>Customizable views</strong> — Override public templates in the host app (<code>rails g postnhost:views</code>)</li>
788
+ <li>🎨 <strong>Customizable views</strong> — Override public templates in the host app (<code>bin/rails g postnhost:views --views-scope=minimal</code>)</li>
789
789
  <li>⚡ <strong>Hotwire UI</strong> — Turbo Frames/Streams and Stimulus</li>
790
790
  <li>🔐 <strong>Session-based auth</strong> — Built-in admin authentication</li>
791
791
  </ul>
@@ -0,0 +1,75 @@
1
+ require "json"
2
+ require "pathname"
3
+ require "rubygems/version"
4
+
5
+ module Postnhost
6
+ class TailwindCompiler
7
+ REQUIRED_MAJOR = 4
8
+
9
+ class MissingCompiler < StandardError; end
10
+ class IncompatibleVersion < StandardError; end
11
+
12
+ def initialize(root:, loaded_specs: Gem.loaded_specs)
13
+ @root = Pathname.new(root)
14
+ @loaded_specs = loaded_specs
15
+ end
16
+
17
+ def command(input_path:, output_path:, watch: false)
18
+ command = npm_command(input_path:, output_path:) || ruby_command(input_path:, output_path:)
19
+ raise MissingCompiler, missing_compiler_message unless command
20
+
21
+ command << "--watch" if watch
22
+ command
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :root, :loaded_specs
28
+
29
+ def ruby_command(input_path:, output_path:)
30
+ spec = loaded_specs["tailwindcss-ruby"]
31
+ return unless spec
32
+
33
+ validate_version!(spec.version, "tailwindcss-ruby")
34
+ require "tailwindcss/ruby"
35
+ executable = Tailwindcss::Ruby.executable
36
+ return if executable.to_s.empty?
37
+
38
+ [executable, "-i", input_path.to_s, "-o", output_path.to_s, "--minify"]
39
+ end
40
+
41
+ def npm_command(input_path:, output_path:)
42
+ executable = root.join("node_modules/.bin/tailwindcss")
43
+ return unless executable.file?
44
+
45
+ validate_package_version!("tailwindcss")
46
+ validate_package_version!("@tailwindcss/cli")
47
+
48
+ [executable.to_s, "-i", input_path.to_s, "-o", output_path.to_s, "--minify"]
49
+ end
50
+
51
+ def validate_package_version!(package_name)
52
+ package_path = root.join("node_modules", package_name, "package.json")
53
+ raise MissingCompiler, "[postnhost] Missing #{package_name}. Install Tailwind CSS 4 and @tailwindcss/cli 4." unless package_path.file?
54
+
55
+ version = JSON.parse(package_path.read).fetch("version")
56
+ validate_version!(version, package_name)
57
+ rescue JSON::ParserError, KeyError
58
+ raise MissingCompiler, "[postnhost] Cannot read the installed #{package_name} version."
59
+ end
60
+
61
+ def validate_version!(version, source)
62
+ return if Gem::Version.new(version.to_s).segments.first == REQUIRED_MAJOR
63
+
64
+ raise IncompatibleVersion,
65
+ "[postnhost] Host CSS requires Tailwind CSS 4; found #{source} #{version}."
66
+ end
67
+
68
+ def missing_compiler_message
69
+ <<~MESSAGE.strip
70
+ [postnhost] No Tailwind CSS 4 compiler found for host customization.
71
+ Add tailwindcss-ruby 4, or install tailwindcss 4 and @tailwindcss/cli 4.
72
+ MESSAGE
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,32 @@
1
+ require "pathname"
2
+
3
+ module Postnhost
4
+ class TailwindInput
5
+ SOURCE_PATTERN = /@source\s+"([^"]+)";/
6
+
7
+ class << self
8
+ def build(engine_source:, host_source:)
9
+ [
10
+ expand_sources(engine_source),
11
+ expand_sources(host_source)
12
+ ].join("\n\n")
13
+ end
14
+
15
+ private
16
+
17
+ def expand_sources(path)
18
+ source_path = Pathname.new(path)
19
+
20
+ source_path.read.gsub(SOURCE_PATTERN) do
21
+ relative_source = Regexp.last_match(1)
22
+ absolute_source = source_path.dirname.join(relative_source).expand_path
23
+ %(@source "#{css_path(absolute_source)}";)
24
+ end
25
+ end
26
+
27
+ def css_path(path)
28
+ Pathname.new(path).expand_path.to_s.tr("\\", "/").gsub('"', '\\"')
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,3 +1,3 @@
1
1
  module Postnhost
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end