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,23 +1,63 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NitroKit
2
4
  class Card < Component
3
5
  def initialize(**attrs)
4
- @attrs = attrs
6
+ super(
7
+ attrs,
8
+ class: base_class
9
+ )
10
+ end
11
+
12
+ def view_template
13
+ div(**attrs) do
14
+ yield
15
+ end
16
+ end
17
+
18
+ def title(text = nil, **attrs, &block)
19
+ h2(**mattr(attrs, class: "text-lg font-bold -mb-2")) do
20
+ text_or_block(text, &block)
21
+ end
22
+ end
23
+
24
+ def body(text = nil, **attrs, &block)
25
+ div(**mattr(attrs, class: "text-muted-foreground text-sm leading-relaxed")) do
26
+ text_or_block(text, &block)
27
+ end
5
28
  end
6
29
 
7
- def view_template(&block)
8
- div(class: "rounded-lg border p-6 space-y-6 shadow-sm", &block)
30
+ def footer(text = nil, **attrs, &block)
31
+ div(**mattr(attrs, class: "flex gap-2 items-center")) do
32
+ text_or_block(text, &block)
33
+ end
9
34
  end
10
35
 
11
- def title(**attrs)
12
- h2(**attrs, class: merge(["text-lg font-bold", attrs[:class]])) { yield }
36
+ def divider(**attrs)
37
+ full_width do
38
+ hr(**attrs)
39
+ end
13
40
  end
14
41
 
15
- def body(**attrs)
16
- div(**attrs) { yield }
42
+ def full_width(**attrs)
43
+ div(**mattr(attrs, data: {slot: "full"}, class: "-mx-(--gap)")) do
44
+ yield
45
+ end
17
46
  end
18
47
 
19
- def footer(**attrs)
20
- div(**attrs, class: merge(["flex gap-2", attrs[:class]])) { yield }
48
+ private
49
+
50
+ def base_class
51
+ [
52
+ # Configure spacing with breakpoints
53
+ "[--gap:calc(var(--spacing)*4)] sm:[--gap:calc(var(--spacing)*6)]",
54
+ # Base styles
55
+ "flex flex-col items-stretch rounded-lg bg-background border p-(--gap) gap-(--gap) overflow-hidden",
56
+ # If a `data-slot=full` is the first thing, move it to the top
57
+ "[&>[data-slot=full]:first-child]:-mt-(--gap)",
58
+ # Group for hover, focus
59
+ "group/card"
60
+ ]
21
61
  end
22
62
  end
23
63
  end
@@ -1,37 +1,33 @@
1
1
  module NitroKit
2
2
  class Checkbox < Component
3
- include ActionView::Helpers::FormTagHelper
3
+ def initialize(label: nil, id: nil, **attrs)
4
+ @id = id || "nk--" + SecureRandom.hex(4)
5
+ @label = label
4
6
 
5
- def initialize(name, value: "1", label: nil, **attrs)
6
- super(**attrs)
7
-
8
- @name = name
9
- @label_text = label
7
+ super(
8
+ attrs,
9
+ id: @id,
10
+ type: "checkbox",
11
+ class: input_class
12
+ )
10
13
  end
11
14
 
12
- attr_reader(
13
- :name,
14
- :value,
15
- :label_text
16
- )
15
+ alias :html_label :label
16
+
17
+ attr_reader :label, :id
17
18
 
18
19
  def view_template
19
- div(class: merge(["isolate inline-flex items-center gap-2", class_list])) do
20
- label(class: "relative flex shrink-0") do
21
- input(
22
- **attrs,
23
- type: "checkbox",
24
- class: class_names(
25
- "peer appearance-none shadow size-4 rounded border text-foreground",
26
- "checked:bg-primary checked:border-primary",
27
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
28
- )
29
- )
20
+ div(class: wrapper_class) do
21
+ html_label(
22
+ class: "inline-grid *:[grid-area:1/1] shrink-0 place-items-center group/checkbox"
23
+ ) do
24
+ input(**attrs)
30
25
  checkmark
26
+ dash
31
27
  end
32
28
 
33
- if label_text.present?
34
- render(Label.new(for: attrs[:id])) { label_text }
29
+ if label.present? || block_given?
30
+ render(Label.new(for: id)) { label || yield }
35
31
  end
36
32
  end
