lycan_ui 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +33 -0
- data/Rakefile +5 -0
- data/lib/generators/lycan_ui/add_generator.rb +109 -0
- data/lib/generators/lycan_ui/setup_generator.rb +76 -0
- data/lib/generators/lycan_ui/templates/components/accordion.rb +63 -0
- data/lib/generators/lycan_ui/templates/components/alert.rb +35 -0
- data/lib/generators/lycan_ui/templates/components/avatar.rb +38 -0
- data/lib/generators/lycan_ui/templates/components/badge.rb +29 -0
- data/lib/generators/lycan_ui/templates/components/button.rb +49 -0
- data/lib/generators/lycan_ui/templates/components/checkbox.rb +31 -0
- data/lib/generators/lycan_ui/templates/components/collapsible.rb +40 -0
- data/lib/generators/lycan_ui/templates/components/component.rb +72 -0
- data/lib/generators/lycan_ui/templates/components/dialog.rb +129 -0
- data/lib/generators/lycan_ui/templates/components/dropdown.rb +242 -0
- data/lib/generators/lycan_ui/templates/components/input.rb +26 -0
- data/lib/generators/lycan_ui/templates/components/label.rb +30 -0
- data/lib/generators/lycan_ui/templates/components/popover.rb +53 -0
- data/lib/generators/lycan_ui/templates/components/radio.rb +27 -0
- data/lib/generators/lycan_ui/templates/components/select.rb +38 -0
- data/lib/generators/lycan_ui/templates/components/switch.rb +26 -0
- data/lib/generators/lycan_ui/templates/components/textarea.rb +25 -0
- data/lib/generators/lycan_ui/templates/extras/form_builder.rb +90 -0
- data/lib/generators/lycan_ui/templates/javascript/accordion_controller.js +46 -0
- data/lib/generators/lycan_ui/templates/javascript/avatar_controller.js +34 -0
- data/lib/generators/lycan_ui/templates/javascript/collapsible_controller.js +23 -0
- data/lib/generators/lycan_ui/templates/javascript/dialog_controller.js +90 -0
- data/lib/generators/lycan_ui/templates/javascript/dropdown_controller.js +395 -0
- data/lib/generators/lycan_ui/templates/javascript/popover_controller.js +114 -0
- data/lib/generators/lycan_ui/templates/setup/application.tailwind.css +94 -0
- data/lib/generators/lycan_ui/templates/setup/lycan_ui_helper.rb +39 -0
- data/lib/lycan_ui/configuration.rb +32 -0
- data/lib/lycan_ui/railtie.rb +6 -0
- data/lib/lycan_ui/version.rb +3 -0
- data/lib/lycan_ui.rb +8 -0
- data/lib/tasks/lycan_ui_tasks.rake +6 -0
- metadata +107 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fa1792973d0fd046ee21833d5608a50aede8f258be1673ab775b8c19f76212d6
|
|
4
|
+
data.tar.gz: 6fb5d6c3b5427198226653455564c74ef2f3f292680d1f85943d13ab34b93d2c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2afc70394bd910943a561bde1f909080748c57d7fca4fe9b83a9851aedebcab1dd319ef16469109e83b6f5c5185d72148761b0818cd7b38b31c482faeca8bb62
|
|
7
|
+
data.tar.gz: cede02b1b57b0f5055b878dd0e66c660a87b969f5810ada54db4ea283313bbbd1ac3a7c31e33d2f4020509b30665da1b5e65d9bca62e91005b012b071d6e0741
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Ethan Kircher
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# LycanUi
|
|
2
|
+
|
|
3
|
+
Accessible, open source, and customizable ui components for your Rails app.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to the `development` group in your `Gemfile`,
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
group :development do
|
|
11
|
+
gem 'lycan_ui', github: 'MSILycanthropy/lycan_ui', require: false
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run `bundle install` and then run the setup script with,
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
rails generate lycan_ui:setup
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Follow the prompts in your terminal and then.. you're done!
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
Visit http://ui.lycanthropy.dev to view documentation.
|
|
26
|
+
|
|
27
|
+
## Contributing
|
|
28
|
+
|
|
29
|
+
Please read the [contributing guide](/CONTRIBUTING.md).
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lycan_ui/configuration"
|
|
4
|
+
|
|
5
|
+
module LycanUi
|
|
6
|
+
module Generators
|
|
7
|
+
class AddGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
REQUIRES_FLOATING = [ "dropdown", "popover" ].freeze
|
|
11
|
+
REQUIRES_FOCUS_TRAP = [ "popover" ].freeze
|
|
12
|
+
|
|
13
|
+
alias_method :component, :file_name
|
|
14
|
+
|
|
15
|
+
def load_configuration
|
|
16
|
+
Configuration.setup
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def install_form
|
|
20
|
+
return unless component == "form"
|
|
21
|
+
|
|
22
|
+
copy_file("extras/form_builder.rb", "app/helpers/lycan_ui/form_helper.rb")
|
|
23
|
+
|
|
24
|
+
[ "label", "input", "textarea", "checkbox", "button", "radio", "switch" ].each do |comp|
|
|
25
|
+
puts "Installing #{comp.titleize}..."
|
|
26
|
+
%x(rails g lycan_ui:add #{comp} --force)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
exit
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def copy_views
|
|
33
|
+
copy_file("components/#{component}.rb", "app/components/lycan_ui/#{component}.rb")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def copy_js
|
|
37
|
+
if file_exists?("javascript/#{component}_controller.js")
|
|
38
|
+
copy_file(
|
|
39
|
+
"javascript/#{component}_controller.js",
|
|
40
|
+
"#{Configuration.javascript_dir}/#{component}_controller.js",
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if dir_exists?("javascript/#{component}")
|
|
45
|
+
directory("javascript/#{component}", "#{Configuration.javascript_dir}/#{component}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless importmaps?
|
|
49
|
+
content = <<~JS
|
|
50
|
+
|
|
51
|
+
import #{component.titleize}Controller from "./#{component}_controller"
|
|
52
|
+
application.register("#{component}", #{component.titleize}Controller)
|
|
53
|
+
JS
|
|
54
|
+
append_to_file("#{Configuration.javascript_dir}/index.js", content)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def install_focus_trap
|
|
59
|
+
return unless focus_trapping?
|
|
60
|
+
|
|
61
|
+
js_command = if importmaps?
|
|
62
|
+
"bin/importmap pin focus-trap"
|
|
63
|
+
else
|
|
64
|
+
"yarn add focus-trap"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
run(js_command)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def install_floating
|
|
71
|
+
return unless floating?
|
|
72
|
+
|
|
73
|
+
js_command = if importmaps?
|
|
74
|
+
"bin/importmap pin @floating-ui/dom"
|
|
75
|
+
else
|
|
76
|
+
"yarn add @floating-ui/dom"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
run(js_command)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def focus_trapping?
|
|
85
|
+
REQUIRES_FOCUS_TRAP.include?(component)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def floating?
|
|
89
|
+
REQUIRES_FLOATING.include?(component)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def dir_exists?(name)
|
|
93
|
+
root = source_paths.first
|
|
94
|
+
|
|
95
|
+
Dir.exist?("#{root}/#{name}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def file_exists?(name)
|
|
99
|
+
root = source_paths.first
|
|
100
|
+
|
|
101
|
+
File.exist?("#{root}/#{name}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def importmaps?
|
|
105
|
+
File.exist?("#{Rails.root}/config/importmap.rb")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-prompt"
|
|
4
|
+
require "lycan_ui/configuration"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
module LycanUi
|
|
8
|
+
module Generators
|
|
9
|
+
class SetupGenerator < Rails::Generators::Base
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
|
|
12
|
+
def load_configuration
|
|
13
|
+
Configuration.setup
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def install_lucide_rails
|
|
17
|
+
return if lucide_rails_installed?
|
|
18
|
+
|
|
19
|
+
run("bundle add lucide-rails")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def install_tailwind_merge
|
|
23
|
+
return unless tailwind?
|
|
24
|
+
return if tailwind_merge_installed?
|
|
25
|
+
|
|
26
|
+
run("bundle add tailwind_merge")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def install_tailwind_config
|
|
30
|
+
return unless tailwind?
|
|
31
|
+
|
|
32
|
+
template("setup/application.tailwind.css", Configuration.stylesheet, force: true)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def copy_helpers
|
|
36
|
+
copy_file("setup/lycan_ui_helper.rb", "app/helpers/lycan_ui_helper.rb")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# TODO: add `all`
|
|
40
|
+
def install_components
|
|
41
|
+
copy_file("components/component.rb", "app/components/lycan_ui/component.rb")
|
|
42
|
+
|
|
43
|
+
prompt = TTY::Prompt.new
|
|
44
|
+
path = source_paths.first + "/components"
|
|
45
|
+
choices = Dir.glob("#{path}/*.rb").map do |c|
|
|
46
|
+
c.sub(path, "").sub(".rb", "").gsub("_", " ").slice(1..).strip
|
|
47
|
+
end.reject { |c| c == "component" }.push("form").sort.index_by { |comp| comp.titleize }
|
|
48
|
+
|
|
49
|
+
selected = prompt.multi_select("Select your options:", choices, filter: true)
|
|
50
|
+
|
|
51
|
+
selected.each do |component|
|
|
52
|
+
puts "Installing #{component.titleize}..."
|
|
53
|
+
%x(rails g lycan_ui:add #{component} --force)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def lucide_rails_installed?
|
|
60
|
+
Gem.loaded_specs.key?("lucide-rails")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def tailwind_merge_installed?
|
|
64
|
+
Gem.loaded_specs.key?("tailwind_merge")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def tailwind?
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def importmaps?
|
|
72
|
+
File.exist?("#{Rails.root}/config/importmap.rb")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LycanUi
|
|
4
|
+
class Accordion < Component
|
|
5
|
+
attr_reader :name, :multiple
|
|
6
|
+
|
|
7
|
+
def initialize(multiple: false, **attributes)
|
|
8
|
+
@multiple = multiple
|
|
9
|
+
|
|
10
|
+
action = <<~ACTION.squish
|
|
11
|
+
keydown.up->accordion#focusPrevious:prevent keydown.down->accordion#focusNext:prevent
|
|
12
|
+
keydown.home->accordion#focusFirst:prevent keydown.end->accordion#focusLast:prevent
|
|
13
|
+
ACTION
|
|
14
|
+
|
|
15
|
+
super(attributes, data: { controller: "accordion", action: })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def template(&)
|
|
19
|
+
@name = lycan_ui_id unless multiple
|
|
20
|
+
|
|
21
|
+
tag.div(**attributes) { yield self }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
ITEM_CLASSES = <<~CLASSES.squish
|
|
25
|
+
border-b border-surface interpolate-keywords details:h-0 details-open:h-auto
|
|
26
|
+
details:overflow-hidden details:text-sm details:motion-safe:transition-all
|
|
27
|
+
details:duration-200 details:ease-out details:transition-discrete
|
|
28
|
+
disabled:opacity-50 disabled:pointer-events-none
|
|
29
|
+
open:[&>summary>svg]:rotate-180
|
|
30
|
+
CLASSES
|
|
31
|
+
|
|
32
|
+
def item(open: false, **item_attributes, &)
|
|
33
|
+
final_attributes = merge_attributes(item_attributes, open:, class: ITEM_CLASSES, name:)
|
|
34
|
+
|
|
35
|
+
tag.details(**final_attributes, &)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
TRIGGER_CLASSES = <<~CLASSES.squish
|
|
39
|
+
cursor-pointer flex flex-1 items-center justify-between py-4 font-medium
|
|
40
|
+
transition-all underline-offset-4 hover:underline decoration-accent
|
|
41
|
+
CLASSES
|
|
42
|
+
def trigger(content = nil, icon: "chevron-down", **trigger_attributes, &block)
|
|
43
|
+
final_attributes = merge_attributes(
|
|
44
|
+
trigger_attributes,
|
|
45
|
+
class: TRIGGER_CLASSES,
|
|
46
|
+
data: { accordion_target: "item" },
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
tag.summary(**final_attributes) do
|
|
50
|
+
safe_join([
|
|
51
|
+
determine_content(content, &block),
|
|
52
|
+
icon.present? ? lucide_icon(icon, class: "size-4 shrink-0 transition-transform duration-200") : nil,
|
|
53
|
+
].compact)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def content(**content_attributes, &)
|
|
58
|
+
final_attributes = merge_attributes(content_attributes, class: "pb-4")
|
|
59
|
+
|
|
60
|
+
tag.div(**final_attributes, &)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LycanUi
|
|
4
|
+
class Alert < Component
|
|
5
|
+
DEFAULT_CLASSES = <<~CLASSES.squish
|
|
6
|
+
relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px]
|
|
7
|
+
[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-3.5
|
|
8
|
+
CLASSES
|
|
9
|
+
|
|
10
|
+
VARIANT_CLASSES = {
|
|
11
|
+
primary: "border-on-surface",
|
|
12
|
+
danger: "border-danger bg-danger-500 text-on-danger [&>svg]:text-on-danger",
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def initialize(variant: :primary, **attributes)
|
|
16
|
+
super(attributes, role: "alert", class: [ DEFAULT_CLASSES, VARIANT_CLASSES[variant] ])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def template(&)
|
|
20
|
+
tag.div(**attributes) { yield self }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def title(content = nil, **title_attributes, &block)
|
|
24
|
+
final_attributes = merge_attributes(title_attributes, class: "mb-1 font-medium leading-none tracking-tight")
|
|
25
|
+
|
|
26
|
+
tag.div(**final_attributes) { determine_content(content, &block) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def description(content = nil, **description_attributes, &block)
|
|
30
|
+
final_attributes = merge_attributes(description_attributes, class: "text-sm [&_p]:leading-relaxed")
|
|
31
|
+
|
|
32
|
+
tag.div(**final_attributes) { determine_content(content, &block) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LycanUi
|
|
4
|
+
class Avatar < Component
|
|
5
|
+
attr_reader :args, :fallback
|
|
6
|
+
|
|
7
|
+
FALLBACK_CLASSES = <<~CLASSES.squish
|
|
8
|
+
relative flex items-center justify-center h-10 w-10 shrink-0 overflow-hidden
|
|
9
|
+
rounded-full bg-secondary-400 shadow-lg
|
|
10
|
+
CLASSES
|
|
11
|
+
|
|
12
|
+
def initialize(*args, fallback: nil, **attributes)
|
|
13
|
+
@args = args
|
|
14
|
+
@fallback = fallback
|
|
15
|
+
|
|
16
|
+
if fallback.present?
|
|
17
|
+
super(attributes, class: FALLBACK_CLASSES, data: { controller: "avatar" })
|
|
18
|
+
else
|
|
19
|
+
super(attributes, class: "rounded-full h-10 w-10 object-cover")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def template
|
|
24
|
+
if fallback.present?
|
|
25
|
+
tag.div(**attributes) do
|
|
26
|
+
concat(tag.div(fallback, class: "text-on-secondary font-medium", data: { avatar_target: "fallback" }))
|
|
27
|
+
concat(image_tag(
|
|
28
|
+
*args,
|
|
29
|
+
class: "absolute w-full h-full object-cover shadow-lg",
|
|
30
|
+
data: { action: "error->avatar#showFallback load->avatar#hideFallback", avatar_target: "image" },
|
|
31
|
+
))
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
image_tag(*args, **attributes)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LycanUi
|
|
4
|
+
class Badge < Component
|
|
5
|
+
attr_reader :content
|
|
6
|
+
|
|
7
|
+
DEFAULT_CLASSES = <<~CLASSES.squish
|
|
8
|
+
inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors
|
|
9
|
+
focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2
|
|
10
|
+
CLASSES
|
|
11
|
+
|
|
12
|
+
VARIANT_CLASSES = {
|
|
13
|
+
primary: "border-transparent bg-primary text-on-primary hover:bg-primary/80",
|
|
14
|
+
secondary: "border-transparent bg-secondary text-on-secondary hover:bg-secondary/80",
|
|
15
|
+
outline: "border-primary",
|
|
16
|
+
danger: "border-transparent bg-danger text-on-danger hover:bg-danger/80",
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(content = nil, variant: :primary, **attributes)
|
|
20
|
+
@content = content
|
|
21
|
+
|
|
22
|
+
super(attributes, class: [ DEFAULT_CLASSES, VARIANT_CLASSES[variant] ])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def template(&block)
|
|
26
|
+
tag.div(**attributes) { determine_content(content, &block) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LycanUi
|
|
4
|
+
class Button < Component
|
|
5
|
+
attr_accessor :content, :as_child
|
|
6
|
+
|
|
7
|
+
DEFAULT_CLASSES = <<~CLASSES.squish
|
|
8
|
+
cursor-pointer inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium
|
|
9
|
+
transition-colors disabled:pointer-events-none disabled:opacity-50 ring-offset-background
|
|
10
|
+
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
|
|
11
|
+
CLASSES
|
|
12
|
+
|
|
13
|
+
SIZE_CLASSES = {
|
|
14
|
+
extra_small: "h-6 px-1 text-xs",
|
|
15
|
+
small: "h-7 px-2 text-sm",
|
|
16
|
+
medium: "h-10 px-4 text-base",
|
|
17
|
+
large: "h-11 px-5 text-lg",
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
VARIANT_CLASSES = {
|
|
21
|
+
primary:
|
|
22
|
+
"bg-primary text-on-primary hover:bg-primary/80 focus-visible:ring-primary",
|
|
23
|
+
secondary:
|
|
24
|
+
"bg-secondary text-on-secondary hover:bg-secondary/80 focus-visible:ring-secondary",
|
|
25
|
+
danger:
|
|
26
|
+
"bg-danger text-on-danger hover:bg-danger/80 focus-visible:ring-danger",
|
|
27
|
+
outline:
|
|
28
|
+
"border border-primary hover:bg-primary hover:text-on-primary focus-visible:ring-primary",
|
|
29
|
+
ghost:
|
|
30
|
+
"hover:bg-secondary/30 hover:text-on-secondary focus-visible:ring-secondary/30",
|
|
31
|
+
link:
|
|
32
|
+
"underline-offset-4 decoration-accent hover:underline focus-visible:ring-accent",
|
|
33
|
+
none: nil,
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def initialize(content = nil, size: :medium, variant: :primary, as_child: false, **attributes)
|
|
37
|
+
@content = content
|
|
38
|
+
@as_child = as_child
|
|
39
|
+
|
|
40
|
+
super(attributes, type: :button, class: [ DEFAULT_CLASSES, VARIANT_CLASSES[variant], SIZE_CLASSES[size] ])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def template(&block)
|
|
44
|
+
return yield attributes.except(:type) if as_child
|
|
45
|
+
|
|
46
|
+
tag.button(**attributes) { determine_content(content, &block) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LycanUi
|
|
4
|
+
class Checkbox < Component
|
|
5
|
+
attr_reader :object_name, :method, :checked_value, :unchecked_value
|
|
6
|
+
|
|
7
|
+
DEFAULT_CLASSES = <<~CLASSES.squish
|
|
8
|
+
appearance-none size-4 border border-accent rounded motion-safe:transition-all
|
|
9
|
+
ease-[ease] relative cursor-pointer
|
|
10
|
+
after:top-[45%] after:left-1/2 after:w-1.5 after:h-2.5 after:opacity-0
|
|
11
|
+
checked:bg-accent checked:border-transparent checked:scale-110 after:absolute
|
|
12
|
+
after:border-r-2 after:border-b-2 after:border-surface-50 dark:after:border-surface-950
|
|
13
|
+
after:-translate-x-1/2 after:-translate-y-1/2 after:rotate-45 after:scale-0
|
|
14
|
+
after:transition-transform checked:after:scale-100 checked:after:opacity-100
|
|
15
|
+
disabled:opacity-65 disabled:cursor-not-allowed
|
|
16
|
+
CLASSES
|
|
17
|
+
|
|
18
|
+
def initialize(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
|
|
19
|
+
@object_name = object_name
|
|
20
|
+
@method = method
|
|
21
|
+
@checked_value = checked_value
|
|
22
|
+
@unchecked_value = unchecked_value
|
|
23
|
+
|
|
24
|
+
super(options, class: DEFAULT_CLASSES)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def template
|
|
28
|
+
checkbox(object_name, method, attributes, checked_value, unchecked_value)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LycanUi
|
|
4
|
+
class Collapsible < Component
|
|
5
|
+
def initialize(**attributes)
|
|
6
|
+
super(attributes, data: { controller: "collapsible" })
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def template(&)
|
|
10
|
+
@id = lycan_ui_id
|
|
11
|
+
|
|
12
|
+
tag.div(**attributes) { yield self }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def trigger(content = nil, **trigger_attributes, &)
|
|
16
|
+
final_attributes = merge_attributes(
|
|
17
|
+
trigger_attributes,
|
|
18
|
+
aria: { controls: @id, expanded: false },
|
|
19
|
+
data: { collapsible_target: "trigger", action: "collapsible#toggle" },
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
ui.button(content, **final_attributes, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
CONTENT_CLASSES = <<~CLASSES
|
|
26
|
+
overflow-hidden interpolate-keywords transition-discrete starting:h-0 hidden:h-0 h-auto motion-safe:transition-all duration-300
|
|
27
|
+
CLASSES
|
|
28
|
+
def content(**content_attributes, &)
|
|
29
|
+
final_attributes = merge_attributes(
|
|
30
|
+
content_attributes,
|
|
31
|
+
id: @id,
|
|
32
|
+
class: CONTENT_CLASSES,
|
|
33
|
+
hidden: true,
|
|
34
|
+
data: { collapsible_target: "content" },
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
tag.div(**final_attributes, &)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LycanUi
|
|
4
|
+
class Component
|
|
5
|
+
attr_accessor :attributes
|
|
6
|
+
delegate_missing_to :@view_context
|
|
7
|
+
|
|
8
|
+
def initialize(attributes = {}, **default_attributes)
|
|
9
|
+
@attributes = merge_attributes(attributes, **default_attributes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render_in(view_context, &)
|
|
13
|
+
@view_context = view_context
|
|
14
|
+
|
|
15
|
+
template(&)
|
|
16
|
+
ensure
|
|
17
|
+
@view_context = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def template
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def merge_attributes(*incoming, **default)
|
|
27
|
+
default.merge(*incoming) do |key, default_value, incoming_value|
|
|
28
|
+
case key
|
|
29
|
+
when :class
|
|
30
|
+
merge_classes(default_value, incoming_value)
|
|
31
|
+
when :data
|
|
32
|
+
merge_data(default_value, incoming_value)
|
|
33
|
+
when :aria
|
|
34
|
+
merge_aria(default_value, incoming_value)
|
|
35
|
+
else
|
|
36
|
+
incoming_value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def merge_classes(*classes)
|
|
42
|
+
@@merger ||= TailwindMerge::Merger.new
|
|
43
|
+
@@merger.merge(classes)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def merge_data(*hashes)
|
|
47
|
+
hashes.compact.each_with_object({}) do |hash, result|
|
|
48
|
+
result.deep_merge(hash) do |key, old_value, new_value|
|
|
49
|
+
case key
|
|
50
|
+
when :action, :controller
|
|
51
|
+
[ old_value, new_value ].compact.join(" ")
|
|
52
|
+
else
|
|
53
|
+
new_value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def merge_aria(*hashes)
|
|
60
|
+
hashes.compact.each_with_object({}) do |hash, result|
|
|
61
|
+
result.deep_merge(hash)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def determine_content(text = nil, &)
|
|
66
|
+
return text if text.present?
|
|
67
|
+
return unless block_given?
|
|
68
|
+
|
|
69
|
+
@view_context.capture(&)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|