nitro_kit 0.1.0 → 0.3.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -1
  4. data/Rakefile +6 -4
  5. data/app/components/nitro_kit/accordion.rb +69 -33
  6. data/app/components/nitro_kit/alert.rb +69 -0
  7. data/app/components/nitro_kit/avatar.rb +52 -0
  8. data/app/components/nitro_kit/badge.rb +47 -23
  9. data/app/components/nitro_kit/button.rb +97 -65
  10. data/app/components/nitro_kit/button_group.rb +18 -13
  11. data/app/components/nitro_kit/card.rb +49 -9
  12. data/app/components/nitro_kit/checkbox.rb +59 -41
  13. data/app/components/nitro_kit/checkbox_group.rb +38 -0
  14. data/app/components/nitro_kit/combobox.rb +138 -0
  15. data/app/components/nitro_kit/component.rb +46 -17
  16. data/app/components/nitro_kit/datepicker.rb +9 -0
  17. data/app/components/nitro_kit/dialog.rb +95 -0
  18. data/app/components/nitro_kit/dropdown.rb +116 -73
  19. data/app/components/nitro_kit/field.rb +281 -30
  20. data/app/components/nitro_kit/field_group.rb +10 -5
  21. data/app/components/nitro_kit/fieldset.rb +42 -7
  22. data/app/components/nitro_kit/form_builder.rb +45 -22
  23. data/app/components/nitro_kit/icon.rb +29 -8
  24. data/app/components/nitro_kit/input.rb +26 -0
  25. data/app/components/nitro_kit/label.rb +18 -5
  26. data/app/components/nitro_kit/pagination.rb +98 -0
  27. data/app/components/nitro_kit/radio_button.rb +28 -27
  28. data/app/components/nitro_kit/radio_button_group.rb +53 -0
  29. data/app/components/nitro_kit/select.rb +72 -0
  30. data/app/components/nitro_kit/switch.rb +49 -39
  31. data/app/components/nitro_kit/table.rb +56 -0
  32. data/app/components/nitro_kit/tabs.rb +98 -0
  33. data/app/components/nitro_kit/textarea.rb +26 -0
  34. data/app/components/nitro_kit/toast.rb +104 -0
  35. data/app/components/nitro_kit/tooltip.rb +53 -0
  36. data/app/helpers/nitro_kit/accordion_helper.rb +3 -1
  37. data/app/helpers/nitro_kit/alert_helper.rb +11 -0
  38. data/app/helpers/nitro_kit/avatar_helper.rb +9 -0
  39. data/app/helpers/nitro_kit/badge_helper.rb +3 -5
  40. data/app/helpers/nitro_kit/button_group_helper.rb +2 -0
  41. data/app/helpers/nitro_kit/button_helper.rb +37 -28
  42. data/app/helpers/nitro_kit/card_helper.rb +2 -0
  43. data/app/helpers/nitro_kit/checkbox_helper.rb +19 -16
  44. data/app/helpers/nitro_kit/combobox_helper.rb +9 -0
  45. data/app/helpers/nitro_kit/datepicker_helper.rb +9 -0
  46. data/app/helpers/nitro_kit/dialog_helper.rb +9 -0
  47. data/app/helpers/nitro_kit/dropdown_helper.rb +3 -1
  48. data/app/helpers/nitro_kit/field_group_helper.rb +9 -0
  49. data/app/helpers/nitro_kit/field_helper.rb +4 -2
  50. data/app/helpers/nitro_kit/fieldset_helper.rb +9 -0
  51. data/app/helpers/nitro_kit/form_helper.rb +13 -0
  52. data/app/helpers/nitro_kit/icon_helper.rb +3 -1
  53. data/app/helpers/nitro_kit/input_helper.rb +35 -0
  54. data/app/helpers/nitro_kit/label_helper.rb +12 -8
  55. data/app/helpers/nitro_kit/pagination_helper.rb +42 -0
  56. data/app/helpers/nitro_kit/radio_button_helper.rb +15 -12
  57. data/app/helpers/nitro_kit/select_helper.rb +24 -0
  58. data/app/helpers/nitro_kit/switch_helper.rb +4 -10
  59. data/app/helpers/nitro_kit/table_helper.rb +9 -0
  60. data/app/helpers/nitro_kit/tabs_helper.rb +9 -0
  61. data/app/helpers/nitro_kit/textarea_helper.rb +9 -0
  62. data/app/helpers/nitro_kit/toast_helper.rb +36 -0
  63. data/app/helpers/nitro_kit/tooltip_helper.rb +9 -0
  64. data/lib/generators/nitro_kit/add_generator.rb +38 -41
  65. data/lib/generators/nitro_kit/install_generator.rb +2 -1
  66. data/lib/nitro_kit/engine.rb +4 -0
  67. data/lib/nitro_kit/schema_builder.rb +90 -16
  68. data/lib/nitro_kit/version.rb +1 -1
  69. data/lib/nitro_kit.rb +39 -1
  70. data/lib/tasks/nitro_kit_tasks.rake +4 -0
  71. metadata +40 -12
  72. data/app/components/nitro_kit/radio_group.rb +0 -35
  73. data/app/helpers/application_helper.rb +0 -89
  74. data/lib/nitro_kit/railtie.rb +0 -8
