maquina 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.overmind.env +1 -0
  3. data/Gemfile +20 -0
  4. data/Gemfile.lock +346 -0
  5. data/Procfile.dev +3 -0
  6. data/app/assets/images/maquina/.keep +0 -0
  7. data/app/assets/javascripts/maquina/controllers/file_controller.js +1 -1
  8. data/app/assets/javascripts/maquina/controllers/modal_controller.js +1 -1
  9. data/app/assets/javascripts/maquina/controllers/modal_open_controller.js +3 -8
  10. data/app/assets/stylesheets/maquina/application.tailwind.css +96 -97
  11. data/app/controllers/concerns/.keep +0 -0
  12. data/app/controllers/concerns/maquina/authenticate.rb +8 -1
  13. data/app/controllers/concerns/maquina/resourceful.rb +9 -9
  14. data/app/models/concerns/.keep +0 -0
  15. data/app/models/concerns/maquina/blockeable.rb +13 -3
  16. data/app/models/concerns/maquina/searchable.rb +2 -2
  17. data/app/models/maquina/application_record.rb +3 -3
  18. data/app/models/maquina/current.rb +2 -1
  19. data/app/models/maquina/membership.rb +10 -0
  20. data/app/models/maquina/organization.rb +7 -0
  21. data/app/models/maquina/plan.rb +1 -3
  22. data/app/models/maquina/user.rb +8 -3
  23. data/app/policies/maquina/application_policy.rb +3 -6
  24. data/app/policies/maquina/invitation_policy.rb +2 -2
  25. data/app/policies/maquina/navigation_policy.rb +2 -2
  26. data/app/policies/maquina/user_policy.rb +7 -3
  27. data/app/views/layouts/maquina/application.html.erb +1 -2
  28. data/app/views/layouts/maquina/sessions.html.erb +1 -2
  29. data/app/views/maquina/application/_navbar.html.erb +1 -1
  30. data/app/views/maquina/application/components/file_component.rb +38 -5
  31. data/app/views/maquina/application/form.rb +3 -2
  32. data/app/views/maquina/application/index_header.rb +1 -1
  33. data/app/views/maquina/application/index_modal.rb +1 -3
  34. data/config/importmap.rb +12 -0
  35. data/db/migrate/20230829183530_create_maquina_organizations.rb +11 -0
  36. data/db/migrate/20230829192656_create_maquina_memberships.rb +15 -0
  37. data/lib/generators/maquina/install_generator.rb +0 -4
  38. data/lib/generators/maquina/tailwind_config/USAGE +14 -0
  39. data/lib/generators/maquina/tailwind_config/tailwind_config_generator.rb +17 -3
  40. data/lib/generators/maquina/tailwind_config/templates/app/assets/stylesheets/maquina.css +96 -0
  41. data/lib/generators/maquina/tailwind_config/templates/lib/generators/tailwind_config/tailwind_config_generator.rb.tt +11 -0
  42. data/lib/generators/maquina/tailwind_config/templates/{app/assets/config/maquina → lib/generators/tailwind_config/templates/config}/tailwind.config.js.tt +4 -0
  43. data/lib/generators/maquina/tailwind_config/templates/lib/tasks/tailwind.rake.tt +11 -0
  44. data/lib/generators/maquina/templates/config/initializers/maquina.rb.tt +6 -0
  45. data/lib/maquina/engine.rb +0 -4
  46. data/lib/maquina/version.rb +1 -1
  47. data/lib/maquina.rb +11 -4
  48. metadata +23 -10
  49. data/config/definitions.rb +0 -1
  50. data/config/initializers/importmap.rb +0 -17
  51. data/lib/generators/maquina/templates/config/initializers/maquina.rb +0 -3
  52. data/lib/tasks/tailwind.rake +0 -25
@@ -16,6 +16,7 @@ module Maquina
16
16
 
17
17
  included do
18
18
  class_attribute :resource_class, instance_writer: false
19
+ class_attribute :namespace, instance_writer: false
19
20
  class_attribute :find_by_param, instance_writer: false
20
21
  class_attribute :list_attributes, instance_writer: false
21
22
  class_attribute :form_attributes, instance_writer: false
@@ -28,18 +29,16 @@ module Maquina
28
29
 
29
30
  # Route proxies
30
31
 