37
33
  end
@@ -39,24 +35,46 @@ module NitroKit
39
35
  private
40
36
 
41
37
  def checkmark
42
- span(
43
- class: class_names(
44
- "absolute w-full h-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
45
- "text-zinc-50 dark:text-zinc-950 opacity-0 peer-checked:opacity-100 pointer-events-none"
46
- )
47
- ) do
48
- svg(
49
- class: "size-full",
50
- viewbox: "0 0 20 20",
51
- fill: "currentColor",
52
- stroke: "currentColor",
53
- stroke_width: 1
54
- ) do |svg|
55
- svg.path(
56
- "d" => "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
57
- )
58
- end
38
+ svg(
39
+ class: merge_class(svg_class, "group-has-[:checked]/checkbox:visible"),
40
+ viewbox: "0 0 16 16",
41
+ fill: "none",
42
+ stroke: "currentColor",
43
+ stroke_linecap: "round",
44
+ stroke_linejoin: "round",
45
+ stroke_width: 3
46
+ ) do |svg|
47
+ svg.path(d: "M 3 8 L 6 12 L 12 5")
48
+ end
49
+ end
50
+
51
+ def dash
52
+ svg(
53
+ class: merge_class(svg_class, "group-has-[:indeterminate]/checkbox:visible"),
54
+ viewbox: "0 0 16 16",
55
+ fill: "none",
56
+ stroke: "currentColor",
57
+ stroke_linecap: "round",
58
+ stroke_width: 3
59
+ ) do |svg|
60
+ svg.line(x1: "3", y1: "8", x2: "12", y2: "8")
59
61
  end
60
62
  end
63
+
64
+ def input_class
65
+ [
66
+ "appearance-none shadow-sm size-4 rounded-sm border text-foreground",
67
+ "checked:bg-primary checked:border-primary indeterminate:bg-primary indeterminate:border-primary",
68
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ring-offset-2 ring-offset-background"
69
+ ]
70
+ end
71
+
72
+ def svg_class
73
+ "size-3 text-zinc-50 [&>svg]:size-full dark:text-zinc-950 pointer-events-none invisible"
74
+ end
75
+
76
+ def wrapper_class
77
+ "isolate inline-flex items-center gap-2"
78
+ end
61
79
  end
