nitro_kit 0.1.0 → 0.3.0

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