phlex_ui 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/lib/phlex_ui/accordion/content.rb +21 -0
  3. data/lib/phlex_ui/accordion/default_content.rb +17 -0
  4. data/lib/phlex_ui/accordion/default_trigger.rb +19 -0
  5. data/lib/phlex_ui/accordion/icon.rb +38 -0
  6. data/lib/phlex_ui/accordion/item.rb +28 -0
  7. data/lib/phlex_ui/accordion/trigger.rb +16 -0
  8. data/lib/phlex_ui/accordion.rb +28 -0
  9. data/lib/phlex_ui/alert/description.rb +17 -0
  10. data/lib/phlex_ui/alert/title.rb +17 -0
  11. data/lib/phlex_ui/alert.rb +36 -0
  12. data/lib/phlex_ui/alert_dialog/action.rb +17 -0
  13. data/lib/phlex_ui/alert_dialog/cancel.rb +21 -0
  14. data/lib/phlex_ui/alert_dialog/content.rb +45 -0
  15. data/lib/phlex_ui/alert_dialog/description.rb +17 -0
  16. data/lib/phlex_ui/alert_dialog/footer.rb +17 -0
  17. data/lib/phlex_ui/alert_dialog/header.rb +17 -0
  18. data/lib/phlex_ui/alert_dialog/title.rb +17 -0
  19. data/lib/phlex_ui/alert_dialog/trigger.rb +18 -0
  20. data/lib/phlex_ui/alert_dialog.rb +26 -0
  21. data/lib/phlex_ui/aspect_ratio.rb +33 -0
  22. data/lib/phlex_ui/attribute_merger.rb +75 -0
  23. data/lib/phlex_ui/avatar/fallback.rb +17 -0
  24. data/lib/phlex_ui/avatar/image.rb +26 -0
  25. data/lib/phlex_ui/avatar.rb +49 -0
  26. data/lib/phlex_ui/badge.rb +60 -0
  27. data/lib/phlex_ui/base.rb +24 -0
  28. data/lib/phlex_ui/button.rb +85 -16
  29. data/lib/phlex_ui/card/content.rb +17 -0
  30. data/lib/phlex_ui/card/description.rb +17 -0
  31. data/lib/phlex_ui/card/footer.rb +17 -0
  32. data/lib/phlex_ui/card/header.rb +17 -0
  33. data/lib/phlex_ui/card/title.rb +17 -0
  34. data/lib/phlex_ui/card.rb +17 -0
  35. data/lib/phlex_ui/checkbox.rb +18 -0
  36. data/lib/phlex_ui/clipboard/popover.rb +36 -0
  37. data/lib/phlex_ui/clipboard/source.rb +19 -0
  38. data/lib/phlex_ui/clipboard/trigger.rb +20 -0
  39. data/lib/phlex_ui/clipboard.rb +39 -0
  40. data/lib/phlex_ui/codeblock.rb +105 -0
  41. data/lib/phlex_ui/collapsible/content.rb +18 -0
  42. data/lib/phlex_ui/collapsible/trigger.rb +19 -0
  43. data/lib/phlex_ui/collapsible.rb +25 -0
  44. data/lib/phlex_ui/context_menu/content.rb +25 -0
  45. data/lib/phlex_ui/context_menu/item.rb +66 -0
  46. data/lib/phlex_ui/context_menu/label.rb +24 -0
  47. data/lib/phlex_ui/context_menu/separator.rb +19 -0
  48. data/lib/phlex_ui/context_menu/trigger.rb +20 -0
  49. data/lib/phlex_ui/context_menu.rb +26 -0
  50. data/lib/phlex_ui/dialog/content.rb +78 -0
  51. data/lib/phlex_ui/dialog/description.rb +17 -0
  52. data/lib/phlex_ui/dialog/footer.rb +17 -0
  53. data/lib/phlex_ui/dialog/header.rb +17 -0
  54. data/lib/phlex_ui/dialog/middle.rb +17 -0
  55. data/lib/phlex_ui/dialog/title.rb +17 -0
  56. data/lib/phlex_ui/dialog/trigger.rb +19 -0
  57. data/lib/phlex_ui/dialog.rb +25 -0
  58. data/lib/phlex_ui/dropdown_menu/content.rb +22 -0
  59. data/lib/phlex_ui/dropdown_menu/item.rb +28 -0
  60. data/lib/phlex_ui/dropdown_menu/label.rb +17 -0
  61. data/lib/phlex_ui/dropdown_menu/separator.rb +19 -0
  62. data/lib/phlex_ui/dropdown_menu/trigger.rb +17 -0
  63. data/lib/phlex_ui/dropdown_menu.rb +26 -0
  64. data/lib/phlex_ui/form/item.rb +17 -0
  65. data/lib/phlex_ui/form/spacer.rb +17 -0
  66. data/lib/phlex_ui/form.rb +34 -0
  67. data/lib/phlex_ui/hint.rb +17 -0
  68. data/lib/phlex_ui/hover_card/content.rb +22 -0
  69. data/lib/phlex_ui/hover_card/trigger.rb +19 -0
  70. data/lib/phlex_ui/hover_card.rb +27 -0
  71. data/lib/phlex_ui/input.rb +29 -0
  72. data/lib/phlex_ui/input_error.rb +17 -0
  73. data/lib/phlex_ui/label.rb +17 -0
  74. data/lib/phlex_ui/link.rb +97 -0
  75. data/lib/phlex_ui/popover/content.rb +22 -0
  76. data/lib/phlex_ui/popover/trigger.rb +19 -0
  77. data/lib/phlex_ui/popover.rb +25 -0
  78. data/lib/phlex_ui/shortcut_key.rb +17 -0
  79. data/lib/phlex_ui/table/body.rb +17 -0
  80. data/lib/phlex_ui/table/builder.rb +77 -0
  81. data/lib/phlex_ui/table/caption.rb +17 -0
  82. data/lib/phlex_ui/table/cell.rb +17 -0
  83. data/lib/phlex_ui/table/footer.rb +17 -0
  84. data/lib/phlex_ui/table/head.rb +17 -0
  85. data/lib/phlex_ui/table/header.rb +17 -0
  86. data/lib/phlex_ui/table/row.rb +17 -0
  87. data/lib/phlex_ui/table.rb +19 -0
  88. data/lib/phlex_ui/tabs/content.rb +26 -0
  89. data/lib/phlex_ui/tabs/list.rb +17 -0
  90. data/lib/phlex_ui/tabs/trigger.rb +28 -0
  91. data/lib/phlex_ui/tabs.rb +25 -0
  92. data/lib/phlex_ui/theme_toggle.rb +41 -0
  93. data/lib/phlex_ui/tooltip/content.rb +22 -0
  94. data/lib/phlex_ui/tooltip/trigger.rb +17 -0
  95. data/lib/phlex_ui/tooltip.rb +25 -0
  96. data/lib/phlex_ui/typography/blockquote.rb +17 -0
  97. data/lib/phlex_ui/typography/h1.rb +17 -0
  98. data/lib/phlex_ui/typography/h2.rb +17 -0
  99. data/lib/phlex_ui/typography/h3.rb +17 -0
  100. data/lib/phlex_ui/typography/h4.rb +17 -0
  101. data/lib/phlex_ui/typography/inline_code.rb +17 -0
  102. data/lib/phlex_ui/typography/large.rb +17 -0
  103. data/lib/phlex_ui/typography/lead.rb +17 -0
  104. data/lib/phlex_ui/typography/list.rb +47 -0
  105. data/lib/phlex_ui/typography/list_item.rb +17 -0
  106. data/lib/phlex_ui/typography/muted.rb +17 -0
  107. data/lib/phlex_ui/typography/p.rb +17 -0
  108. data/lib/phlex_ui/typography/small.rb +17 -0
  109. data/lib/phlex_ui.rb +0 -8
  110. metadata +113 -28
  111. data/tasks/install.rake +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96bee30ae4924b1a8300df9b2ca4322877078a064d4b1326f9c4c43d184461f1