32
+ def namespace_path
33
+ @namespace_path ||= namespace.present? ? "#{namespace&.to_s&.underscore}_" : ""
34
+ end
35
+
31
36
  def base_singular_path_name
32
- @base_path_name ||= begin
33
- namespace_path = ""
34
- "#{namespace_path}#{resource_class.model_name.singular_route_key}_path"
35
- end
37
+ @base_path_name ||= "#{namespace_path}#{resource_class.model_name.singular_route_key}_path"
36
38
  end
37
39
 
38
40
  def base_plural_path_name
39
- @base_plural_path_name ||= begin
40
- namespace_path = ""
41
- "#{namespace_path}#{resource_class.model_name.route_key}_path"
42
- end
41
+ @base_plural_path_name ||= "#{namespace_path}#{resource_class.model_name.route_key}_path"
43
42
  end
44
43
 
45
44
  def new_resource_path(options = {})
@@ -142,13 +141,14 @@ module Maquina
142
141
  end
143
142
 
144
143
  class_methods do
145
- def resourceful(resource_class: nil, find_by_param: :id, only: [], except: [], list_attributes: [], form_attributes: [], show_attributes: [], policy_class: nil)
144
+ def resourceful(resource_class: nil, namespace: nil, find_by_param: :id, only: [], except: [], list_attributes: [], form_attributes: [], show_attributes: [], policy_class: nil)
146
145
  self.resource_class = resource_class || controller_path.classify.safe_constantize
147
146
  self.find_by_param = find_by_param || :id
148
147
  self.list_attributes = Array(list_attributes).compact
149
148
  self.form_attributes = Array(form_attributes).compact
150
149
  self.show_attributes = Array(show_attributes).compact
151
150
  self.policy_class = policy_class
151
+ self.namespace = namespace
152
152
 