@@ -1,110 +1,153 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NitroKit
2
4
  class Dropdown < Component
3
- include Phlex::Rails::Helpers::LinkTo
4
-
5
- CONTENT = [
6
- "w-max-content absolute top-0 left-0",
7
- "p-1 bg-background rounded-md border shadow-sm",
8
- "w-fit max-w-sm flex-col text-left",
9
- "[&[aria-hidden=true]]:hidden flex"
10
- ].freeze
5
+ ITEM_VARIANTS = %i[default destructive]
11
6
 
12
- ITEM = [
13
- "px-3 py-1.5 rounded",
14
- "font-medium truncate",
15
- "cursor-default"
16
- ].freeze
17
-
18
- ITEM_VARIANTS = {
19
- default: ["hover:bg-muted"],
20
- destructive: ["text-destructive-foreground hover:bg-destructive hover:text-white"]
21
- }.freeze
22
-
23
- SEPARATOR = "border-t my-1 -mx-1"
7
+ include Phlex::Rails::Helpers::LinkTo
24
8
 
25
9
  def initialize(placement: nil, **attrs)
26
10
  @placement = placement
27
- @attrs = attrs
11
+
12
+ super(
13
+ attrs,
14
+ data: {
15
+ controller: "nk--dropdown",
16
+ nk__dropdown_placement_value: placement
17
+ }
18
+ )
28
19
  end
29
20
 
30
21
  attr_reader :placement
31
22
 
32
- def view_template(&block)
33
- div(data: {:controller => "nk--dropdown", :"nk--dropdown-placement-value" => placement}, &block)
23
+ def view_template
24
+ div(**mattr(attrs)) do
25
+ yield
26
+ end
34
27
  end
35
28
 
