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,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