phlex_ui 0.0.3 → 0.0.5

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