rails_notion_like_multiselect 0.1.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.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNotionLikeMultiselect
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace RailsNotionLikeMultiselect
6
+
7
+ # Add helpers to Rails applications
8
+ initializer 'rails_notion_like_multiselect.helpers' do
9
+ ActiveSupport.on_load(:action_view) do
10
+ include RailsNotionLikeMultiselect::Helpers::MultiselectHelper
11
+ end
12
+ end
13
+
14
+ # Add JavaScript to importmap
15
+ initializer 'rails_notion_like_multiselect.importmap', before: 'importmap' do |app|
16
+ app.config.importmap.paths << Engine.root.join('config/importmap.rb') if defined?(Importmap)
17
+ end
18
+
19
+ # Add assets to precompile
20
+ initializer 'rails_notion_like_multiselect.assets' do |app|
21
+ app.config.assets.paths << Engine.root.join('app/javascript')
22
+ app.config.assets.precompile += %w[
23
+ rails_notion_like_multiselect_controller.js
24
+ ]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNotionLikeMultiselect
4
+ module Helpers
5
+ module MultiselectHelper
6
+ # Renders a Notion-like multiselect component
7
+ #
8
+ # @param form [ActionView::Helpers::FormBuilder] The form builder object
9
+ # @param field [Symbol] The field name (e.g., :category_ids, :tag_ids)
10
+ # @param options [Hash] Options for customizing the multiselect
11
+ # @option options [Array] :collection The collection of items to select from
12
+ # @option options [Array] :selected The currently selected items
13
+ # @option options [Boolean] :allow_create Whether to allow creating new items (default: false)
14
+ # @option options [String] :placeholder Placeholder text for the input
15
+ # @option options [String] :label Label text for the field
16
+ # @option options [String] :item_type Type of items (e.g., 'category', 'tag')
17
+ # @option options [String] :badge_color Color scheme for badges (e.g., 'blue', 'green', 'purple')
18
+ # @option options [String] :api_endpoint API endpoint for creating new items
19
+ # @option options [String] :help_text Help text to display below the field
20
+ # @option options [String] :theme Theme mode ('light', 'dark', or 'auto')
21
+ #
22
+ def multiselect_field(form, field, options = {})
23
+ collection = options[:collection] || []
24
+ selected = options[:selected] || []
25
+ allow_create = options[:allow_create] || false
26
+ placeholder = options[:placeholder] || RailsNotionLikeMultiselect.default_placeholder
27
+ label = options[:label] || field.to_s.humanize
28
+ item_type = options[:item_type] || field.to_s.singularize
29
+ badge_color = options[:badge_color] || RailsNotionLikeMultiselect.default_badge_color
30
+ api_endpoint = options[:api_endpoint]
31
+ help_text = options[:help_text]
32
+ theme = options[:theme] || 'auto'
33
+ input_name = "#{form.object_name}[#{field}][]"
34
+
35
+ # Determine theme-specific classes based on theme option
36
+ is_dark_theme = theme == 'dark'
37
+ is_light_theme = theme == 'light'
38
+
39
+ # Support both light and dark modes with proper contrast
40
+ badge_classes = if is_dark_theme
41
+ # Force dark theme styles
42
+ case badge_color
43
+ when 'green'
44
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
45
+ 'bg-green-900/30 text-green-200 border border-green-800'
46
+ when 'purple'
47
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
48
+ 'bg-purple-900/30 text-purple-200 border border-purple-800'
49
+ when 'yellow'
50
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
51
+ 'bg-yellow-900/30 text-yellow-200 border border-yellow-800'
52
+ when 'red'
53
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
54
+ 'bg-red-900/30 text-red-200 border border-red-800'
55
+ else
56
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
57
+ 'bg-blue-900/30 text-blue-200 border border-blue-800'
58
+ end
59
+ elsif is_light_theme
60
+ # Force light theme styles
61
+ case badge_color
62
+ when 'green'
63
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
64
+ 'bg-green-100 text-green-800 border border-green-200'
65
+ when 'purple'
66
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
67
+ 'bg-purple-100 text-purple-800 border border-purple-200'
68
+ when 'yellow'
69
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
70
+ 'bg-yellow-100 text-yellow-800 border border-yellow-200'
71
+ when 'red'
72
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
73
+ 'bg-red-100 text-red-800 border border-red-200'
74
+ else
75
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
76
+ 'bg-blue-100 text-blue-800 border border-blue-200'
77
+ end
78
+ else
79
+ # Auto mode - use Tailwind's dark: variants
80
+ case badge_color
81
+ when 'green'
82
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
83
+ 'bg-green-100 text-green-800 border border-green-200 ' +
84
+ 'dark:bg-green-900/30 dark:text-green-200 dark:border-green-800'
85
+ when 'purple'
86
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
87
+ 'bg-purple-100 text-purple-800 border border-purple-200 ' +
88
+ 'dark:bg-purple-900/30 dark:text-purple-200 dark:border-purple-800'
89
+ when 'yellow'
90
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
91
+ 'bg-yellow-100 text-yellow-800 border border-yellow-200 ' +
92
+ 'dark:bg-yellow-900/30 dark:text-yellow-200 dark:border-yellow-800'
93
+ when 'red'
94
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
95
+ 'bg-red-100 text-red-800 border border-red-200 ' +
96
+ 'dark:bg-red-900/30 dark:text-red-200 dark:border-red-800'
97
+ else
98
+ 'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
99
+ 'bg-blue-100 text-blue-800 border border-blue-200 ' +
100
+ 'dark:bg-blue-900/30 dark:text-blue-200 dark:border-blue-800'
101
+ end
102
+ end
103
+
104
+ content_tag :div,
105
+ data: {
106
+ controller: 'rails-notion-multiselect',
107
+ rails_notion_multiselect_allow_create_value: allow_create,
108
+ rails_notion_multiselect_item_type_value: item_type,
109
+ rails_notion_multiselect_input_name_value: input_name,
110
+ rails_notion_multiselect_placeholder_value: placeholder,
111
+ rails_notion_multiselect_create_prompt_value: "Create \"#{item_type}\"",
112
+ rails_notion_multiselect_badge_class_value: badge_classes,
113
+ rails_notion_multiselect_api_endpoint_value: api_endpoint,
114
+ rails_notion_multiselect_theme_value: theme
115
+ },
116
+ class: 'relative',
117
+ style: 'z-index: auto;' do
118
+ # Label
119
+ label_class = if is_dark_theme
120
+ 'block text-sm font-medium text-gray-300 mb-2'
121
+ elsif is_light_theme
122
+ 'block text-sm font-medium text-gray-700 mb-2'
123
+ else
124
+ 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'
125
+ end
126
+ label_html = content_tag(:label, label, class: label_class)
127
+
128
+ # Input container with selected items inside
129
+ input_container_class = if is_dark_theme
130
+ 'flex flex-wrap items-center gap-1.5 rounded-lg ' +
131
+ 'bg-gray-900 py-2 px-3 text-sm text-white ' +
132
+ 'border border-gray-700 ' +
133
+ 'focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-transparent ' +
134
+ 'min-h-[42px]'
135
+ elsif is_light_theme
136
+ 'flex flex-wrap items-center gap-1.5 rounded-lg ' +
137
+ 'bg-white py-2 px-3 text-sm text-gray-900 ' +
138
+ 'border border-gray-300 ' +
139
+ 'focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-transparent ' +
140
+ 'min-h-[42px]'
141
+ else
142
+ 'flex flex-wrap items-center gap-1.5 rounded-lg ' +
143
+ 'bg-white dark:bg-gray-900 ' +
144
+ 'py-2 px-3 text-sm ' +
145
+ 'text-gray-900 dark:text-white ' +
146
+ 'border border-gray-300 dark:border-gray-700 ' +
147
+ 'focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-transparent ' +
148
+ 'min-h-[42px]'
149
+ end
150
+ input_container_html = content_tag :div,
151
+ class: input_container_class,
152
+ data: { action: 'click->rails-notion-multiselect#focusInput' } do
153
+ # Selected items display inside input
154
+ selected_items_html = content_tag :div,
155
+ data: { rails_notion_multiselect_target: 'selectedItems' },
156
+ class: 'flex flex-wrap gap-1.5 items-center' do
157
+ selected.map do |item|
158
+ item_id = item.respond_to?(:id) ? item.id.to_s : item.to_s
159
+ item_name = item.respond_to?(:name) ? item.name : item.to_s
160
+
161
+ content_tag :span,
162
+ class: badge_classes,
163
+ data: { item_id: item_id } do
164
+ content_tag(:span, item_name, data: { item_name: true }) +
165
+ button_class = if is_dark_theme
166
+ 'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-400/20'
167
+ elsif is_light_theme
168
+ 'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20'
169
+ else
170
+ 'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20 dark:hover:bg-gray-400/20'
171
+ end
172
+ content_tag(:button,
173
+ type: 'button',
174
+ data: { action: 'click->rails-notion-multiselect#handleRemove', item_id: item_id },
175
+ class: button_class) do
176
+ content_tag(:svg,
177
+ xmlns: 'http://www.w3.org/2000/svg',
178
+ viewBox: '0 0 14 14',
179
+ fill: 'none',
180
+ stroke: 'currentColor',
181
+ 'stroke-width': '2',
182
+ 'stroke-linecap': 'round',
183
+ 'stroke-linejoin': 'round',
184
+ class: 'h-3.5 w-3.5 opacity-60 group-hover:opacity-100') do
185
+ tag.path(d: 'M4 4l6 6m0-6l-6 6')
186
+ end
187
+ end
188
+ end
189
+ end.join.html_safe
190
+ end
191
+
192
+ # Input field
193
+ input_placeholder_class = if is_dark_theme
194
+ 'placeholder-gray-500'
195
+ elsif is_light_theme
196
+ 'placeholder-gray-400'
197
+ else
198
+ 'placeholder-gray-400 dark:placeholder-gray-500'
199
+ end
200
+ input_html = tag.input(type: 'text',
201
+ placeholder: selected.empty? ? placeholder : '',
202
+ data: { rails_notion_multiselect_target: 'input' },
203
+ class: "flex-1 bg-transparent border-0 outline-none focus:outline-none min-w-[120px] #{input_placeholder_class}")
204
+
205
+ selected_items_html + input_html
206
+ end
207
+
208
+ # Dropdown
209
+ dropdown_class = if is_dark_theme
210
+ 'absolute mt-1 w-full rounded-lg bg-gray-800 shadow-lg ring-1 ring-white/10'
211
+ elsif is_light_theme
212
+ 'absolute mt-1 w-full rounded-lg bg-white shadow-lg ring-1 ring-black/5'
213
+ else
214
+ 'absolute mt-1 w-full rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10'
215
+ end
216
+ dropdown_html = content_tag :div,
217
+ data: { rails_notion_multiselect_target: 'dropdown' },
218
+ style: 'display: none; z-index: 9999;',
219
+ class: dropdown_class do
220
+ content_tag :div,
221
+ data: { rails_notion_multiselect_target: 'optionsList' },
222
+ class: 'max-h-60 overflow-auto py-1' do
223
+ collection.map do |item|
224
+ item_id = item.respond_to?(:id) ? item.id.to_s : item.to_s
225
+ item_name = item.respond_to?(:name) ? item.name : item.to_s
226
+ is_selected = selected.any? { |s|
227
+ s_id = s.respond_to?(:id) ? s.id.to_s : s.to_s
228
+ s_id == item_id
229
+ }
230
+
231
+ content_tag :div,
232
+ data: {
233
+ option_id: item_id,
234
+ option_name: item_name
235
+ },
236
+ class: "px-3 py-2 text-sm cursor-pointer flex items-center #{if is_selected
237
+ if is_dark_theme
238
+ 'bg-blue-600 text-white hover:bg-blue-700'
239
+ elsif is_light_theme
240
+ 'bg-blue-500 text-white hover:bg-blue-600'
241
+ else
242
+ 'bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700'
243
+ end
244
+ elsif is_dark_theme
245
+ 'text-gray-300 hover:bg-gray-700'
246
+ elsif is_light_theme
247
+ 'text-gray-700 hover:bg-gray-100'
248
+ else
249
+ 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
250
+ end}" do
251
+ content_tag(:span, item_name, class: 'flex-1')
252
+ end
253
+ end.join.html_safe
254
+ end
255
+ end
256
+
257
+ # Hidden inputs container
258
+ hidden_inputs_html = content_tag :div, data: { rails_notion_multiselect_target: 'hiddenInputs' } do
259
+ if selected.any?
260
+ selected.map do |item|
261
+ item_id = item.respond_to?(:id) ? item.id.to_s : item.to_s
262
+ tag.input(type: 'hidden', name: input_name, value: item_id)
263
+ end.join.html_safe
264
+ else
265
+ tag.input(type: 'hidden', name: input_name, value: '')
266
+ end
267
+ end
268
+
269
+ # Help text
270
+ help_text_class = if is_dark_theme
271
+ 'mt-1 text-xs text-gray-400'
272
+ elsif is_light_theme
273
+ 'mt-1 text-xs text-gray-500'
274
+ else
275
+ 'mt-1 text-xs text-gray-500 dark:text-gray-400'
276
+ end
277
+ help_text_html = if help_text.present?
278
+ content_tag(:p, help_text, class: help_text_class)
279
+ else
280
+ ''.html_safe
281
+ end
282
+
283
+ label_html + input_container_html + dropdown_html + hidden_inputs_html + help_text_html
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNotionLikeMultiselect
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rails_notion_like_multiselect/version'
4
+ require_relative 'rails_notion_like_multiselect/engine'
5
+ require_relative 'rails_notion_like_multiselect/helpers/multiselect_helper'
6
+
7
+ module RailsNotionLikeMultiselect
8
+ class Error < StandardError; end
9
+
10
+ # Configuration
11
+ mattr_accessor :default_badge_color
12
+ @@default_badge_color = 'blue'
13
+
14
+ mattr_accessor :default_placeholder
15
+ @@default_placeholder = 'Search or select...'
16
+
17
+ mattr_accessor :enable_keyboard_navigation
18
+ @@enable_keyboard_navigation = true
19
+
20
+ # Setup block for configuration
21
+ def self.setup
22
+ yield self
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_notion_like_multiselect
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Sulman Baig
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: stimulus-rails
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: tailwindcss-rails
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '4.0'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '4.0'
60
+ - !ruby/object:Gem::Dependency
61
+ name: bundler
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '2.0'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '2.0'
74
+ - !ruby/object:Gem::Dependency
75
+ name: rake
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '13.0'
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '13.0'
88
+ - !ruby/object:Gem::Dependency
89
+ name: rspec
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '3.0'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '3.0'
102
+ description: A beautiful, keyboard-navigable multiselect component for Rails applications,
103
+ inspired by Notion's elegant UI/UX. Features include real-time search, inline item
104
+ creation, full keyboard navigation, dark mode support, and customizable themes.
105
+ Built with Hotwire Stimulus and Tailwind CSS for modern Rails applications.
106
+ email:
107
+ - sulmanweb@gmail.com
108
+ executables: []
109
+ extensions: []
110
+ extra_rdoc_files: []
111
+ files:
112
+ - CHANGELOG.md
113
+ - LICENSE.txt
114
+ - README.md
115
+ - Rakefile
116
+ - app/javascript/rails_notion_multiselect_controller.js
117
+ - config/importmap.rb
118
+ - lib/generators/rails_notion_like_multiselect/install/install_generator.rb
119
+ - lib/generators/rails_notion_like_multiselect/install/templates/rails_notion_multiselect_controller.js
120
+ - lib/rails_notion_like_multiselect.rb
121
+ - lib/rails_notion_like_multiselect/engine.rb
122
+ - lib/rails_notion_like_multiselect/helpers/multiselect_helper.rb
123
+ - lib/rails_notion_like_multiselect/version.rb
124
+ homepage: https://github.com/pageinteract/rails_notion_like_multiselect
125
+ licenses:
126
+ - MIT
127
+ metadata:
128
+ allowed_push_host: https://rubygems.org
129
+ homepage_uri: https://github.com/pageinteract/rails_notion_like_multiselect
130
+ source_code_uri: https://github.com/pageinteract/rails_notion_like_multiselect/tree/main
131
+ changelog_uri: https://github.com/pageinteract/rails_notion_like_multiselect/blob/main/CHANGELOG.md
132
+ bug_tracker_uri: https://github.com/pageinteract/rails_notion_like_multiselect/issues
133
+ documentation_uri: https://github.com/pageinteract/rails_notion_like_multiselect#readme
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: 3.0.0
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.6.9
149
+ specification_version: 4
150
+ summary: A Notion-like multiselect component for Rails applications with Tailwind
151
+ CSS
152
+ test_files: []