36
- def trigger(**attrs, &block)
37
- class_list = "inline-block"
38
- data = {
39
- :"nk--dropdown-target" => "trigger",
40
- :action => "click->nk--dropdown#toggle",
41
- **attrs.fetch(:data, {})
42
- }
43
- div(
44
- **attrs,
45
- class: class_list,
46
- data:,
29
+ def trigger(text = nil, as: NitroKit::Button, **attrs, &block)
30
+ trigger_attrs = mattr(
31
+ attrs,
47
32
  aria: {haspopup: "true", expanded: "false"},
48
- &block
33
+ data: {nk__dropdown_target: "trigger", action: "click->nk--dropdown#toggle"}
49
34
  )
50
- end
51
35
 
52
- def content(**attrs, &block)
53
- class_list = merge([CONTENT, attrs[:class]])
36
+ case as
37
+ when Symbol
38
+ send(as, **trigger_attrs) do
39
+ text_or_block(text, &block)
40
+ end
41
+ else
42
+ render(as.new(**trigger_attrs)) do
43
+ text_or_block(text, &block)
44
+ end
45
+ end
46
+ end
54
47
 
55
- data = {
56
- :"nk--dropdown-target" => "content",
57
- **attrs.fetch(:data, {})
58
- }
48
+ def content(as: :div, **attrs)
59
49
  div(
60
- **attrs,
61
- class: class_list,
62
- data:,
63
- role: "menu",
64
- aria: {hidden: "true"},
65
- &block
66
- )
50
+ **mattr(
51
+ attrs,
52
+ role: "menu",
53
+ aria: {hidden: "true"},
54
+ class: content_class,
55
+ data: {nk__dropdown_target: "content"},
56
+ popover: true
57
+ )
58
+ ) do
59
+ yield
60
+ end
67
61
  end
68
62
 
69
63
  def title(text = nil, **attrs, &block)
70
- class_list = merge(["px-3 pt-2 pb-1.5 text-muted-foreground text-sm", attrs[:class]])
71
- div(**attrs, class: class_list) { text || block.call }
64
+ div(**mattr(attrs, class: title_class)) do
65
+ text_or_block(text, &block)
66
+ end
72
67
  end
73
68
 
74
- def item(
75
- text = nil,
76
- href = nil,
77
- variant: :default,
78
- **attrs,
79
- &block
80
- )
81
- class_list = merge([ITEM, ITEM_VARIANTS[variant], attrs[:class]])
82
-
83
- common_attrs = {
84
- **attrs,
85
- class: class_list,
69
+ def item(text = nil, href: nil, variant: :default, **attrs, &block)
70
+ common_attrs = mattr(
71
+ attrs,
86
72
  role: "menuitem",
87
- tabindex: "-1"
88
- }
73
+ tabindex: "-1",
74
+ class: [item_class, item_variant_class(variant)]
75
+ )
89
76
 
90
77
  if href
91
- link_to(
92
- href,
93
- **common_attrs
94
- ) {
95
- text || block.call
96
- }
78
+ link_to(href, **common_attrs) do
79
+ text_or_block(text, &block)
80
+ end
97
81
  else
98
- div(**common_attrs) { text || block.call }
82
+ div(**common_attrs) do
83
+ text_or_block(text, &block)
84
+ end
99
85
  end
100
86
  end
101
87
 
88
+ def item_to(
89
+ text_or_href,
90
+ href = nil,
91
+ **attrs,
92
+ &block
93
+ )
94
+ href = text_or_href if block_given?
95
+ item(text_or_href, href: href, **attrs, &block)
96
+ end
97
+
102
98
  def destructive_item(*args, **attrs, &block)
103
99
  item(*args, **attrs, variant: :destructive, &block)
104
100
  end
105
101
 
102
+ def destructive_item_to(text_or_block, href = nil, **attrs, &block)
103
+ href = args.shift if block_given?
104
+ destructive_item(text_or_block, href: href, **attrs, &block)
105
+ end
106
+
106
107
  def separator
107
- div(class: SEPARATOR)
108
+ hr(class: separator_class)
109
+ end
110
+
111
+ private
112
+
113
+ def content_class
114
+ [
115
+ "z-10 w-max-content absolute top-0 left-0",
116
+ "p-1 bg-background text-foreground rounded-md border shadow-sm",
117
+ "w-fit max-w-sm flex-col text-left",
118
+ "[&[aria-hidden=true]]:hidden flex"
119
+ ]
120
+ end
121
+
122
+ def trigger_class
123
+ ""
124
+ end
125
+
126
+ def title_class
127
+ "px-3 pt-2 pb-1.5 text-muted-foreground text-sm"
128
+ end
129
+
130
+ def item_class
131
+ [
132
+ "px-3 py-1.5 rounded",
133
+ "font-medium truncate",
134
+ "cursor-default"
135
+ ]
136
+ end
137
+
138
+ def item_variant_class(variant)
139
+ case variant
140
+ when :default
141
+ "[&[href]]:hover:bg-muted"
142
+ when :destructive
143
+ "text-destructive-foreground [&[href]]:hover:bg-destructive [&[href]]:hover:text-white"
144
+ else
145
+ raise ArgumentError, "Unknown variant: #{variant.inspect}"
146
+ end
147
+ end
148
+
149
+ def separator_class
150
+ "border-t my-1 -mx-1"
108
151
  end
