maquina 0.1.0 → 0.2.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 (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: {