153
153
  valid_rest_actions = {
154
154
  index: Maquina::Index,
File without changes
@@ -4,7 +4,16 @@ module Maquina
4
4
  module Blockeable
5
5
  extend ActiveSupport::Concern
6
6
 
7
- included do
7
+ included do |base|
8
+ if base.eql?(Maquina::User)
9
+ define_method(:memberships_blocked?) do
10
+ return false if management?
11
+
12
+ return @memberships_blocked if !@memberships_blocked.nil?
13
+ @memberships_blocked = !memberships.includes(:organization).where(blocked_at: nil).where(maquina_organizations: {active: true}).exists?
14
+ end
15
+ end
16
+
8
17
  if has_attribute?(:blocked_at) && has_attribute?(:temporary_blocked_at)
9
18
  define_method(:blocked?) do
10
19
  temporary_blocked_until = nil
@@ -12,13 +21,14 @@ module Maquina
12
21
  temporary_blocked_until = temporary_blocked_at.since(Maquina.configuration.temporary_block)
13
22
  end
14
23
 
15
- blocked_at.present? || (temporary_blocked_until.present? && temporary_blocked_until > Time.zone.now)
24
+ blocked_at.present? || (temporary_blocked_until.present? && temporary_blocked_until > Time.zone.now) ||
25
+ (respond_to?(:memberships_blocked?) && memberships_blocked?)
16
26
  end
17
27
 
18
28
  scope :unblocked, -> { where(blocked_at: nil).where("(coalesce(temporary_blocked_at + interval '? minutes', now())) <= now()", Maquina.configuration.temporary_block&.in_minutes&.to_i || 0) }
19
29
  elsif has_attribute(:blocked_at)
20
30
  define_method(:blocked?) do
21
- blocked_at.present?
31
+ blocked_at.present? || (respond_to?(:memberships_blocked?) && memberships_blocked?)
22
32
  end
23
33
 
24
34
  scope :unblocked, -> { where(blocked_at: nil) }
@@ -11,13 +11,13 @@ module Maquina
11
11
  end
12
12
 
13
13
  class_methods do
14
- def search_scope(fields: [], options: {})
14
+ def search_scope(fields: [], options: {}, associated_against: {})
15
15
  return if fields.empty?
16
16
 
17
17
  default_options = {tsearch: {prefix: true, any_word: true}}
18
18
  search_options = options.deep_merge(default_options)
19
19
 
20
- pg_search_scope :search_full, against: fields, using: search_options # , ignoring: :accents
20
+ pg_search_scope :search_full, against: fields, using: search_options, associated_against: associated_against # , ignoring: :accents
21
21
  end
22
22
  end
23
23
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pg_search"
4
-
5
3
  module Maquina
6
4
  class ApplicationRecord < ActiveRecord::Base
7
5
  self.abstract_class = true
8
6
 
9
- include PgSearch::Model
7
+ def self.searchable?
8
+ false
9
+ end
10
10
  end
11
11
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Maquina
4
4
  class Current < ActiveSupport::CurrentAttributes
5
- attribute :active_session, :user
5
+ attribute :active_session, :user, :membership
6
6
 
7
7
  def signed_in?
8
8
  return false if active_session.blank?
@@ -12,6 +12,7 @@ module Maquina
12
12
  def active_session=(value)
13
13
  super
14
14
  self.user = value&.user
15
+ self.membership = user.default_membership
15
16
  end
16
17
 
17
18
  def management?
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class Membership < ApplicationRecord
5
+ belongs_to :organization, class_name: "Maquina::Organization", foreign_key: :maquina_organization_id
6
+ belongs_to :user, class_name: "Maquina::User", foreign_key: :maquina_user_id
7
+
8
+ enum role: Maquina.configuration.membership_roles
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class Organization < ApplicationRecord
5
+ belongs_to :plan, class_name: "Maquina::Plan", foreign_key: :maquina_plan_id, optional: true
6
+ end
7
+ end
@@ -25,14 +25,12 @@ module Maquina
25
25
  # attributes and use the provided methods and scopes for querying and manipulating plan data.
26
26
 
27
27
  class Plan < ApplicationRecord
28
- include Searchable
28
+ has_many :organizations, class_name: "Maquina::Organization", foreign_key: :maquina_plan_id, dependent: :nullify
29
29
 
30
30
  monetize :price_cents
31
31
 
32
32
  validates :price, numericality: {greater_than_or_equal_to: 0}, if: ->(plan) { plan.free? }
33
33
  validates :price, numericality: {greater_than: 0}, if: ->(plan) { !plan.free? }
34
34
  validates :name, presence: true, uniqueness: true
35
-
36
- search_scope(fields: [:name])
37
35
  end
38
36
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Maquina
4
4
  class User < ApplicationRecord
5
- include Maquina::Searchable
6
5
  include Maquina::RetainPasswords
7
6
  include Maquina::AuthenticateBy
8
7
  include Maquina::Blockeable
@@ -11,19 +10,25 @@ module Maquina
11
10
  PASSWORD_COMPLEXITY_REGEX = /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#-=+])[A-Za-z\d@$!%*?&#-=+]{8,}\z/
12
11
  has_secure_password
13
12
 
13
+ has_many :memberships, class_name: "Maquina::Membership", foreign_key: :maquina_user_id, inverse_of: :user
14
+
14
15
  validates :email, presence: true, uniqueness: true, format: {with: URI::MailTo::EMAIL_REGEXP}
15
16
  validates :password, format: {with: PASSWORD_COMPLEXITY_REGEX}, unless: ->(user) { user.password.blank? }
16
17
 
17
18
  before_save :downcase_email
18
19
 
19
- search_scope(fields: [:email])
20
-
21
20
  def expired_password?
22
21
  return false if password_expires_at.blank?
23
22
 
24
23
  password_expires_at < Time.zone.now
25
24
  end
26
25
 
26
+ def default_membership
27
+ return nil if management?
28
+
29
+ memberships.detect { |membership| membership.blocked_at.blank? && membership.organization.present? && membership.organization.active? }
30
+ end
31
+
27
32
  private
28
33
 
29
34
  def downcase_email
@@ -40,11 +40,8 @@ module Maquina
40
40
  user.management?
41
41
  end
42
42
 
43
- # Define shared methods useful for most policies.
44
- # For example:
45
- #
46
- # def owner?
47
- # record.user_id == user.id
48
- # end
43
+ def admin?
44
+ (Maquina::Current.membership.present? && Maquina::Current.membership.admin?)
45
+ end
49
46
  end
50
47
  end
@@ -3,11 +3,11 @@
3
3
  module Maquina
4
4
  class InvitationPolicy < ApplicationPolicy
5
5
  def new?
6
- management?
6
+ management? || admin?
7
7
  end
8
8
 
9
9
  def create?
10
- new?
10
+ new? || admin?
11
11
  end
12
12
  end
13
13
  end
@@ -3,11 +3,11 @@
3
3
  module Maquina
4
4
  class NavigationPolicy < ApplicationPolicy
5
5
  def plans?
6
- user.management?
6
+ management?
7
7
  end
8
8
 
9
9
  def users?
10
- user.management?
10
+ management? || admin?
11
11
  end
12
12
  end
13
13
  end
@@ -3,11 +3,11 @@
3
3
  module Maquina
4
4
  class UserPolicy < ApplicationPolicy
5
5
  def index?
6
- management?
6
+ management? || admin?
7
7
  end
8
8
 
9
9
  def new?
10
- management?
10
+ management? || admin?
11
11
  end
12
12
 
13
13
  def create?
@@ -21,7 +21,11 @@ module Maquina
21
21
  private
22
22
 
23
23
  relation_scope do |scope|
24
- scope.where(management: management?)
24
+ if management?
25
+ scope.where(management: management?)
26
+ else
27
+ scope.joins(memberships: :organization).where(maquina_organizations: {id: Maquina::Current.membership.organization.id})
28
+ end
25
29
  end
26
30
  end
27
31
  end
@@ -6,8 +6,7 @@
6
6
  <%= csrf_meta_tags %>
7
7
  <%= csp_meta_tag %>
8
8
 
9
- <%= stylesheet_link_tag "maquina/tailwind", "inter-font", "data-turbo-track": "reload" %>
10
- <%= stylesheet_link_tag "maquina/application", "data-turbo-track": "reload", media: "all" %>
9
+ <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
11
10
 
12
11
  <%= maquina_importmap_tags %>
13
12
  </head>
@@ -6,8 +6,7 @@
6
6
  <%= csrf_meta_tags %>
7
7
  <%= csp_meta_tag %>
8
8
 
9
- <%= stylesheet_link_tag "maquina/tailwind", "inter-font", "data-turbo-track": "reload" %>
10
- <%= stylesheet_link_tag "maquina/application", "data-turbo-track": "reload" %>
9
+ <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
11
10
 
12
11
  <%= maquina_importmap_tags %>
13
12
  </head>
@@ -6,7 +6,7 @@
6
6
  <%= render Maquina::Navbar::Menu.new %>
7
7
  </div>
8
8
 
9
- <%= render Maquina::Navbar::Search.new(query: params[:q], url: collection_path) if action_name == "index" && respond_to?(:collection_path) && collection_path.present? %>
9
+ <%= render Maquina::Navbar::Search.new(query: params[:q], url: collection_path) if action_name == "index" && respond_to?(:collection_path) && collection_path.present? && respond_to?(:resource_class) && resource_class.searchable? %>
10
10
 
11
11
  <%= render Maquina::Navbar::MobileButton.new %>
12
12
 
@@ -5,17 +5,21 @@ module Maquina
5
5
  module Components
6
6
  class FileComponent < ComponentBase
7
7
  include Phlex::Rails::Helpers::NumberToHumanSize
8
+ include Phlex::Rails::Helpers::TokenList
9
+
10
+ delegate :main_app, to: :helpers
8
11
 
9
12
  def template
10
- div(**extend_control_options(attribute_name, control_html)) do
13
+ div(**extend_control_options(control_html)) do
11
14
  @form.label attribute_name, class: "label #{label_css_class}"
12
15
  div(class: "mt-1") do
13
16
  div(data_file_target: "container") do
14
17
  @form.file_field attribute_name, **extend_input_options(input_html(no_helpers: true))
15
- img(class: "hidden", data_file_target: "preview")
16
- svg(viewbox: "0 0 24 24", fill: "currentColor", aria_hidden: "true", data_action: "click->file#select", data_file_target: "placeholder") do |s|
18
+ img(class: token_list("", hidden: !attached_file?), data_file_target: "preview", src: attached_url)
19
+ svg(viewbox: "0 0 24 24", fill: "currentColor", class: token_list("", hidden: attached_file?), aria_hidden: "true", data_action: "click->file#select", data_file_target: "placeholder") do |s|
17
20
  s.path(fill_rule: "evenodd", d: "M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z", clip_rule: "evenodd")
18
21
  end
22
+ p(class: "text-skin-muted pb-4 pt-2 italic text-sm") { attached_info } if attached_file?
19
23
  help_template
20
24
  error_template
21
25
  end
@@ -41,18 +45,47 @@ module Maquina
41
45
  }.deep_merge(input_html)