109
152
  end
110
153
  end
@@ -1,37 +1,288 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NitroKit
2
4
  class Field < Component
3
- FIELD_BASE = [
4
- "flex flex-col gap-2 align-start",
5
- "[&:has([data-slot='error'])_[data-slot='control']]:border-destructive"
6
- ].freeze
7
- LABEL_BASE = "text-sm font-medium"
8
- DESCRIPTION_BASE = "text-sm text-muted-foreground"
9
- ERROR_BASE = "text-sm text-destructive"
10
- INPUT_BASE = [
11
- "rounded-md border bg-background border-border text-base px-3 py-2",
12
- "focus:outline-none focus:ring-2 focus:ring-primary",
13
- ""
14
- ].freeze
15
-
16
- def initialize(attribute, as: :string, label: nil, description: nil, errors: nil, **attrs)
17
- @attribute = attribute
18
- @as = as
19
- @label_text = label
20
- @description_text = description
21
- @errors = errors || []
22
- @attrs = attrs
23
- end
24
-
25
- attr_reader :attribute, :as, :label_text, :description_text, :errors, :attrs
26
-
27
- def view_template(&block)
28
- div(**attrs, class: FIELD_BASE) do
29
- label(**attrs, data: {slot: "label"}, class: LABEL_BASE) { label_text }
30
- input(**attrs, data: {slot: "control"}, class: INPUT_BASE)
31
- errors.each do |msg|
32
- div(**attrs, data: {slot: "error"}, class: ERROR_BASE) { msg }
5
+ def initialize(
6
+ form = nil,
7
+ field_name = nil,
8
+ as: :string,
9
+ label: nil,
10
+ description: nil,
11
+ errors: nil,
12
+ **attrs
13
+ )
14
+ @form = form
15
+ @field_name = field_name.to_s
16
+ @as = as.to_sym
17
+
18
+ @name = attrs[:name] || form&.field_name(field_name)
19
+ @id = attrs[:id] || form&.field_id(field_name)
20
+
21
+ # select
22
+ @options = attrs[:options]
23
+
24
+ @field_attrs = attrs
25
+ @field_label = label || field_name.to_s.humanize
26
+ @field_description = description
27
+ @field_error_messages = errors
28
+
29
+ super(
30
+ attrs,
31
+ data: {as: @as},
32
+ class: base_class
33
+ )
34
+ end
35
+
36
+ attr_reader(
37
+ :as,
38
+ :form,
39
+ :name,
40
+ :id,
41
+ :field_attrs,
42
+ :field_label,
43
+ :field_description,
44
+ :field_error_messages
45
+ )
46
+
47
+ def view_template
48
+ div(**attrs) do
49
+ if block_given?
50
+ yield
51
+ else
52
+ default_field
53
+ end
54
+ end
55
+ end
56
+
57
+ alias :html_label :label
58
+
59
+ def label(text = nil, **attrs)
60
+ text ||= field_label
61
+
62
+ return unless text
63
+
64
+ render(Label.new(**mattr(attrs, for: id, data: {slot: "label"}))) do
65
+ text
66
+ end
67
+ end
68
+
69
+ def description(text = nil, **attrs, &block)
70
+ text ||= field_description
71
+
72
+ return unless text || block_given?
73
+
74
+ div(**mattr(attrs, data: {slot: "description"}, class: description_class)) do
75
+ text_or_block(text, &block)
76
+ end
77
+ end
78
+
79
+ def errors(error_messages = nil, **attrs)
80
+ error_messages ||= field_error_messages
81
+
82
+ return unless error_messages&.any?
83
+
84
+ ul(**mattr(attrs, data: {slot: "error"}, class: error_class)) do |msg|
85
+ error_messages.each do |msg|
86
+ li { msg }
33
87
  end
