keystone_ui-blocks 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2a9effabb0c7a22247df585e2f60cf4ec1dfbd2256feeca4cfdeafc108bf170c
4
+ data.tar.gz: 01d24bf7544dbb1ab376e72edacf336f002b0ee815f13981fbe8cfacd1b196e5
5
+ SHA512:
6
+ metadata.gz: 8ed0465e1674ddd075da1d207408dbeaa5e8d59c0e071a565efff1f0ddf2e02fc17a089132ed4e2cfdcda225b32a26758e5ffa9057c9e5743e03f38386d4ebd0
7
+ data.tar.gz: 62885b92e2be83b5a448b1f8c8d816d44ffa7637799270cefc0bc0b6a57b1df432330155be7ec530b6cfcc103f343f86bdc9b1e9f506bcd83e8e4ace0f10da77
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tyler Schneider
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ .ks-blocks-layout {
2
+ display: grid;
3
+ grid-template-columns: repeat(var(--ks-blocks-columns, 12), minmax(0, 1fr));
4
+ gap: var(--ks-blocks-gap, 10px);
5
+ }
6
+
7
+ @media (max-width: 640px) {
8
+ .ks-blocks-layout {
9
+ display: block;
10
+ }
11
+
12
+ .ks-blocks-layout > * + * {
13
+ margin-top: var(--ks-blocks-gap, 10px);
14
+ }
15
+ }
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module KeystoneUi
6
+ module Blocks
7
+ class Engine < ::Rails::Engine
8
+ engine_name "keystone_ui_blocks_engine"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Blocks
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "keystone_ui/blocks/version"
4
+ require "keystone_ui/blocks/engine"
5
+ require "ks_blocks"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "keystone_ui/blocks"
@@ -0,0 +1,7 @@
1
+ module KsBlocks
2
+ BlockType = Data.define(:key, :name, :width, :height, :min_width, :max_width, :min_height, :max_height, :resizable, :once, :full_width, :fixed, :description, :group, :fields) do
3
+ def initialize(key:, name:, width:, height:, min_width: 1, max_width: nil, min_height: 1, max_height: nil, resizable: true, once: false, full_width: false, fixed: false, description: nil, group: nil, fields: [])
4
+ super
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module KsBlocks
2
+ module ContentHelper
3
+ def block_contents(blocks, &block)
4
+ tag.div(hidden: true, data: { block_contents: true }) do
5
+ safe_join(blocks.map { |placed| tag.div(capture(placed, &block), data: { block_content: placed["id"] }) })
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,114 @@
1
+ require "securerandom"
2
+ require "ks_blocks/block_type"
3
+ require "ks_blocks"
4
+
5
+ module KsBlocks
6
+ class InvalidLayout < StandardError; end
7
+
8
+ module Grid
9
+ COLUMNS = 12
10
+ ROW_HEIGHT = 60
11
+ GAP = 10
12
+
13
+ module_function
14
+
15
+ def add(blocks, block_type, x: nil, y: nil, columns: COLUMNS)
16
+ refuse_second_use(blocks, block_type)
17
+ x, y = first_open_place(blocks, block_type, columns) if x.nil? || y.nil?
18
+ added = { "id" => SecureRandom.uuid, "type" => block_type.key.to_s, "x" => x, "y" => y, "w" => block_type.width, "h" => block_type.height }
19
+ (blocks + [ added ]).tap { |arranged| refuse_unfit(arranged, [ added["id"] ], columns, []) }
20
+ end
21
+
22
+ def place(blocks, positions, columns: COLUMNS, types: [])
23
+ placed = positions.index_by { |position| position["id"] }
24
+ blocks.map { |block| block.merge(placed.fetch(block["id"], {}).slice("x", "y", "w", "h")) }.tap { |arranged| refuse_unfit(arranged, placed.keys, columns, types, blocks.index_by { |block| block["id"] }) }
25
+ end
26
+
27
+ def refuse_unfit(blocks, moved_ids, columns, types = [], blocks_before = {})
28
+ blocks.select { |block| moved_ids.include?(block["id"]) }.each do |moved|
29
+ block_type = types.find { |registered| registered.key.to_s == moved["type"] }
30
+ refuse_resizing(moved, block_type, blocks_before[moved["id"]])
31
+ refuse_outside_limits(moved, block_type)
32
+ refuse_narrower_than_the_grid(moved, block_type, columns)
33
+ refuse_moving(moved, block_type, blocks_before[moved["id"]])
34
+ raise InvalidLayout, "#{named(moved)} must be at least one column wide and one row tall" if moved["w"] < 1 || moved["h"] < 1
35
+ raise InvalidLayout, "#{named(moved)} runs past the grid's last column" if moved["x"] + moved["w"] > columns
36
+ end
37
+ refuse_overlaps(blocks, moved_ids)
38
+ end
39
+
40
+ def refuse_overlaps(blocks, moved_ids)
41
+ blocks.select { |block| moved_ids.include?(block["id"]) }.each do |moved|
42
+ covered = blocks.find { |other| other["id"] != moved["id"] && overlaps?(other, moved["x"], moved["y"], moved["w"], moved["h"]) }
43
+ raise InvalidLayout, "#{named(moved)} overlaps #{named(covered)}" if covered
44
+ end
45
+ end
46
+
47
+ def refuse_moving(block, block_type, before)
48
+ return if block_type.nil? || !block_type.fixed || before.nil?
49
+ return if block["x"] == before["x"] && block["y"] == before["y"]
50
+
51
+ raise InvalidLayout, "#{named(block)} cannot be moved"
52
+ end
53
+
54
+ def refuse_narrower_than_the_grid(block, block_type, columns)
55
+ return if block_type.nil? || !block_type.full_width || block["w"] == columns
56
+
57
+ raise InvalidLayout, "#{named(block)} must span the full width"
58
+ end
59
+
60
+ def refuse_second_use(blocks, block_type)
61
+ return unless block_type.once && blocks.any? { |block| block["type"] == block_type.key.to_s }
62
+
63
+ raise InvalidLayout, "#{block_type.name} can only be used once"
64
+ end
65
+
66
+ def refuse_resizing(block, block_type, before)
67
+ return if block_type.nil? || block_type.resizable || before.nil?
68
+ return if block["w"] == before["w"] && block["h"] == before["h"]
69
+
70
+ raise InvalidLayout, "#{named(block)} cannot be resized"
71
+ end
72
+
73
+ def refuse_outside_limits(block, block_type)
74
+ return unless block_type
75
+
76
+ raise InvalidLayout, "#{named(block)} cannot be narrower than #{block_type.min_width} columns" if block["w"] < block_type.min_width
77
+ raise InvalidLayout, "#{named(block)} cannot be shorter than #{block_type.min_height} rows" if block["h"] < block_type.min_height
78
+ raise InvalidLayout, "#{named(block)} cannot be wider than #{block_type.max_width} columns" if block_type.max_width && block["w"] > block_type.max_width
79
+ raise InvalidLayout, "#{named(block)} cannot be taller than #{block_type.max_height} rows" if block_type.max_height && block["h"] > block_type.max_height
80
+ end
81
+
82
+ def named(block)
83
+ KsBlocks.registry.block_types.find { |block_type| block_type.key.to_s == block["type"] }&.name || block["type"]
84
+ end
85
+
86
+ def fill(blocks, id, content)
87
+ blocks.map { |block| block["id"] == id ? block.merge("content" => content) : block }
88
+ end
89
+
90
+ def remove(blocks, id, types: [])
91
+ refuse_removing(blocks.find { |block| block["id"] == id }, types)
92
+ blocks.reject { |block| block["id"] == id }
93
+ end
94
+
95
+ def refuse_removing(block, types)
96
+ return if block.nil?
97
+ return unless types.find { |registered| registered.key.to_s == block["type"] }&.fixed
98
+
99
+ raise InvalidLayout, "#{named(block)} cannot be removed"
100
+ end
101
+
102
+ def first_open_place(blocks, block_type, columns)
103
+ (0..).each do |y|
104
+ (0..columns - block_type.width).each do |x|
105
+ return [ x, y ] unless blocks.any? { |block| overlaps?(block, x, y, block_type.width, block_type.height) }
106
+ end
107
+ end
108
+ end
109
+
110
+ def overlaps?(block, x, y, width, height)
111
+ x < block["x"] + block["w"] && block["x"] < x + width && y < block["y"] + block["h"] && block["y"] < y + height
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,37 @@
1
+ require "active_support/concern"
2
+ require "ks_blocks"
3
+ require "ks_blocks/grid"
4
+
5
+ module KsBlocks
6
+ module Layout
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def block_layout(column, kind: :blocks, columns: Grid::COLUMNS, row_height: Grid::ROW_HEIGHT, gap: Grid::GAP)
11
+ define_method(:add_block) do |block_type, x: nil, y: nil|
12
+ update!(column => Grid.add(public_send(column), block_type, x: x, y: y, columns: columns))
13
+ end
14
+
15
+ define_method(:place_blocks) do |positions, version: nil|
16
+ raise InvalidLayout, "This layout changed since it was last drawn" if version && version != KsBlocks.version_of(public_send(column))
17
+
18
+ update!(column => Grid.place(public_send(column), positions, columns: columns, types: KsBlocks.registry.block_types(kind: kind)))
19
+ end
20
+
21
+ define_method(:fill_block) do |id, content|
22
+ update!(column => Grid.fill(public_send(column), id, content))
23
+ end
24
+
25
+ define_method(:remove_block) do |id|
26
+ update!(column => Grid.remove(public_send(column), id, types: KsBlocks.registry.block_types(kind: kind)))
27
+ end
28
+
29
+ define_method(:block_layout_kind) { kind }
30
+
31
+ define_method(:layout_data) do
32
+ KsBlocks.layout_data(public_send(column), kind: kind, grid: { columns: columns, row_height: row_height, gap: gap })
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ require "active_support/concern"
2
+ require "ks_blocks"
3
+ require "ks_blocks/grid"
4
+
5
+ module KsBlocks
6
+ module LayoutEndpoints
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ rescue_from KsBlocks::InvalidLayout do |refusal|
11
+ render json: { error: refusal.message }, status: :unprocessable_entity
12
+ end
13
+ end
14
+
15
+ def layout
16
+ data = block_layout_record.layout_data
17
+ render json: data.merge(contents: block_contents(data[:blocks]))
18
+ end
19
+
20
+ def add_block
21
+ block_type = KsBlocks.registry.block_types(kind: block_layout_record.block_layout_kind).find { |registered| registered.key.to_s == params[:type] }
22
+ raise InvalidLayout, "No block type is registered as #{params[:type]}" unless block_type
23
+
24
+ block_layout_record.add_block(block_type, x: params[:x], y: params[:y])
25
+ head :no_content
26
+ end
27
+
28
+ def fill_block
29
+ block_layout_record.fill_block(params[:block_id], params.require(:content).permit!.to_h)
30
+ head :no_content
31
+ end
32
+
33
+ def remove_block
34
+ block_layout_record.remove_block(params[:block_id])
35
+ head :no_content
36
+ end
37
+
38
+ def block_contents(blocks)
39
+ blocks.to_h { |block| [ block["id"], block_content(block) ] }.compact
40
+ end
41
+
42
+ def block_content(_block)
43
+ nil
44
+ end
45
+
46
+ def place_blocks
47
+ block_layout_record.place_blocks(params.require(:layout).map { |position| position.permit(:id, :x, :y, :w, :h).to_h }, version: params[:version])
48
+ head :no_content
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,23 @@
1
+ require "ks_blocks"
2
+ require "ks_blocks/grid"
3
+
4
+ module KsBlocks
5
+ module LayoutHelper
6
+ def block_layout(blocks, columns: Grid::COLUMNS, gap: Grid::GAP, kind: nil, &block)
7
+ tag.div(class: "ks-blocks-layout", style: "--ks-blocks-columns: #{columns}; --ks-blocks-gap: #{gap}px") do
8
+ safe_join(block_layout_shown(blocks, kind).map { |placed| block_layout_block(placed, &block) })
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def block_layout_shown(blocks, kind)
15
+ shown = kind.nil? ? blocks : blocks.select { |placed| KsBlocks.registry.block_types(kind: kind).any? { |block_type| block_type.key.to_s == placed["type"] } }
16
+ shown.sort_by { |placed| [ placed["y"], placed["x"] ] }
17
+ end
18
+
19
+ def block_layout_block(placed, &block)
20
+ tag.div(capture(placed, &block), data: { block: placed["id"] }, style: "grid-column: #{placed['x'] + 1} / span #{placed['w']}; grid-row: #{placed['y'] + 1} / span #{placed['h']}")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ require "ks_blocks/block_type"
2
+
3
+ module KsBlocks
4
+ class Registry
5
+ def initialize
6
+ @block_types = {}
7
+ end
8
+
9
+ def register(block_type, kind: :blocks)
10
+ @block_types[[ kind.to_sym, block_type.key.to_sym ]] = block_type
11
+ end
12
+
13
+ def block_types(kind: :blocks)
14
+ @block_types.filter_map { |(registered_kind, _key), block_type| block_type if registered_kind == kind.to_sym }
15
+ end
16
+ end
17
+ end
data/lib/ks_blocks.rb ADDED
@@ -0,0 +1,23 @@
1
+ require "digest"
2
+ require "json"
3
+ require "ks_blocks/registry"
4
+
5
+ module KsBlocks
6
+ class << self
7
+ def block(key, name:, width:, height:, kind: :blocks, **limits)
8
+ registry.register(BlockType.new(key: key, name: name, width: width, height: height, **limits), kind: kind)
9
+ end
10
+
11
+ def layout_data(blocks, kind: :blocks, grid: {})
12
+ { block_types: registry.block_types(kind: kind).map(&:to_h), blocks: blocks, version: version_of(blocks), grid: grid }
13
+ end
14
+
15
+ def version_of(blocks)
16
+ Digest::SHA256.hexdigest(JSON.generate(blocks))
17
+ end
18
+
19
+ def registry
20
+ @registry ||= Registry.new
21
+ end
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: keystone_ui-blocks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tyler Schneider
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: actionview
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '7.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '7.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: keystone_ui-react
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 0.1.2
69
+ description: Block types a host registers, a layout its own record keeps, and a grid
70
+ a designer arranges blocks on, following keystone_ui's design.
71
+ email:
72
+ - tylercschneider@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - MIT-LICENSE
78
+ - app/assets/stylesheets/ks_blocks.css
79
+ - lib/keystone_ui-blocks.rb
80
+ - lib/keystone_ui/blocks.rb
81
+ - lib/keystone_ui/blocks/engine.rb
82
+ - lib/keystone_ui/blocks/version.rb
83
+ - lib/ks_blocks.rb
84
+ - lib/ks_blocks/block_type.rb
85
+ - lib/ks_blocks/content_helper.rb
86
+ - lib/ks_blocks/grid.rb
87
+ - lib/ks_blocks/layout.rb
88
+ - lib/ks_blocks/layout_endpoints.rb
89
+ - lib/ks_blocks/layout_helper.rb
90
+ - lib/ks_blocks/registry.rb
91
+ homepage: https://github.com/DYB-Development/keystone_ui-blocks
92
+ licenses:
93
+ - MIT
94
+ metadata:
95
+ source_code_uri: https://github.com/DYB-Development/keystone_ui-blocks
96
+ changelog_uri: https://github.com/DYB-Development/keystone_ui-blocks/blob/main/CHANGELOG.md
97
+ bug_tracker_uri: https://github.com/DYB-Development/keystone_ui-blocks/issues
98
+ documentation_uri: https://github.com/DYB-Development/keystone_ui-blocks#readme
99
+ rubygems_mfa_required: 'true'
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 3.2.0
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.5.16
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: A drag-and-drop block grid for Rails engine gems.
119
+ test_files: []