42
46
  end
43
47
 
44
- def extend_control_options(attribute_name, control_html)
48
+ def extend_control_options(control_html)
45
49
  max_size = control_html.dig(:data, :file_max_size_value) || 4_194_304
46
50
  human_max_size = number_to_human_size(max_size)
51
+ file_url = attached_url
47
52
 
48
53
  {
49
54
  data: {
50
55
  controller: "file",
51
56
  file_max_size_value: max_size,
52
- file_validation_message_value: t("activerecord.errors.attributes.#{@form.object.model_name.i18n_key}.#{attribute_name}.oversize", size: human_max_size, default: t("activerecord.errors.attributes.file.oversize", size: human_max_size))
57
+ file_validation_message_value: t("activerecord.errors.attributes.#{@form.object.model_name.i18n_key}.#{attribute_name}.oversize", size: human_max_size, default: t("activerecord.errors.attributes.file.oversize", size: human_max_size)),
58
+ file_url_value: file_url
53
59
  }
54
60
  }.deep_merge(control_html)
55
61
  end
62
+
63
+ # Todo: Work on not representable files
64
+ def attached_url
65
+ return "" if !attached_file?
66
+
67
+ if attached_file.blob.representable?
68
+ puts "Representable"
69
+ main_app.url_for(attached_file.blob.variant(resize_to_limit: [1200, 768]))
70
+ else
71
+ puts "No representable"
72
+ main_app.url_for(attached_file.blob)
73
+ end
74
+ end
75
+
76
+ def attached_info
77
+ return "" if !attached_file?
78
+
79
+ "#{attached_file.blob.filename} (#{number_to_human_size(attached_file.blob.byte_size)})"
80
+ end
81
+
82
+ def attached_file
83
+ @attached_file ||= @resource.send(attribute_name)
84
+ end
85
+
86
+ def attached_file?
87
+ attached_file&.attached?
88
+ end
56
89
  end