34
88
  end
35
89
  end
90
+
91
+ def control(**attrs)
92
+ case as
93
+ when :string
94
+ input(**attrs)
95
+ when
96
+ :button,
97
+ :color,
98
+ :date,
99
+ :datetime,
100
+ :datetime_local,
101
+ :email,
102
+ :file,
103
+ :hidden,
104
+ :month,
105
+ :number,
106
+ :password,
107
+ :range,
108
+ :search,
109
+ :tel,
110
+ :text,
111
+ :time,
112
+ :url,
113
+ :week
114
+ input(type: as, **attrs)
115
+ when :select
116
+ select(**attrs)
117
+ when :textarea
118
+ textarea(**attrs)
119
+ when :checkbox
120
+ checkbox(**attrs)
121
+ when :combobox
122
+ combobox(**attrs)
123
+ when :radio, :radio_button, :radio_group
124
+ radio_group(**attrs)
125
+ when :switch
126
+ switch(**attrs)
127
+ else
128
+ raise ArgumentError, "Invalid field type `#{as}'"
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ def default_field
135
+ case as
136
+ when :checkbox
137
+ control
138
+ label
139
+ description
140
+ errors
141
+ else
142
+ label
143
+ description
144
+ control
145
+ errors
146
+ end
147
+ end
148
+
149
+ def control_attrs(**attrs)
150
+ mattr(
151
+ attrs,
152
+ name:,
153
+ id:,
154
+ value: value_before_typecast,
155
+ data: {slot: "control"}
156
+ )
157
+ end
158
+
159
+ alias :html_input :input
160
+
161
+ def input(**attrs)
162
+ render(
163
+ Input.new(
164
+ **control_attrs(
165
+ **field_attrs,
166
+ **attrs
167
+ )
168
+ )
169
+ )
170
+ end
171
+
172
+ alias :html_select :select
173
+
174
+ def select(options: nil, **attrs)
175
+ render(
176
+ Select.new(
177
+ options || @options || [],
178
+ **control_attrs(
179
+ **field_attrs,
180
+ **attrs
181
+ )
182
+ )
183
+ )
184
+ end
185
+
186
+ alias :html_textarea :textarea
187
+
188
+ def textarea(**attrs)
189
+ render(
190
+ Textarea.new(
191
+ **control_attrs(
192
+ **field_attrs,
193
+ **attrs
194
+ )
195
+ )
196
+ )
197
+ end
198
+
199
+ def checkbox(**attrs)
200
+ render(
201
+ Checkbox.new(
202
+ **control_attrs(
203
+ **field_attrs,
204
+ **attrs
205
+ )
206
+ )
207
+ )
208
+ end
209
+
210
+ def combobox(**attrs)
211
+ render(
212
+ Combobox.new(
213
+ **control_attrs(
214
+ **field_attrs,
215
+ **attrs
216
+ )
217
+ )
218
+ )
219
+ end
220
+
221
+ def radio_group(options: nil, **attrs)
222
+ render(
223
+ RadioButtonGroup.new(
224
+ options || @options || [],
225
+ **control_attrs(
226
+ **field_attrs,
227
+ **attrs
228
+ )
229
+ )
230
+ )
231
+ end
232
+
233
+ def switch(**attrs)
234
+ # TODO: support use in forms
235
+ render(
236
+ Switch.new(
237
+ **control_attrs(
238
+ **field_attrs,
239
+ **attrs
240
+ )
241
+ )
242
+ )
243
+ end
244
+
245
+ private
246
+
247
+ def base_class
248
+ [
249
+ "grid align-start",
250
+ # Margins
251
+ "[&>[data-slot=label]+[data-slot=control]]:mt-2 [&>[data-slot=label]+[data-slot=description]]:mt-1 [&>[data-slot=description]+[data-slot=control]]:mt-3 [&>[data-slot=control]+[data-slot=description]]:mt-3 [&>[data-slot=control]+[data-slot=error]]:mt-2",
252
+ # When checkbox
253
+ "[&[data-as=checkbox]]:grid-cols-[auto_1fr] data-[as=checkbox]:gap-x-2 [&[data-as=checkbox]_[data-slot=description]]:col-start-2",
254
+ "[&:has([data-slot=error])_[data-slot=control]]:border-destructive"
255
+ ]
256
+ end
257
+
258
+ def description_class
259
+ "text-sm text-muted-foreground"
260
+ end
261
+
262
+ def error_class
263
+ "text-sm text-destructive"
264
+ end
265
+
266
+ def value
267
+ return attrs[:value] if attrs[:value]
268
+ return unless object = form&.object
269
+
270
+ if object.respond_to?(@field_name)
271
+ object.public_send(@field_name)
272
+ end
273
+ end
274
+
275
+ def value_before_typecast
276
+ return attrs[:value] if attrs[:value]
277
+ return unless object = form&.object
278
+
279
+ method_before_type_cast = @field_name + "_before_type_cast"
280
+
281
+ if object.respond_to?(method_before_type_cast)
282
+ object.public_send(method_before_type_cast)
283
+ else
284
+ value
285
+ end
286
+ end
36
287
  end
