katalyst-content 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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