katalyst-content 0.1.0

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +95 -0
  4. data/app/assets/config/katalyst-content.js +1 -0
  5. data/app/assets/javascripts/controllers/content/editor/container_controller.js +113 -0
  6. data/app/assets/javascripts/controllers/content/editor/item_controller.js +45 -0
  7. data/app/assets/javascripts/controllers/content/editor/list_controller.js +105 -0
  8. data/app/assets/javascripts/controllers/content/editor/new_item_controller.js +12 -0
  9. data/app/assets/javascripts/controllers/content/editor/status_bar_controller.js +22 -0
  10. data/app/assets/javascripts/utils/content/editor/container.js +52 -0
  11. data/app/assets/javascripts/utils/content/editor/item.js +245 -0
  12. data/app/assets/javascripts/utils/content/editor/rules-engine.js +177 -0
  13. data/app/assets/stylesheets/katalyst/content/_index.scss +31 -0
  14. data/app/assets/stylesheets/katalyst/content/editor/_icon.scss +17 -0
  15. data/app/assets/stylesheets/katalyst/content/editor/_index.scss +145 -0
  16. data/app/assets/stylesheets/katalyst/content/editor/_item-actions.scss +93 -0
  17. data/app/assets/stylesheets/katalyst/content/editor/_item-rules.scss +19 -0
  18. data/app/assets/stylesheets/katalyst/content/editor/_new-items.scss +39 -0
  19. data/app/assets/stylesheets/katalyst/content/editor/_status-bar.scss +87 -0
  20. data/app/controllers/katalyst/content/application_controller.rb +8 -0
  21. data/app/controllers/katalyst/content/items_controller.rb +70 -0
  22. data/app/helpers/katalyst/content/application_helper.rb +8 -0
  23. data/app/helpers/katalyst/content/editor/base.rb +44 -0
  24. data/app/helpers/katalyst/content/editor/container.rb +41 -0
  25. data/app/helpers/katalyst/content/editor/item.rb +67 -0
  26. data/app/helpers/katalyst/content/editor/list.rb +41 -0
  27. data/app/helpers/katalyst/content/editor/new_item.rb +53 -0
  28. data/app/helpers/katalyst/content/editor/status_bar.rb +57 -0
  29. data/app/helpers/katalyst/content/editor_helper.rb +42 -0
  30. data/app/models/concerns/katalyst/content/container.rb +100 -0
  31. data/app/models/concerns/katalyst/content/garbage_collection.rb +31 -0
  32. data/app/models/concerns/katalyst/content/has_tree.rb +63 -0
  33. data/app/models/concerns/katalyst/content/version.rb +33 -0
  34. data/app/models/katalyst/content/content.rb +21 -0
  35. data/app/models/katalyst/content/item.rb +36 -0
  36. data/app/models/katalyst/content/node.rb +21 -0
  37. data/app/models/katalyst/content/types/nodes_type.rb +42 -0
  38. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  39. data/app/views/katalyst/content/contents/_content.html+form.erb +39 -0
  40. data/app/views/katalyst/content/contents/_content.html.erb +5 -0
  41. data/app/views/katalyst/content/editor/_item.html.erb +11 -0
  42. data/app/views/katalyst/content/editor/_list_item.html.erb +14 -0
  43. data/app/views/katalyst/content/editor/_new_item.html.erb +3 -0
  44. data/app/views/katalyst/content/editor/_new_items.html.erb +5 -0
  45. data/app/views/katalyst/content/items/_item.html+form.erb +34 -0
  46. data/app/views/katalyst/content/items/_item.html.erb +3 -0
  47. data/app/views/katalyst/content/items/edit.html.erb +4 -0
  48. data/app/views/katalyst/content/items/new.html.erb +4 -0
  49. data/app/views/katalyst/content/items/update.turbo_stream.erb +7 -0
  50. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  51. data/config/importmap.rb +8 -0
  52. data/config/locales/en.yml +12 -0
  53. data/config/routes.rb +3 -0
  54. data/db/migrate/20220913003839_create_katalyst_content_items.rb +17 -0
  55. data/lib/katalyst/content/config.rb +18 -0
  56. data/lib/katalyst/content/engine.rb +36 -0
  57. data/lib/katalyst/content/version.rb +7 -0
  58. data/lib/katalyst/content.rb +19 -0
  59. data/lib/tasks/yarn.rake +18 -0
  60. data/spec/factories/katalyst/content/items.rb +16 -0
  61. metadata +103 -0
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ module Container
6
+ extend ActiveSupport::Concern
7
+
8
+ included do # rubocop:disable Metrics/BlockLength
9
+ include Katalyst::Content::GarbageCollection
10
+
11
+ before_destroy :unset_versions
12
+
13
+ belongs_to :draft_version,
14
+ autosave: true,
15
+ class_name: "#{name}::Version",
16
+ inverse_of: :parent,
17
+ optional: true
18
+ belongs_to :published_version,
19
+ class_name: "#{name}::Version",
20
+ inverse_of: :parent,
21
+ optional: true
22
+
23
+ delegate :nodes, :items, :tree, to: :published_version, prefix: :published, allow_nil: true
24
+ delegate :nodes, :items, :tree, to: :draft_version, prefix: :draft, allow_nil: true
25
+
26
+ has_many :versions,
27
+ class_name: "#{name}::Version",
28
+ dependent: :delete_all,
29
+ foreign_key: :parent_id,
30
+ inverse_of: :parent,
31
+ validate: true do
32
+ def active
33
+ parent = proxy_association.owner
34
+ where(id: [parent.published_version_id, parent.draft_version_id].uniq.compact)
35
+ end
36
+
37
+ def inactive
38
+ parent = proxy_association.owner
39
+ where.not(id: [parent.published_version_id, parent.draft_version_id].uniq.compact)
40
+ end
41
+ end
42
+
43
+ has_many :items,
44
+ as: :container,
45
+ autosave: true,
46
+ class_name: "Katalyst::Content::Item",
47
+ dependent: :destroy,
48
+ validate: true
49
+ end
50
+
51
+ # A resource is in draft mode if it has an unpublished draft or it has no published version.
52
+ # @return the current state of the resource, either `published` or `draft`
53
+ def state
54
+ if published_version_id && published_version_id == draft_version_id
55
+ :published
56
+ else
57
+ :draft
58
+ end
59
+ end
60
+
61
+ # Promotes the draft version to become the published version
62
+ def publish!
63
+ update!(published_version: draft_version)
64
+ self
65
+ end
66
+
67
+ # Reverts the draft version to the current published version
68
+ def revert!
69
+ update!(draft_version: published_version)
70
+ self
71
+ end
72
+
73
+ # Updates the current draft version with new structure. Attributes should be structural information about the
74
+ # items, e.g. `{index => {id:, depth:}` or `[{id:, depth:}]`.
75
+ #
76
+ # This method conforms to the behaviour of `accepts_nested_attributes_for` so that it can be used with rails form
77
+ # helpers.
78
+ def items_attributes=(attributes)
79
+ next_version.nodes = attributes
80
+ end
81
+
82
+ private
83
+
84
+ def unset_versions
85
+ update(draft_version: nil, published_version: nil)
86
+ end
87
+
88
+ # Returns an unsaved copy of draft version for accumulating changes.
89
+ def next_version
90
+ if draft_version.nil?
91
+ build_draft_version
92
+ elsif draft_version.persisted?
93
+ self.draft_version = draft_version.dup
94
+ else
95
+ draft_version
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ module GarbageCollection
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ after_update :remove_stale_versions
10
+ end
11
+
12
+ def remove_stale_versions
13
+ transaction do
14
+ # find all the versions that are not linked to the record
15
+ orphaned_versions = versions.inactive
16
+
17
+ next unless orphaned_versions.any?
18
+
19
+ # find items that are not included in active versions
20
+ orphaned_items = items.pluck(:id) - versions.active.pluck(:nodes).flat_map { |k| k.map(&:id) }.uniq
21
+
22
+ # delete orphaned items with a 2 hour grace period to allow for in-progress editing
23
+ items.where(id: orphaned_items, updated_at: ..2.hours.ago).destroy_all
24
+
25
+ # delete orphaned versions without a grace period as they can only be created by updates
26
+ orphaned_versions.destroy_all
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ module HasTree
6
+ # Constructs a tree from an ordered list of items with depth.
7
+ # * items that have higher depth than their predecessor are nested as `children`.
8
+ # * items with the same depth become siblings.
9
+ def tree
10
+ items.reduce(Builder.new, &:add).tree
11
+ end
12
+
13
+ class Builder
14
+ attr_reader :current, :depth, :children, :tree
15
+
16
+ def initialize
17
+ @depth = 0
18
+ @children = @tree = []
19
+ end
20
+
21
+ def add(node)
22
+ if node.depth == depth
23
+ node.parent = current
24
+ children << node
25
+ self
26
+ elsif node.depth > depth
27
+ push(children.last)
28
+ add(node)
29
+ else
30
+ pop
31
+ add(node)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_writer :current, :depth, :children
38
+
39
+ # Add node to the top of builder stacks
40
+ def push(node)
41
+ self.depth += 1
42
+ self.current = node
43
+ self.children = node.children = []
44
+ node
45
+ end
46
+
47
+ # Remove current from builder stack
48
+ def pop
49
+ previous = current
50
+ self.depth -= 1
51
+ if depth.zero?
52
+ self.current = nil
53
+ self.children = tree
54
+ else
55
+ self.current = previous.parent
56
+ self.children = current.children
57
+ end
58
+ previous
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ module Version
6
+ extend ActiveSupport::Concern
7
+
8
+ included do # rubocop:disable Metrics/BlockLength
9
+ include Katalyst::Content::HasTree
10
+
11
+ # rubocop:disable Rails/ReflectionClassName
12
+ belongs_to :parent, class_name: name.gsub(/::Version$/, ""), inverse_of: :versions
13
+ # rubocop:enable Rails/ReflectionClassName
14
+
15
+ attribute :nodes, Katalyst::Content::Types::NodesType.new, default: -> { [] }
16
+ end
17
+
18
+ def items
19
+ # support building menus in memory
20
+ # requires that items are added in order and index and depth are set
21
+ return parent.items unless parent.persisted?
22
+
23
+ items = parent.items.where(id: nodes.map(&:id)).index_by(&:id)
24
+ nodes.map do |node|
25
+ item = items[node.id]
26
+ item.index = node.index
27
+ item.depth = node.depth
28
+ item
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ class Content < Item
6
+ has_rich_text :content
7
+
8
+ validates :content, presence: true
9
+
10
+ def initialize_copy(source)
11
+ super
12
+
13
+ self.content = source.content&.body if source.content.is_a?(ActionText::RichText)
14
+ end
15
+
16
+ def self.permitted_params
17
+ super + %i[content]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ # STI base class for content items
6
+ class Item < ApplicationRecord
7
+ belongs_to :container, polymorphic: true
8
+
9
+ validates :heading, presence: true
10
+ validates :background, presence: true, inclusion: { in: Katalyst::Content.config.backgrounds }
11
+
12
+ after_initialize :initialize_tree
13
+
14
+ attr_accessor :parent, :children, :index, :depth
15
+
16
+ def self.permitted_params
17
+ %i[
18
+ container_type
19
+ container_id
20
+ type
21
+ heading
22
+ show_heading
23
+ background
24
+ visible
25
+ ]
26
+ end
27
+
28
+ private
29
+
30
+ def initialize_tree
31
+ self.parent ||= nil
32
+ self.children ||= []
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ # Data class for representing structural information about an item
6
+ class Node
7
+ include ActiveModel::Model
8
+ include ActiveModel::Attributes
9
+
10
+ attribute :id, :integer
11
+ attribute :index, :integer
12
+ attribute :depth, :integer, default: 0
13
+
14
+ attr_accessor :item
15
+
16
+ def as_json
17
+ attributes.slice("id", "depth").as_json
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ module Types
6
+ # Data serialization/deserialization for Katalyst::Content structural data
7
+ class NodesType < ActiveRecord::Type::Json
8
+ def serialize(value)
9
+ super(value.as_json)
10
+ end
11
+
12
+ def deserialize(value)
13
+ case value
14
+ when nil
15
+ nil
16
+ when String
17
+ deserialize(super)
18
+ when Hash
19
+ deserialize_params(value)
20
+ when Array
21
+ deserialize_array(value)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Deserialize a params-style array, e.g. "0" => { ... }
28
+ def deserialize_params(value)
29
+ value.map do |index, attributes|
30
+ Node.new(index: index, **attributes)
31
+ end.select(&:id).sort_by(&:index)
32
+ end
33
+
34
+ def deserialize_array(value)
35
+ value.map.with_index do |attributes, index|
36
+ Node.new(index: index, **attributes)
37
+ end.select(&:id).sort_by(&:index)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,14 @@
1
+ <figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
2
+ <% if blob.representable? %>
3
+ <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
4
+ <% end %>
5
+
6
+ <figcaption class="attachment__caption">
7
+ <% if caption = blob.try(:caption) %>
8
+ <%= caption %>
9
+ <% else %>
10
+ <span class="attachment__name"><%= blob.filename %></span>
11
+ <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
12
+ <% end %>
13
+ </figcaption>
14
+ </figure>
@@ -0,0 +1,39 @@
1
+ <%= form_with model: content, scope: :item, url: path do |form| %>
2
+ <%= form.hidden_field :container_type %>
3
+ <%= form.hidden_field :container_id %>
4
+ <%= form.hidden_field :type %>
5
+
6
+ <%= tag.div class: "errors" do %>
7
+ <% content.errors.full_messages.each do |error| %>
8
+ <li class="error"><%= error %></li>
9
+ <% end %>
10
+ <% end if content.errors.any? %>
11
+
12
+ <div class="field">
13
+ <%= form.label :heading %>
14
+ <%= form.text_field :heading %>
15
+ </div>
16
+
17
+ <div class="field">
18
+ <%= form.label :show_heading %>
19
+ <%= form.check_box :show_heading %>
20
+ </div>
21
+
22
+ <div class="field">
23
+ <%= form.label :background %>
24
+ <%= form.select :background, Katalyst::Content.config.backgrounds %>
25
+ </div>
26
+
27
+ <div class="field">
28
+ <%= form.label :visible %>
29
+ <%= form.check_box :visible %>
30
+ </div>
31
+
32
+ <div class="field">
33
+ <%= form.label :content %>
34
+ <%= form.rich_text_area :content %>
35
+ </div>
36
+
37
+ <%= form.submit "Done" %>
38
+ <%= link_to "Discard", :back %>
39
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <div class="<%= content.background %>">
2
+ <%= tag.h2 content.heading if content.show_heading? %>
3
+
4
+ <%= content.content %>
5
+ </div>
@@ -0,0 +1,11 @@
1
+ <%= content_editor_item item: item do |builder| %>
2
+ <div class="tree" data-invisible="<%= !item.visible? %>">
3
+ <%= builder.accordion_actions %>
4
+
5
+ <span role="img" value="<%= item.model_name.to_param %>" title="Type"></span>
6
+ <h4 class="heading" title="<%= item.heading %>"><%= item.heading %></h4>
7
+ <span role="img" value="invisible" title="Hidden"></span>
8
+ </div>
9
+
10
+ <%= builder.item_actions %>
11
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <li draggable="true"
2
+ data-content-item
3
+ data-content-item-id="<%= item.id %>"
4
+ data-content-index="<%= item.index %>"
5
+ data-content-depth="<%= item.depth %>"
6
+ data-deny-de-nest
7
+ data-deny-nest
8
+ data-deny-collapse
9
+ data-deny-expand
10
+ data-deny-remove
11
+ data-deny-drag
12
+ data-deny-edit>
13
+ <%= yield %>
14
+ </li>
@@ -0,0 +1,3 @@
1
+ <%= content_editor_new_item item: item, data: { item_type: item.model_name.param_key } do %>
2
+ <%= item.model_name.human %>
3
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= turbo_frame_tag "content--editor--item-frame" do %>
2
+ <div class="content--editor--new-items" role="listbox">
3
+ <%= render partial: "katalyst/content/editor/new_item", collection: content_editor_new_items(container: container), as: :item %>
4
+ </div>
5
+ <% end %>
@@ -0,0 +1,34 @@
1
+ <%= form_with model: item, scope: :item, url: path do |form| %>
2
+ <%= form.hidden_field :container_type %>
3
+ <%= form.hidden_field :container_id %>
4
+ <%= form.hidden_field :type %>
5
+
6
+ <%= tag.div class: "errors" do %>
7
+ <% item.errors.full_messages.each do |error| %>
8
+ <li class="error"><%= error %></li>
9
+ <% end %>
10
+ <% end if item.errors.any? %>
11
+
12
+ <div class="field">
13
+ <%= form.label :heading %>
14
+ <%= form.text_field :heading %>
15
+ </div>
16
+
17
+ <div class="field">
18
+ <%= form.label :show_heading %>
19
+ <%= form.check_box :show_heading %>
20
+ </div>
21
+
22
+ <div class="field">
23
+ <%= form.label :background %>
24
+ <%= form.select :background, Katalyst::Content.config.backgrounds %>
25
+ </div>
26
+
27
+ <div class="field">
28
+ <%= form.label :visible %>
29
+ <%= form.check_box :visible %>
30
+ </div>
31
+
32
+ <%= form.submit "Done" %>
33
+ <%= link_to "Discard", :back %>
34
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <div class="<%= item.background %>">
2
+ <%= tag.h2 item.heading if item.show_heading? %>
3
+ </div>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag "content--editor--item-frame" do %>
2
+ <h3 class="heading-three">Edit <%= item.model_name.human.downcase %></h3>
3
+ <%= render item, path: katalyst_content.item_path(item) %>
4
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag "content--editor--item-frame" do %>
2
+ <h3 class="heading-three">New <%= item.model_name.human.downcase %></h3>
3
+ <%= render item, path: katalyst_content.items_path %>
4
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <%= turbo_stream.replace "content--editor--item-frame" do %>
2
+ <%= render "katalyst/content/editor/new_items", container: item.container %>
3
+ <% end %>
4
+
5
+ <%= turbo_stream.replace dom_id(previous) do %>
6
+ <%= render "katalyst/content/editor/item", item: item %>
7
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <div class="trix-content">
2
+ <%= yield -%>
3
+ </div>
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin "trix"
4
+ pin "@rails/actiontext", to: "actiontext.js"
5
+
6
+ pin_all_from Katalyst::Content::Engine.root.join("app/assets/javascripts"),
7
+ # preload in tests so that we don't start clicking before controllers load
8
+ preload: Rails.env.test?
@@ -0,0 +1,12 @@
1
+ en:
2
+ views:
3
+ katalyst:
4
+ content:
5
+ editor:
6
+ draft_html: Draft
7
+ dirty_html: Unsaved changes
8
+ discard: Discard
9
+ publish: Publish
10
+ published_html: Published <i>(last updated %{last_update})</i>
11
+ revert: Revert
12
+ save: Save
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Katalyst::Content::Engine.routes.draw do
2
+ resources :items
3
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateKatalystContentItems < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :katalyst_content_items do |t|
6
+ t.string :type
7
+ t.belongs_to :container, polymorphic: true
8
+
9
+ t.string :heading, null: false
10
+ t.boolean :show_heading, null: false, default: true
11
+ t.string :background, null: false
12
+ t.boolean :visible, null: false, default: true
13
+
14
+ t.timestamps
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/configurable"
4
+
5
+ module Katalyst
6
+ module Content
7
+ class Config
8
+ include ActiveSupport::Configurable
9
+
10
+ config_accessor(:backgrounds) { %w[light dark] }
11
+ config_accessor(:items) do
12
+ %w[
13
+ Katalyst::Content::Content
14
+ ]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module Katalyst
6
+ module Content
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace Katalyst::Content
9
+
10
+ initializer "katalyst-content.factories", after: "factory_bot.set_factory_paths" do
11
+ FactoryBot.definition_file_paths << Engine.root.join("spec/factories") if defined?(FactoryBot)
12
+ end
13
+
14
+ config.generators do |g|
15
+ g.test_framework :rspec
16
+ g.fixture_replacement :factory_bot
17
+ g.factory_bot dir: "spec/factories"
18
+ end
19
+
20
+ initializer "katalyst-content.asset" do
21
+ config.after_initialize do |app|
22
+ if app.config.respond_to?(:assets)
23
+ app.config.assets.precompile += %w(katalyst-content.js)
24
+ end
25
+ end
26
+ end
27
+
28
+ initializer "katalyst-content.importmap", before: "importmap" do |app|
29
+ if app.config.respond_to?(:importmap)
30
+ app.config.importmap.paths << root.join("config/importmap.rb")
31
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "katalyst/content/config"
4
+ require "katalyst/content/engine"
5
+ require "katalyst/content/version"
6
+
7
+ module Katalyst
8
+ module Content
9
+ extend self
10
+
11
+ def config
12
+ @config ||= Config.new
13
+ end
14
+
15
+ def configure
16
+ yield config
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :yarn do
4
+ desc "Install npm packages with yarn"
5
+ task install: :environment do
6
+ sh "yarn install"
7
+ end
8
+
9
+ desc "Lint JS/SCSS files using yarn (prettier)"
10
+ task lint: :install do
11
+ sh "yarn lint"
12
+ end
13
+
14
+ desc "Autoformat JS/SCSS files using yarn (prettier)"
15
+ task format: :install do
16
+ sh "yarn format"
17
+ end
18
+ end