37
288
  end
@@ -1,14 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NitroKit
2
4
  class FieldGroup < Component
3
- FIELD_GROUP_BASE = "space-y-6"
4
-
5
5
  def initialize(**attrs)
6
- @attrs = attrs
7
- @class_list = merge([FIELD_GROUP_BASE, attrs[:class]])
6
+ super(attrs, class: base_class, data: {slot: "control"})
8
7
  end
9
8
 
10
9
  def view_template(&block)
11
- div(**attrs, class: class_list, &block)
10
+ div(**attrs, &block)
11
+ end
12
+
13
+ private
14
+
15
+ def base_class
16
+ "space-y-6"
12
17
  end
13
18
  end
14
19
  end
@@ -1,16 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NitroKit
2
4
  class Fieldset < Component
3
- FIELDSET_BASE = "space-y-6"
4
-
5
- def initialize(legend, **attrs)
5
+ def initialize(legend: nil, description: nil, **attrs)
6
6
  @legend = legend
7
- @attrs = attrs
7
+ @description = description
8
+ super(
9
+ attrs,
10
+ class: base_class
11
+ )
12
+ end
13
+
14
+ def view_template
15
+ fieldset(**attrs) do
16
+ legend(@legend) if @legend
17
+ description(@description) if @description
18
+
19
+ yield
20
+ end
21
+ end
22
+
23
+ alias :html_legend :legend
24
+
25
+ def legend(text = nil, **attrs, &block)
26
+ html_legend(**mattr(attrs, class: legend_class)) do
27
+ text_or_block(text, &block)
28
+ end
29
+ end
30
+
31
+ def description(text = nil, **attrs, &block)
32
+ div(**mattr(attrs, class: description_class, data: {slot: "text"})) do
33
+ text_or_block(text, &block)
34
+ end
8
35
  end
9
36
 
10
- attr_reader :legend, :attrs
37
+ private
38
+
39
+ def base_class
40
+ "[&>*+[data-slot=control]]:mt-6 [&>*+[data-slot=text]]:mt-1"
41
+ end
42
+
43
+ def legend_class
44
+ "text-lg font-semibold"
45
+ end
11
46
 
12
- def view_template(&block)
13
- fieldset(**attrs, class: FIELDSET_BASE, &block)
47
+ def description_class
48
+ "text-sm text-muted-foreground"
14
49
  end
15
50
  end
16
51
  end