62
80
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NitroKit
4
+ class CheckboxGroup < Component
5
+ def initialize(options = nil, **attrs)
6
+ @options = options
7
+
8
+ super(
9
+ attrs,
10
+ class: "flex items-start flex-col gap-2"
11
+ )
12
+ end
13
+
14
+ attr_reader :options
15
+
16
+ def view_template
17
+ div(**attrs) do
18
+ if block_given?
19
+ yield
20
+ else
21
+ options.map { |option| item(*option) }
22
+ end
23
+ end
24
+ end
25
+
26
+ def title(text = nil, **attrs, &block)
27
+ render(Label.new(**attrs)) do
28
+ text_or_block(text, &block)
29
+ end
30
+ end
31
+
32
+ def item(text = nil, **attrs, &block)
33
+ render(Checkbox.new(**attrs)) do
34
+ text_or_block(text, &block)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NitroKit
4
+ class Combobox < Component
5
+ def initialize(
6
+ options: [],
7
+ id: nil,
8
+
9
+ placement: "bottom",
10
+ tab_inserts_suggestions: true,
11
+ first_option_selection_mode: "selected",
12
+ scroll_into_view_options: {block: "nearest", inline: "nearest"},
13
+
14
+ **attrs
15
+ )
16
+ # floating-ui options
17
+ @placement = placement
18
+
19
+ # combobox-nav options
20
+ @tab_inserts_suggestions = tab_inserts_suggestions
21
+ @first_option_selection_mode = first_option_selection_mode
22
+ @scroll_into_view_options = scroll_into_view_options
23
+
24
+ @id = id || "nk--combobox-" + SecureRandom.hex(4)
25
+
26
+ @options = options
27
+
28
+ super(
29
+ attrs,
30
+ type: "text",
31
+ class: input_class,
32
+ data: {
33
+ nk__combobox_target: "input",
34
+ action: %w[
35
+ focusin->nk--combobox#open
36
+ focusin@window->nk--combobox#focusShift
37
+ click@window->nk--combobox#windowClick
38
+ input->nk--combobox#input
39
+ keydown.esc->nk--combobox#clear
40
+ keydown.down->nk--combobox#open
41
+ ]
42
+ },
43
+ aria: {
44
+ controls: id(:listbox)
45
+ }
46
+ )
47
+ end
48
+
49
+ attr_reader(
50
+ :options,
51
+ :placement,
52
+ :tab_inserts_suggestions,
53
+ :first_option_selection_mode,
54
+ :scroll_into_view_options
55
+ )
56
+
57
+ def view_template
58
+ div(
59
+ data: {
60
+ class: "isolate",
61
+ slot: "control",
62
+ controller: "nk--combobox",
63
+ nk__combobox_placement_value: placement,
64
+ nk__combobox_tab_inserts_suggestions_value: tab_inserts_suggestions.to_s,
65
+ nk__combobox_first_option_selection_mode_value: first_option_selection_mode.to_s,
66
+ nk__combobox_scroll_into_view_options_value: scroll_into_view_options&.to_json
67
+ }
68
+ ) do
69
+ span(class: wrapper_class) do
70
+ render(Input.new(**attrs))
71
+ chevron_icon
72
+ end
73
+
74
+ # Since a combobox can function like a <select> element where the displayed
75
+ # value and the form value differ, include the value in a hidden field
76
+ input(
77
+ type: "hidden",
78
+ value: attrs[:value],
79
+ data: {nk__combobox_target: "hiddenField"}
80
+ )
81
+
82
+ ul(
83
+ role: "listbox",
84
+ id: id(:listbox),
85
+ class: list_class,
86
+ data: {nk__combobox_target: "list", state: "closed"}
87
+ ) do
88
+ options.each do |(key, value)|
89
+ li(
90
+ role: "option",
91
+ data: {value:},
92
+ class: merge_class(option_class)
93
+ ) { key }
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def id(suffix)
102
+ "#{@id}-#{suffix}"
103
+ end
104
+
105
+ def wrapper_class
106
+ "inline-grid *:[grid-area:1/1] group/combobox"
107
+ end
108
+
109
+ def input_class
110
+ "pr-8"
111
+ end
112
+
113
+ def list_class
114
+ [
115
+ "absolute top-0 left-0 p-1 bg-background rounded-md border shadow-sm w-fit max-w-sm flex-col flex z-10",
116
+ "max-h-60 overflow-y-auto",
117
+ "data-[state=closed]:hidden [&:not(:has([role=option]))]:hidden",
118
+ "[&_[aria-selected]]:bg-muted"
119
+ ]
120
+ end
121
+
122
+ def option_class
123
+ "hidden flex-none px-2 py-1 rounded font-medium truncate cursor-pointer hover:bg-muted [&[role=option]]:block"
124
+ end
125
+
126
+ def chevron_icon
127
+ svg(
128
+ class: "size-4 self-center place-self-end mr-2 pointer-events-none text-muted-foreground group-hover/combobox:text-foreground",
129
+ viewbox: "0 0 24 24",
130
+ fill: "none",
131
+ stroke: "currentColor",
132
+ stroke_width: 1
133
+ ) do |svg|
134
+ svg.path(d: "m6 9 6 6 6-6")
135
+ end
136
+ end
137
+ end
138
+ end
@@ -1,30 +1,59 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NitroKit
2
4
  class Component < Phlex::HTML
3
- attr_reader :attrs, :class_list
4
-
5
- def initialize(**attrs)
6
- @class_list = attrs.delete(:class)
7
- @attrs = attrs.symbolize_keys
5
+ def initialize(*hashes, **defaults)
6
+ @attrs = merge_attrs(*hashes, **defaults)
8
7
  end
9
8
 
10
- attr_reader :class_list, :attrs
9
+ attr_reader :attrs
11
10
 
12
- def merge(*args)
13
- self.class.merge(*args)
11
+ private
12
+
13
+ # Merge attributes with some special cases for matching keys
14
+ def merge_attrs(*hashes, **defaults)
15
+ defaults.merge(*hashes) do |key, old_value, new_value|
16
+ case key
17
+ when :class
18
+ # Use TailwindMerge to merge class names
19
+ merge_class(old_value, new_value)
20
+ when :data
21
+ # Merge data hashes with some special cases for Stimulus
22
+ merge_data(old_value, new_value)
23
+ else
24
+ new_value
25
+ end
26
+ end
14
27
  end