57
90
  end
58
91
  end
@@ -49,8 +49,9 @@ module Maquina
49
49
 
50
50
  def form_header_template
51
51
  div do
52
- h3(class: "text-lg font-medium leading-6 text-skin-base") { t("new.#{resource_class.model_name.i18n_key}.title", model: model_human_name.downcase, default: t("new.title", default: model_human_name.downcase)) }
53
- p(class: "mt-1 text-sm text-skin-dimmed") { t("new.#{resource_class.model_name.i18n_key}.description", model: model_human_name.downcase, default: t("new.description", model: model_human_name.downcase)) }
52
+ prefix = @resource.persisted? ? :edit : :new
53
+ h3(class: "text-lg font-medium leading-6 text-skin-base") { t("#{prefix}.#{resource_class.model_name.i18n_key}.title", model: model_human_name.downcase, default: t("#{prefix}.title", default: model_human_name.downcase)) }
54
+ p(class: "mt-1 text-sm text-skin-dimmed") { t("#{prefix}.#{resource_class.model_name.i18n_key}.description", model: model_human_name.downcase, default: t("#{prefix}.description", model: model_human_name.downcase)) }
54
55
  end
55
56
  end
56
57
 
@@ -27,7 +27,7 @@ module Maquina
27
27
  return if policy_class.blank? || !allowed_to?(:new?, with: policy_class)
28
28
 
29
29
  options = {}
30
- options[:data] = {controller: "modal-open", action: "modal-open#open"} if policy_class.present? && allowed_to?(:new_modal?, with: policy_class)
30
+ options[:data] = {controller: "modal-open", modal_open_modal_outlet: ".modal", action: "modal-open#open"} if policy_class.present? && allowed_to?(:new_modal?, with: policy_class)
31
31
 
32
32
  div(class: "mt-4 sm:mt-0 sm:ml-16 sm:flex-none") do
33
33
  a(href: new_resource_path, class: "inline-flex items-center justify-center button button-accented", **options) { add_new }
@@ -7,9 +7,7 @@ module Maquina
7
7
  register_element :turbo_frame
8
8
 
9
9
  def template
10
- div(data_controller: "modal",
11
- data_action: " modal-open:toggle@window->modal#toggleModal close-frame-modal:toggle@window->modal#toggleModal",
12
- data_frame_src: "") do
10
+ div(data_controller: "modal", class: "modal", data_frame_src: "") do
13
11
  div(class: "fixed inset-0 z-30 hidden overflow-y-auto", aria_labelledby: "modal-title", role: "dialog",
14
12
  aria_modal: "true", data_modal_target: "container") do