4
- data.tar.gz: b3739fab746cef0e2ec75b736b4b92c4ae14daa6071ff4afc07972ecc4db5b42
3
+ metadata.gz: 997466eeaf90ee785e93f2bbb17ddf3014db28e538cfd2b5bb1177800b7eff41
4
+ data.tar.gz: a876736cf42046f4eda6e974ca8d48a2ffceb29dc49679bf2a857815a2d14c4e
5
5
  SHA512:
6
- metadata.gz: aafde25ef57c8477bcfd5cb6f8c3610b9a806bf386fd7947e03943ad665edd893c2a8946e08eb22a97904092ff19813ab66e7b079c5a60ddf0ac5d5faceb31eb
7
- data.tar.gz: a60fe88f9f889563d5ddbc8c8e2f316ecfbf74916547ba370e8bb37a4d6ba33866fc750898325fb83cedeb4b05629b35275c1856dbec4c9aaef00d036010017d
6
+ metadata.gz: ca6af73e57c9c22983f4eab367a894bbcdba53e8b16b707ec14accf80f1faa81994f45f2ea34ffb1867cc800875bce96c7d716043f418b1aa88aa1adfcc3049d
7
+ data.tar.gz: 6fb432282362714d0a1b048e476dc15d7ad38f60ef26c311eaeefe35fcca31b9b34b72224f61147b991fd832c148f1380fa556eb878abe5ecf27dd3a50c8ea46
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Accordion::Content < Base
5
+ def template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ data: {
14
+ accordion_target: "content"
15
+ },
16
+ class: 'overflow-y-hidden',
17
+ style: 'height: 0px;'
18
+ }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Accordion::DefaultContent < Base
5
+ def template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: 'pb-4 pt-0 text-sm'
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Accordion::DefaultTrigger < Base
5
+ def template(&)
6
+ div(class: "flex items-center justify-between w-full") do
7
+ p(&)
8
+ render ::PhlexUI::Accordion::Icon.new
9
+ end
10
+ end
11
+
12
+ def default_attrs
13
+ {
14
+ data: { action: "click->accordion#toggle" },
15
+ class: 'w-full flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline'
16
+ }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Accordion::Icon < Base
5
+ def template(&block)
6
+ span(**attrs) do
7
+ if block
8
+ block.call
9
+ else
10
+ icon
11
+ end
12
+ end
13
+ end
14
+
15
+ def icon
16
+ svg(
17
+ xmlns: "http://www.w3.org/2000/svg",
18
+ viewbox: "0 0 20 20",
19
+ fill: "currentColor",
20
+ class: "w-4 h-4"
21
+ ) do |s|
22
+ s.path(
23
+ fill_rule: "evenodd",
24
+ d:
25
+ "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z",
26
+ clip_rule: "evenodd"
27
+ )
28
+ end
29
+ end
30
+
31
+ def default_attrs
32
+ {
33
+ class: 'opacity-50',
34
+ data: { accordion_target: "icon" },
35
+ }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Accordion::Item < Base
5
+ def initialize(open: false, rotate_icon: 180, **attrs)
6
+ @open = open
7
+ @rotate_icon = rotate_icon
8
+ super(**attrs)
9
+ end
10
+
11
+ def template(&)
12
+ div(**attrs, &)
13
+ end
14
+
15
+ private
16
+
17
+ def default_attrs
18
+ {
19
+ data: {
20
+ controller: "accordion",
21
+ accordion_open_value: @open,
22
+ accordion_rotate_icon_value: @rotate_icon
23
+ },
24
+ class: 'border-b'
25
+ }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Accordion::Trigger < Base
5
+ def template(&)
6
+ button(**attrs, &)
7
+ end
8
+
9
+ def default_attrs
10
+ {
11
+ data: { action: "click->accordion#toggle" },
12
+ class: 'w-full flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline'
13
+ }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Accordion < Base
5
+ def template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ def add_item(title, content, open: false)
10
+ render Accordion::Item.new(open: open) do
11
+ render PhlexUI::Accordion::Trigger.new do
12
+ render PhlexUI::Accordion::DefaultTrigger.new { title }
13
+ end
14
+ render PhlexUI::Accordion::Content.new do
15
+ render PhlexUI::Accordion::DefaultContent.new { content }
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def default_attrs
23
+ {
24
+ class: 'w-full'
25
+ }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Alert::Description < Base
5
+ def template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "text-sm [&_p]:leading-relaxed",
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Alert::Title < Base
5
+ def template(&)
6
+ h5(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "mb-1 font-medium leading-none tracking-tight",
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Alert < Base
5
+ def initialize(variant: nil, **attrs)
6
+ @variant = variant
7
+ super(**attrs) # must be called after variant is set
8
+ end
9
+
10
+ def template(&)
11
+ div(**attrs, &)
12
+ end
13
+
14
+ private
15
+
16
+ def colors
17
+ case @variant
18
+ when nil
19
+ 'ring-border bg-muted-background text-text [&>svg]:opacity-80'
20
+ when :warning
21
+ 'ring-warning/20 bg-warning/10 text-warning [&>svg]:text-warning/80'
22
+ when :success
23
+ 'ring-success/20 bg-success/10 text-success [&>svg]:text-success/80'
24
+ when :destructive
25
+ 'ring-destructive/10 dark:ring-destructive/20 text-destructive bg-destructive/10 [&>svg]:text-destructive/80'
26
+ end
27
+ end
28
+
29
+ def default_attrs
30
+ base_classes = 'relative w-full ring-1 ring-inset rounded-lg px-4 py-4 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-8'
31
+ {
32
+ class: tokens(base_classes, colors),
33
+ }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AlertDialog::Action < Base
5
+ def template(&)
6
+ render PhlexUI::Button.new(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ variant: :primary,
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AlertDialog::Cancel < Base
5
+ def template(&)
6
+ render PhlexUI::Button.new(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ variant: :outline,
14
+ data: {
15
+ action: 'click->dismissable#dismiss'
16
+ },
17
+ class: 'mt-2 sm:mt-0'
18
+ }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AlertDialog::Content < Base
5
+ def template(&)
6
+ template_tag(**attrs) do
7
+ div(data: { controller: 'dismissable' }) do
8
+ background
9
+ container(&)
10
+ end
11
+ end
12
+ end
13
+
14
+ def background
15
+ div(
16
+ data_state: "open",
17
+ class:
18
+ "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
19
+ style: "pointer-events:auto",
20
+ data_aria_hidden: "true",
21
+ aria_hidden: "true"
22
+ )
23
+ end
24
+
25
+ def container(&)
26
+ div(
27
+ role: "alertdialog",
28
+ data_state: "open",
29
+ class: "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
30
+ style: "pointer-events:auto",
31
+ &
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def default_attrs
38
+ {
39
+ data: {
40
+ alert_dialog_target: "content"
41
+ }
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AlertDialog::Description < Base
5
+ def template(&)
6
+ p(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "text-sm text-muted-text"
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AlertDialog::Footer < Base
5
+ def template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2"
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AlertDialog::Header < Base
5
+ def template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "flex flex-col space-y-2 text-center sm:text-left"
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AlertDialog::Title < Base
5
+ def template(&)
6
+ h2(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "text-lg font-semibold"
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AlertDialog::Trigger < Base
5
+ def template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ data: { action: "click->alert-dialog#open" },
14
+ class: 'inline-block'
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AlertDialog < Base
5
+ def initialize(open: false, **attrs)
6
+ @open = open
7
+ super(**attrs)
8
+ end
9
+
10
+ def template(&)
11
+ div(**attrs, &)
12
+ end
13
+
14
+ private
15
+
16
+ def default_attrs
17
+ {
18
+ data: {
19
+ controller: 'alert-dialog',
20
+ alert_dialog_open_value: @open
21
+ },
22
+ class: 'inline-block'
23
+ }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class AspectRatio < Base
5
+ def initialize(aspect_ratio: "16/9", **attrs)
6
+ raise "aspect_ratio must be in the format of a string with a slash in the middle (eg. '16/9', '1/1')" unless aspect_ratio.is_a?(String) && aspect_ratio.include?("/")
7
+
8
+ @aspect_ratio = aspect_ratio
9
+ super(**attrs)
10
+ end
11
+
12
+ def template(&)
13
+ div(
14
+ class: "relative w-full",
15
+ style: "padding-bottom: #{padding_bottom}%;",
16
+ ) do
17
+ div(**attrs, &)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def padding_bottom
24
+ @aspect_ratio.split("/").map(&:to_i).reverse.reduce(&:fdiv) * 100
25
+ end
26
+
27
+ def default_attrs
28
+ {
29
+ class: "bg-muted-background absolute inset-0 [&>img]:object-cover [&>img]:absolute [&>img]:h-full [&>img]:w-full [&>img]:inset-0 [&>img]:text-transparent"
30
+ }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,75 @@
1
+ module PhlexUI
2
+ class AttributeMerger
3
+ attr_reader :default_attrs, :user_attrs
4
+ OVERRIDE_KEY = '!'.freeze
5
+
6
+ def initialize(default_attrs, user_attrs)
7
+ @default_attrs = flatten_hash(default_attrs)
8
+ @user_attrs = flatten_hash(user_attrs)
9
+ end
10
+
11
+ def call
12
+ merged_attrs = merge_hashes(default_attrs, non_override_attrs)
13
+ mix(merged_attrs, override_attrs)
14
+ end
15
+
16
+ private
17
+
18
+ # @return [Hash]
19
+ def mix(*args)
20
+ args.each_with_object({}) do |object, result|
21
+ result.merge!(object) do |_key, old, new|
22
+ case new
23
+ when Hash
24
+ old.is_a?(Hash) ? mix(old, new) : new
25
+ when Array
26
+ old.is_a?(Array) ? (old + new) : new
27
+ when String
28
+ old.is_a?(String) ? "#{old} #{new}" : new
29
+ else
30
+ new
31
+ end
32
+ end
33
+
34
+ result.transform_keys! do |key|
35
+ key.end_with?("!") ? key.name.chop.to_sym : key
36
+ end
37
+ end
38
+ end
39
+
40
+ def override_attrs
41
+ user_attrs.select do |key, value|
42
+ key.to_s.include?(OVERRIDE_KEY)
43
+ end
44
+ end
45
+
46
+ def non_override_attrs
47
+ user_attrs.reject do |key, value|
48
+ key.to_s.include?(OVERRIDE_KEY)
49
+ end
50
+ end
51
+
52
+ def flatten_hash(hash, parent_key = '', result_hash = {})
53
+ hash.each do |key, value|
54
+ new_key = parent_key.empty? ? key : "#{parent_key}_#{key}".to_sym
55
+ if value.is_a? Hash
56
+ flatten_hash(value, new_key, result_hash)
57
+ else
58
+ result_hash[new_key] = value
59
+ end
60
+ end
61
+ result_hash
62
+ end
63
+
64
+ def merge_hashes(hash1, hash2)
65
+ flat_hash1 = flatten_hash(hash1)
66
+ flat_hash2 = flatten_hash(hash2)
67
+
68
+ merged = flat_hash1.merge(flat_hash2) do |key, oldval, newval|
69
+ "#{oldval} #{newval}"
70
+ end
71
+
72
+ merged
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Avatar::Fallback < Base
5
+ def template(&)
6
+ span(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "flex h-full w-full items-center justify-center rounded-full bg-muted-background"
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Avatar::Image < Base
5
+ def initialize(src:, alt: '', **attrs)
6
+ @src = src
7
+ @alt = alt
8
+ super(**attrs)
9
+ end
10
+
11
+ def template
12
+ img(**attrs)
13
+ end
14
+
15
+ private
16
+
17
+ def default_attrs
18
+ {
19
+ loading: "lazy",
20
+ class: "aspect-square h-full w-full",
21
+ alt: @alt,
22
+ src: @src
23
+ }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhlexUI
4
+ class Avatar < Base
5
+ SIZES = {
6
+ xs: "h-4 w-4 text-[0.5rem]",
7
+ sm: "h-6 w-6 text-xs",
8
+ md: "h-10 w-10 text-base",
9
+ lg: "h-14 w-14 text-xl",
10
+ xl: "h-20 w-20 text-3xl"
11
+ }
12
+
13
+ def initialize(size: :md, src: nil, alt: nil, initials: nil, **attrs)
14
+ @size = size
15
+ @src = src
16
+ @alt = alt
17
+ @initials = initials
18
+ @size_classes = SIZES[@size]
19
+ super(**attrs)
20
+ end
21
+
22
+ def template(&block)
23
+ if block_given?
24
+ span(**attrs, &block)
25
+ else
26
+ span(**attrs) do
27
+ render_image if @src
28
+ render_initials
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def render_image
36
+ render ::PhlexUI::Avatar::Image.new(src: @src, alt: @alt)
37
+ end
38
+
39
+ def render_initials
40
+ render ::PhlexUI::Avatar::Fallback.new { @initials }
41
+ end
42
+
43
+ def default_attrs
44
+ {
45
+ class: tokens("relative flex shrink-0 overflow-hidden rounded-full", @size_classes)
46
+ }
47
+ end
48
+ end
49
+ end