15
28
 
16
- def self.merge(*args)
17
- @merger ||= TailwindMerge::Merger.new
18
- @merger.merge(*args)
29
+ alias :mattr :merge_attrs
30
+
31
+ def merge_class(*args)
32
+ @@merger ||= TailwindMerge::Merger.new
33
+ @@merger.merge(args)
19
34
  end
20
35
 
21
- def data_merge(data = {}, new_data = {})
22
- return data if new_data.blank?
23
- return new_data if data.blank?
36
+ def merge_data(*hashes)
37
+ hashes.compact.reduce({}) do |acc, hash|
38
+ acc.deep_merge(hash) do |key, old_val, new_val|
39
+ # Concat Stimulus actions
40
+ case key
41
+ when :action, :controller
42
+ [new_val, old_val].compact.join(" ")
43
+ else
44
+ new_val
45
+ end
46
+ end
47
+ end
48
+ end
24
49
 
25
- data.deep_merge(new_data) do |_key, old_val, new_val|
26
- # Put new value first so overrides can stopPropagation to old value
27
- [new_val, old_val].compact.join(" ")
50
+ def text_or_block(text = nil, &block)
51
+ if text
52
+ plain(text)
53
+ elsif block_given?
54
+ yield
55
+ else
56
+ nil
28
57
  end
29
58
  end
30
59
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NitroKit
4
+ class Datepicker < Component
5
+ def view_template
6
+ render(Input.new(type: "text", **attrs, data: {controller: "nk--datepicker"}))
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NitroKit
4
+ class Dialog < Component
5
+ def initialize(identifier: nil, **attrs)
6
+ @identifier = identifier || SecureRandom.hex(6)
7
+
8
+ super(
9
+ attrs,
10
+ data: {controller: "nk--dialog", action: "click->nk--dialog#clickOutside"}
11
+ )
12
+ end
13
+
14
+ attr_reader :identifier
15
+
16
+ def view_template
17
+ div(**attrs) do
18
+ yield
19
+ end
20
+ end
21
+
22
+ def trigger(text = nil, **attrs, &block)
23
+ render(
24
+ NitroKit::Button.new(**mattr(attrs, data: {nk__dialog_target: "trigger", action: "click->nk--dialog#open"}))
25
+ ) do
26
+ text_or_block(text, &block)
27
+ end
28
+ end
29
+
30
+ alias :html_dialog :dialog
31
+
32
+ def dialog(**attrs)
33
+ html_dialog(
34
+ **mattr(
35
+ attrs,
36
+ class: dialog_class,
37
+ data: {nk__dialog_target: "dialog"},
38
+ aria: {
39
+ labelledby: id(:title),
40
+ describedby: id(:description)
41
+ }
42
+ )
43
+ ) do
44
+ yield
45
+ end
46
+ end
47
+
48
+ def close_button(**attrs)
49
+ render(
50
+ Button.new(
51
+ **mattr(
52
+ attrs,
53
+ variant: :ghost,
54
+ size: :sm,
55
+ class: "absolute top-2 right-2",
56
+ data: {action: "nk--dialog#close"}
57
+ )
58
+ )
59
+ ) do
60
+ render(Icon.new(:x))
61
+ end
62
+ end
63
+
64
+ def title(text = nil, **attrs, &block)
65
+ h2(**mattr(attrs, id: id(:title), class: "text-lg font-semibold mb-2")) do
66
+ text_or_block(text, &block)
67
+ end
68
+ end
69
+
70
+ def description(text = nil, **attrs, &block)
71
+ div(
72
+ **mattr(
73
+ attrs,
74
+ id: id(:description),
75
+ class: "text-muted-foreground mb-6 text-sm leading-relaxed"
76
+ )
77
+ ) do
78
+ text_or_block(text, &block)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def id(suffix = nil)
85
+ "nk-#{identifier}#{suffix ? "-#{suffix}" : ""}"
86
+ end
87
+
88
+ def dialog_class
89
+ [
90
+ "border rounded-xl max-w-lg w-full bg-background text-foreground shadow-lg m-auto p-6",
91
+ "dark:backdrop:bg-black/50"
92
+ ]
93
+ end
94
+ end
95
+ end