15
13
  div(class: "flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0") do
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stimulus & Turbo
4
+ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
5
+ pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
6
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
7
+ pin "stimulus-use", to: "https://ga.jspm.io/npm:stimulus-use@0.52.0/dist/index.js"
8
+
9
+ # Maquina entrypoint
10
+ pin "application", to: "maquina/application.js", preload: true
11
+
12
+ pin_all_from Maquina::Engine.root.join("app/assets/javascripts/maquina/controllers"), under: "controllers", to: "maquina/controllers"
@@ -0,0 +1,11 @@
1
+ class CreateMaquinaOrganizations < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :maquina_organizations do |t|
4
+ t.string :name
5
+ t.boolean :active, default: true
6
+ t.references :maquina_plan, null: true, foreign_key: true
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ class CreateMaquinaMemberships < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_enum :maquina_role, %w[admin member]
4
+
5
+ create_table :maquina_memberships do |t|
6
+ t.references :maquina_organization, null: false, foreign_key: true
7
+ t.references :maquina_user, null: false, foreign_key: true
8
+ t.boolean :owner, default: false
9
+ t.enum :role, enum_type: :maquina_role, default: "member", null: false
10
+ t.timestamp :blocked_at
11
+
12
+ t.timestamps
13
+ end
14
+ end
15
+ end
@@ -24,9 +24,5 @@ module Maquina
24
24
  def copy_migrations
25
25
  rake "maquina:install:migrations"
26
26
  end
27
-
28
- def build_tailwind
29
- rake "maquina:tailwindcss:build"
30
- end
31
27
  end
32
28
  end
@@ -0,0 +1,14 @@
1
+ Description:
2
+ It will create a custom TailwindCSS configuration file for Rails applications. The custom configuration file is
3
+ auto-generated that allows to include Engines files.
4
+
5
+ Add the file config/tailwind.config.js to the .gitignore file, and if you need to modify it, modify the template
6
+ file included in the generator.
7
+
8
+ Example:
9
+ bin/rails generate maquina:tailwind_config
10
+
11
+ This will create:
12
+ lib/tailwind.rake
13
+ lib/generators/tailwind_config/tailwind_config_generator.rb
14
+ lib/generators/tailwind_config/templates/tailwind.config.js.tt
@@ -2,10 +2,24 @@
2
2
 
3
3
  module Maquina
4
4
  class TailwindConfigGenerator < Rails::Generators::Base
5
- source_root File.expand_path("../templates", __FILE__)
5
+ source_root File.expand_path("templates", __dir__)
6
6
 
7
- def create_tailwind_config_file
8
- template "app/assets/config/maquina/tailwind.config.js"
7
+ def create_generator
8
+ tailwind_config_file = "lib/generators/tailwind_config/templates/config/tailwind.config.js.tt"
9
+
10
+ template "lib/tasks/tailwind.rake"
11
+ template "lib/generators/tailwind_config/tailwind_config_generator.rb"
12
+
13
+ # Seems that template can't handle .tt.tt extensions correctly
14
+ copy_file tailwind_config_file, tailwind_config_file
15
+ end
16
+
17
+ def configure_css
18
+ copy_file "app/assets/stylesheets/maquina.css"
19
+
20
+ inject_into_file "app/assets/stylesheets/application.tailwind.css", after: /base"?;/ do
21
+ %(\n@import "./maquina.css";)
22
+ end
9
23
  end
10
24
  end
11
25
  end
@@ -0,0 +1,96 @@
1
+ :root {
2
+ --color-base: 31 27 54;
3
+ --color-accented: 37 99 235;
4
+ --color-inverted: 255 255 255;
5
+ --color-accented-hover: 59 130 246;
6
+ --color-muted: 55 65 81;
7
+ --color-dimmed: 75 85 99;
8
+ --color-error: 220 38 38;
9
+
10
+ --color-border-base: 209 213 219;
11
+ --color-border-accented: 37 99 235;
12
+ }
13
+
14
+ .label {
15
+ @apply text-sm font-medium text-skin-muted;
16
+ }
17
+
18
+ .input {
19
+ @apply appearance-none rounded-md border border-skin-base px-3 py-2 placeholder-gray-400 shadow-sm focus:border-skin-accented focus:outline-none focus:ring-skin-accented sm:text-sm;
20
+ }
21
+
22
+ .link {
23
+ @apply font-medium text-skin-accented hover:text-skin-accented-hover;
24
+ }
25
+
26
+ .check {
27
+ @apply border-skin-base h-4 w-4 text-skin-accented focus:ring-skin-accented rounded;
28
+ }
29
+
30
+ .button {
31
+ @apply inline-flex justify-center py-2 px-4 border border-skin-base rounded-md shadow-sm text-sm font-medium text-skin-muted bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-skin-accented;
32
+ }
33
+
34
+ .control-error .label {
35
+ @apply text-skin-error;
36
+ }
37
+
38
+ .control-error .input {
39
+ @apply border-skin-error focus:border-skin-error focus:ring-skin-error;
40
+ }
41
+
42
+ .help {
43
+ @apply mt-2 text-sm text-skin-dimmed;
44
+ }
45
+
46
+ .help-error {
47
+ @apply text-skin-error;
48
+ }
49
+
50
+ .button[disabled] {
51
+ @apply bg-gray-50 text-skin-dimmed cursor-default;
52
+ }
53
+
54
+ .button-accented {
55
+ @apply rounded-md border border-transparent bg-skin-accented py-2 px-4 text-sm font-medium text-skin-inverted shadow-sm hover:bg-skin-accented-hover focus:outline-none focus:ring-2 focus:ring-skin-accented focus:ring-offset-2;
56
+ }
57
+
58
+ input[type="search"] {
59
+ @apply block w-full rounded-md border border-skin-base pl-10 leading-5 placeholder-gray-400 focus:placeholder-gray-300 focus:outline-none focus:ring-1 focus:ring-skin-accented focus:border-skin-accented sm:text-sm;
60
+ }
61
+
62
+ .desktop-menu {
63
+ @apply hidden lg:ml-6 lg:flex lg:space-x-8;
64
+ }
65
+
66
+ .desktop-menu-item {
67
+ @apply font-sans uppercase border-transparent text-skin-dimmed underline-offset-4 decoration-2 hover:underline hover:decoration-skin-accented hover:text-skin-muted inline-flex items-center px-1 pt-1 text-sm font-medium;
68
+ }
69
+
70
+ .desktop-menu-item__active {
71
+ @apply decoration-skin-accented underline text-skin-base;
72
+ }
73
+
74
+ .mobile-menu {
75
+ @apply pt-2 pb-3 space-y-1;
76
+ }
77
+
78
+ .mobile-menu-item {
79
+ @apply font-sans border-transparent text-skin-dimmed hover:bg-gray-200 hover:border-gray-300 hover:text-skin-muted block pl-3 pr-4 py-2 border-l-4 text-base font-medium;
80
+ }
81
+
82
+ .mobile-menu-item__active {
83
+ @apply bg-skin-accented-hover border-skin-accented text-skin-accented hover:text-skin-accented hover:bg-skin-accented-hover hover:border-skin-accented;
84
+ }
85
+
86
+ .desktop-profile-menu-item {
87
+ @apply block w-full text-left font-sans px-4 py-2 text-sm text-skin-dimmed hover:bg-gray-200 hover:text-skin-muted;
88
+ }
89
+
90
+ .mobile-profile-menu-item {
91
+ @apply block font-sans px-4 py-2 text-base font-medium text-skin-dimmed hover:text-skin-muted hover:bg-gray-200;
92
+ }
93
+
94
+ .mobile-button {
95
+ @apply inline-flex items-center justify-center p-2 rounded-md text-skin-dimmed hover:text-skin-muted hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-skin-accented;
96
+ }
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TailwindConfigGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ def create_tailwind_config_file
7
+ @engines_paths = Maquina.configuration.tailwind_content
8
+
9
+ template "config/tailwind.config.js"
10
+ end
11
+ end
@@ -2,6 +2,10 @@ const defaultTheme = require('tailwindcss/defaultTheme')
2
2
 
3
3
  module.exports = {
4
4
  content: [
5
+ 'public/*.html',
6
+ 'app/helpers/**/*.rb',
7
+ 'app/javascript/**/*.js',
8
+ 'app/views/**/*.{erb,haml,html,slim}',
5
9
  <%= Maquina.configuration.tailwind_content.map{|path| "'#{path}'"}.join(",\n") %>
6
10
  ],
7
11
  theme: {