shadcn-phlex 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.
Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +195 -0
  3. data/app.css +20 -0
  4. data/css/shadcn-source.css +3 -0
  5. data/css/shadcn-tailwind.css +160 -0
  6. data/css/themes/mauve.css +62 -0
  7. data/css/themes/mist.css +62 -0
  8. data/css/themes/neutral.css +74 -0
  9. data/css/themes/olive.css +62 -0
  10. data/css/themes/stone.css +62 -0
  11. data/css/themes/taupe.css +62 -0
  12. data/css/themes/zinc.css +62 -0
  13. data/js/controllers/accordion_controller.js +135 -0
  14. data/js/controllers/checkbox_controller.js +52 -0
  15. data/js/controllers/collapsible_controller.js +85 -0
  16. data/js/controllers/combobox_controller.js +168 -0
  17. data/js/controllers/command_controller.js +171 -0
  18. data/js/controllers/context_menu_controller.js +132 -0
  19. data/js/controllers/dark_mode_controller.js +106 -0
  20. data/js/controllers/dialog_controller.js +205 -0
  21. data/js/controllers/drawer_controller.js +161 -0
  22. data/js/controllers/dropdown_menu_controller.js +189 -0
  23. data/js/controllers/hover_card_controller.js +85 -0
  24. data/js/controllers/index.js +89 -0
  25. data/js/controllers/menubar_controller.js +171 -0
  26. data/js/controllers/navigation_menu_controller.js +160 -0
  27. data/js/controllers/popover_controller.js +151 -0
  28. data/js/controllers/radio_group_controller.js +78 -0
  29. data/js/controllers/scroll_area_controller.js +117 -0
  30. data/js/controllers/select_controller.js +198 -0
  31. data/js/controllers/sheet_controller.js +130 -0
  32. data/js/controllers/slider_controller.js +142 -0
  33. data/js/controllers/switch_controller.js +40 -0
  34. data/js/controllers/tabs_controller.js +96 -0
  35. data/js/controllers/toast_controller.js +206 -0
  36. data/js/controllers/toggle_controller.js +30 -0
  37. data/js/controllers/toggle_group_controller.js +73 -0
  38. data/js/controllers/tooltip_controller.js +146 -0
  39. data/lib/generators/shadcn_phlex/component_generator.rb +79 -0
  40. data/lib/generators/shadcn_phlex/install_generator.rb +217 -0
  41. data/lib/shadcn/base.rb +27 -0
  42. data/lib/shadcn/engine.rb +24 -0
  43. data/lib/shadcn/kit.rb +1158 -0
  44. data/lib/shadcn/themes/accent_colors.rb +106 -0
  45. data/lib/shadcn/themes/base_colors.rb +313 -0
  46. data/lib/shadcn/ui/accordion.rb +135 -0
  47. data/lib/shadcn/ui/alert.rb +79 -0
  48. data/lib/shadcn/ui/alert_dialog.rb +220 -0
  49. data/lib/shadcn/ui/aspect_ratio.rb +35 -0
  50. data/lib/shadcn/ui/avatar.rb +134 -0
  51. data/lib/shadcn/ui/badge.rb +48 -0
  52. data/lib/shadcn/ui/breadcrumb.rb +180 -0
  53. data/lib/shadcn/ui/button.rb +63 -0
  54. data/lib/shadcn/ui/button_group.rb +58 -0
  55. data/lib/shadcn/ui/card.rb +133 -0
  56. data/lib/shadcn/ui/checkbox.rb +72 -0
  57. data/lib/shadcn/ui/collapsible.rb +76 -0
  58. data/lib/shadcn/ui/combobox.rb +229 -0
  59. data/lib/shadcn/ui/command.rb +256 -0
  60. data/lib/shadcn/ui/context_menu.rb +319 -0
  61. data/lib/shadcn/ui/dialog.rb +226 -0
  62. data/lib/shadcn/ui/direction.rb +23 -0
  63. data/lib/shadcn/ui/drawer.rb +217 -0
  64. data/lib/shadcn/ui/dropdown_menu.rb +384 -0
  65. data/lib/shadcn/ui/empty.rb +97 -0
  66. data/lib/shadcn/ui/field.rb +126 -0
  67. data/lib/shadcn/ui/hover_card.rb +75 -0
  68. data/lib/shadcn/ui/input.rb +36 -0
  69. data/lib/shadcn/ui/input_group.rb +32 -0
  70. data/lib/shadcn/ui/input_otp.rb +112 -0
  71. data/lib/shadcn/ui/item.rb +115 -0
  72. data/lib/shadcn/ui/kbd.rb +45 -0
  73. data/lib/shadcn/ui/label.rb +28 -0
  74. data/lib/shadcn/ui/menubar.rb +345 -0
  75. data/lib/shadcn/ui/native_select.rb +31 -0
  76. data/lib/shadcn/ui/navigation_menu.rb +238 -0
  77. data/lib/shadcn/ui/pagination.rb +224 -0
  78. data/lib/shadcn/ui/popover.rb +147 -0
  79. data/lib/shadcn/ui/progress.rb +40 -0
  80. data/lib/shadcn/ui/radio_group.rb +92 -0
  81. data/lib/shadcn/ui/resizable.rb +108 -0
  82. data/lib/shadcn/ui/scroll_area.rb +75 -0
  83. data/lib/shadcn/ui/select.rb +235 -0
  84. data/lib/shadcn/ui/separator.rb +36 -0
  85. data/lib/shadcn/ui/sheet.rb +231 -0
  86. data/lib/shadcn/ui/sidebar.rb +420 -0
  87. data/lib/shadcn/ui/skeleton.rb +23 -0
  88. data/lib/shadcn/ui/slider.rb +72 -0
  89. data/lib/shadcn/ui/sonner.rb +177 -0
  90. data/lib/shadcn/ui/spinner.rb +58 -0
  91. data/lib/shadcn/ui/switch.rb +75 -0
  92. data/lib/shadcn/ui/table.rb +154 -0
  93. data/lib/shadcn/ui/tabs.rb +154 -0
  94. data/lib/shadcn/ui/text_field.rb +146 -0
  95. data/lib/shadcn/ui/textarea.rb +32 -0
  96. data/lib/shadcn/ui/theme_toggle.rb +74 -0
  97. data/lib/shadcn/ui/toggle.rb +66 -0
  98. data/lib/shadcn/ui/toggle_group.rb +75 -0
  99. data/lib/shadcn/ui/tooltip.rb +78 -0
  100. data/lib/shadcn/ui/typography.rb +217 -0
  101. data/lib/shadcn/version.rb +5 -0
  102. data/lib/shadcn-phlex.rb +6 -0
  103. data/lib/shadcn.rb +80 -0
  104. data/package.json +14 -0
  105. data/skills/shadcn-phlex/SKILL.md +190 -0
  106. data/skills/shadcn-phlex/evals/evals.json +90 -0
  107. data/skills/shadcn-phlex/references/component-catalog.md +355 -0
  108. data/skills/shadcn-phlex/rules/composition.md +235 -0
  109. data/skills/shadcn-phlex/rules/forms.md +151 -0
  110. data/skills/shadcn-phlex/rules/helpers.md +54 -0
  111. data/skills/shadcn-phlex/rules/stimulus.md +61 -0
  112. data/skills/shadcn-phlex/rules/styling.md +177 -0
  113. metadata +209 -0
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Slider
6
+ class Slider < Base
7
+ def initialize(value: 0, min: 0, max: 100, name: nil, **attrs)
8
+ @value = value
9
+ @min = min
10
+ @max = max
11
+ @name = name
12
+ @attrs = attrs
13
+ end
14
+
15
+ def view_template
16
+ span(**build_attrs) do
17
+ if @name
18
+ input(type: "hidden", name: @name, value: @value.to_s, data_shadcn__slider_target: "input")
19
+ end
20
+ pct = ((@value.to_f - @min) / (@max - @min) * 100).clamp(0, 100)
21
+
22
+ span(
23
+ data_slot: "slider-track",
24
+ data_shadcn__slider_target: "track",
25
+ data_action: "click->shadcn--slider#clickTrack",
26
+ data_orientation: "horizontal",
27
+ class: "relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
28
+ ) do
29
+ span(
30
+ data_slot: "slider-range",
31
+ data_shadcn__slider_target: "range",
32
+ data_orientation: "horizontal",
33
+ class: "absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
34
+ style: "left: 0; width: #{pct}%"
35
+ )
36
+ end
37
+
38
+ span(
39
+ data_slot: "slider-thumb",
40
+ data_shadcn__slider_target: "thumb",
41
+ data_action: "pointerdown->shadcn--slider#startDrag keydown->shadcn--slider#keydown",
42
+ class: "absolute block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
43
+ style: "left: calc(#{pct}% - 8px)",
44
+ role: "slider",
45
+ tabindex: "0",
46
+ aria_valuemin: @min,
47
+ aria_valuemax: @max,
48
+ aria_valuenow: @value
49
+ )
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def build_attrs
56
+ classes = cn(
57
+ "relative flex w-full touch-none items-center select-none",
58
+ "data-[disabled]:opacity-50",
59
+ @attrs.delete(:class)
60
+ )
61
+ @attrs.merge(
62
+ data_slot: "slider",
63
+ data_controller: "shadcn--slider",
64
+ data_shadcn__slider_value_value: @value,
65
+ data_shadcn__slider_min_value: @min,
66
+ data_shadcn__slider_max_value: @max,
67
+ class: classes
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Sonner (Toast container)
6
+ # The JS behavior must be provided by a Stimulus controller or similar
7
+ # This provides the styled markup structure
8
+ class Toaster < Base
9
+ def initialize(position: "bottom-right", **attrs)
10
+ @position = position
11
+ @attrs = attrs
12
+ end
13
+
14
+ def view_template(&block)
15
+ div(**build_attrs, &block)
16
+ end
17
+
18
+ private
19
+
20
+ def build_attrs
21
+ classes = cn(
22
+ "fixed z-[100] flex max-h-screen flex-col gap-2 p-4",
23
+ position_classes,
24
+ @attrs.delete(:class)
25
+ )
26
+ @attrs.merge(
27
+ data_slot: "toaster",
28
+ data_controller: "shadcn--toast",
29
+ data_shadcn__toast_target: "container",
30
+ data_position: @position,
31
+ class: classes
32
+ )
33
+ end
34
+
35
+ def position_classes
36
+ case @position
37
+ when "top-left" then "top-0 left-0"
38
+ when "top-center" then "top-0 left-1/2 -translate-x-1/2"
39
+ when "top-right" then "top-0 right-0"
40
+ when "bottom-left" then "bottom-0 left-0"
41
+ when "bottom-center" then "bottom-0 left-1/2 -translate-x-1/2"
42
+ when "bottom-right" then "bottom-0 right-0"
43
+ else "bottom-0 right-0"
44
+ end
45
+ end
46
+ end
47
+
48
+ class Toast < Base
49
+ VARIANTS = ClassVariants.build(
50
+ base: [
51
+ "group pointer-events-auto relative flex w-full items-center justify-between gap-4 overflow-hidden",
52
+ "rounded-md border p-4 pr-6 shadow-lg transition-all",
53
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full",
54
+ "data-[state=open]:animate-in data-[state=open]:slide-in-from-top-full data-[state=open]:fade-in-0"
55
+ ].join(" "),
56
+ variants: {
57
+ variant: {
58
+ default: "border bg-background text-foreground",
59
+ success: "border-green-500/50 bg-green-50 text-green-900 dark:bg-green-950 dark:text-green-100",
60
+ error: "border-destructive/50 bg-destructive text-white",
61
+ warning: "border-yellow-500/50 bg-yellow-50 text-yellow-900 dark:bg-yellow-950 dark:text-yellow-100",
62
+ info: "border-blue-500/50 bg-blue-50 text-blue-900 dark:bg-blue-950 dark:text-blue-100"
63
+ }
64
+ },
65
+ defaults: { variant: :default }
66
+ )
67
+
68
+ def initialize(variant: :default, **attrs)
69
+ @variant = variant
70
+ @attrs = attrs
71
+ end
72
+
73
+ def view_template(&block)
74
+ div(**build_attrs, &block)
75
+ end
76
+
77
+ private
78
+
79
+ def build_attrs
80
+ classes = cn(VARIANTS.render(variant: @variant), @attrs.delete(:class))
81
+ @attrs.merge(data_slot: "toast", data_variant: @variant, role: "alert", class: classes)
82
+ end
83
+ end
84
+
85
+ class ToastTitle < Base
86
+ def initialize(**attrs)
87
+ @attrs = attrs
88
+ end
89
+
90
+ def view_template(&block)
91
+ div(**build_attrs, &block)
92
+ end
93
+
94
+ private
95
+
96
+ def build_attrs
97
+ classes = cn("text-sm font-semibold", @attrs.delete(:class))
98
+ @attrs.merge(data_slot: "toast-title", class: classes)
99
+ end
100
+ end
101
+
102
+ class ToastDescription < Base
103
+ def initialize(**attrs)
104
+ @attrs = attrs
105
+ end
106
+
107
+ def view_template(&block)
108
+ div(**build_attrs, &block)
109
+ end
110
+
111
+ private
112
+
113
+ def build_attrs
114
+ classes = cn("text-sm opacity-90", @attrs.delete(:class))
115
+ @attrs.merge(data_slot: "toast-description", class: classes)
116
+ end
117
+ end
118
+
119
+ class ToastAction < Base
120
+ def initialize(**attrs)
121
+ @attrs = attrs
122
+ end
123
+
124
+ def view_template(&block)
125
+ button(**build_attrs, &block)
126
+ end
127
+
128
+ private
129
+
130
+ def build_attrs
131
+ classes = cn(
132
+ "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3",
133
+ "text-sm font-medium transition-colors",
134
+ "hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring",
135
+ "disabled:pointer-events-none disabled:opacity-50",
136
+ @attrs.delete(:class)
137
+ )
138
+ @attrs.merge(data_slot: "toast-action", type: "button", class: classes)
139
+ end
140
+ end
141
+
142
+ class ToastClose < Base
143
+ def initialize(**attrs)
144
+ @attrs = attrs
145
+ end
146
+
147
+ def view_template
148
+ button(**build_attrs) do
149
+ svg(
150
+ xmlns: "http://www.w3.org/2000/svg",
151
+ width: "14", height: "14",
152
+ viewbox: "0 0 24 24",
153
+ fill: "none",
154
+ stroke: "currentColor",
155
+ stroke_width: "2",
156
+ class: "size-4"
157
+ ) do |s|
158
+ s.path(d: "M18 6 6 18")
159
+ s.path(d: "m6 6 12 12")
160
+ end
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def build_attrs
167
+ classes = cn(
168
+ "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity",
169
+ "group-hover:opacity-100",
170
+ "hover:text-foreground focus:opacity-100 focus:outline-none",
171
+ @attrs.delete(:class)
172
+ )
173
+ @attrs.merge(data_slot: "toast-close", type: "button", class: classes)
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Spinner
6
+ class Spinner < Base
7
+ def initialize(size: :default, **attrs)
8
+ @size = size
9
+ @attrs = attrs
10
+ end
11
+
12
+ def view_template
13
+ span(**build_attrs) do
14
+ # Animated loader SVG (Lucide Loader2 equivalent)
15
+ svg(
16
+ xmlns: "http://www.w3.org/2000/svg",
17
+ width: "24", height: "24",
18
+ viewbox: "0 0 24 24",
19
+ fill: "none",
20
+ stroke: "currentColor",
21
+ stroke_width: "2",
22
+ stroke_linecap: "round",
23
+ stroke_linejoin: "round",
24
+ class: cn("animate-spin", size_class)
25
+ ) do |s|
26
+ s.path(d: "M21 12a9 9 0 1 1-6.219-8.56")
27
+ end
28
+ span(class: "sr-only") { "Loading..." }
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def build_attrs
35
+ classes = cn(
36
+ "inline-flex items-center justify-center text-muted-foreground",
37
+ @attrs.delete(:class)
38
+ )
39
+ @attrs.merge(
40
+ data_slot: "spinner",
41
+ role: "status",
42
+ aria_label: "Loading",
43
+ class: classes
44
+ )
45
+ end
46
+
47
+ def size_class
48
+ case @size
49
+ when :xs then "size-3"
50
+ when :sm then "size-4"
51
+ when :default then "size-5"
52
+ when :lg then "size-8"
53
+ else "size-5"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Switch
6
+ # Sizes: default, sm
7
+ class Switch < Base
8
+ def initialize(checked: false, size: :default, name: nil, **attrs)
9
+ @checked = checked
10
+ @size = size
11
+ @name = name
12
+ @attrs = attrs
13
+ end
14
+
15
+ def view_template(&block)
16
+ label(
17
+ data_controller: "shadcn--switch",
18
+ data_shadcn__switch_checked_value: @checked.to_s,
19
+ class: "inline-flex items-center gap-2 cursor-pointer"
20
+ ) do
21
+ if @name
22
+ input(type: "hidden", name: @name, value: @checked ? "1" : "0", data_shadcn__switch_target: "input")
23
+ end
24
+ button(**build_attrs) do
25
+ span(**thumb_attrs)
26
+ end
27
+ if block
28
+ span(class: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", &block)
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def build_attrs
36
+ classes = cn(
37
+ "peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs",
38
+ "transition-all outline-none",
39
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
40
+ "disabled:cursor-not-allowed disabled:opacity-50",
41
+ "data-[size=default]:h-[1.15rem] data-[size=default]:w-8",
42
+ "data-[size=sm]:h-3.5 data-[size=sm]:w-6",
43
+ "data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
44
+ "dark:data-[state=unchecked]:bg-input/80",
45
+ @attrs.delete(:class)
46
+ )
47
+ @attrs.merge(
48
+ data_slot: "switch",
49
+ data_shadcn__switch_target: "button",
50
+ data_action: "click->shadcn--switch#toggle",
51
+ data_state: @checked ? "checked" : "unchecked",
52
+ data_size: @size,
53
+ role: "switch",
54
+ type: "button",
55
+ aria_checked: @checked,
56
+ class: classes
57
+ )
58
+ end
59
+
60
+ def thumb_attrs
61
+ {
62
+ data_slot: "switch-thumb",
63
+ data_shadcn__switch_target: "thumb",
64
+ data_state: @checked ? "checked" : "unchecked",
65
+ class: cn(
66
+ "pointer-events-none block rounded-full bg-background ring-0 transition-transform",
67
+ "group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3",
68
+ "data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
69
+ "dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
70
+ )
71
+ }
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Table
6
+ class Table < Base
7
+ def initialize(**attrs)
8
+ @attrs = attrs
9
+ end
10
+
11
+ def view_template(&block)
12
+ div(data_slot: "table-container", class: "relative w-full overflow-x-auto") do
13
+ table(**build_attrs, &block)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def build_attrs
20
+ classes = cn("w-full caption-bottom text-sm", @attrs.delete(:class))
21
+ @attrs.merge(data_slot: "table", class: classes)
22
+ end
23
+ end
24
+
25
+ class TableHeader < Base
26
+ def initialize(**attrs)
27
+ @attrs = attrs
28
+ end
29
+
30
+ def view_template(&block)
31
+ thead(**build_attrs, &block)
32
+ end
33
+
34
+ private
35
+
36
+ def build_attrs
37
+ classes = cn("[&_tr]:border-b", @attrs.delete(:class))
38
+ @attrs.merge(data_slot: "table-header", class: classes)
39
+ end
40
+ end
41
+
42
+ class TableBody < Base
43
+ def initialize(**attrs)
44
+ @attrs = attrs
45
+ end
46
+
47
+ def view_template(&block)
48
+ tbody(**build_attrs, &block)
49
+ end
50
+
51
+ private
52
+
53
+ def build_attrs
54
+ classes = cn("[&_tr:last-child]:border-0", @attrs.delete(:class))
55
+ @attrs.merge(data_slot: "table-body", class: classes)
56
+ end
57
+ end
58
+
59
+ class TableFooter < Base
60
+ def initialize(**attrs)
61
+ @attrs = attrs
62
+ end
63
+
64
+ def view_template(&block)
65
+ tfoot(**build_attrs, &block)
66
+ end
67
+
68
+ private
69
+
70
+ def build_attrs
71
+ classes = cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", @attrs.delete(:class))
72
+ @attrs.merge(data_slot: "table-footer", class: classes)
73
+ end
74
+ end
75
+
76
+ class TableRow < Base
77
+ def initialize(**attrs)
78
+ @attrs = attrs
79
+ end
80
+
81
+ def view_template(&block)
82
+ tr(**build_attrs, &block)
83
+ end
84
+
85
+ private
86
+
87
+ def build_attrs
88
+ classes = cn(
89
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
90
+ @attrs.delete(:class)
91
+ )
92
+ @attrs.merge(data_slot: "table-row", class: classes)
93
+ end
94
+ end
95
+
96
+ class TableHead < Base
97
+ def initialize(**attrs)
98
+ @attrs = attrs
99
+ end
100
+
101
+ def view_template(&block)
102
+ th(**build_attrs, &block)
103
+ end
104
+
105
+ private
106
+
107
+ def build_attrs
108
+ classes = cn(
109
+ "text-muted-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap",
110
+ "[&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
111
+ @attrs.delete(:class)
112
+ )
113
+ @attrs.merge(data_slot: "table-head", class: classes)
114
+ end
115
+ end
116
+
117
+ class TableCell < Base
118
+ def initialize(**attrs)
119
+ @attrs = attrs
120
+ end
121
+
122
+ def view_template(&block)
123
+ td(**build_attrs, &block)
124
+ end
125
+
126
+ private
127
+
128
+ def build_attrs
129
+ classes = cn(
130
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
131
+ @attrs.delete(:class)
132
+ )
133
+ @attrs.merge(data_slot: "table-cell", class: classes)
134
+ end
135
+ end
136
+
137
+ class TableCaption < Base
138
+ def initialize(**attrs)
139
+ @attrs = attrs
140
+ end
141
+
142
+ def view_template(&block)
143
+ caption(**build_attrs, &block)
144
+ end
145
+
146
+ private
147
+
148
+ def build_attrs
149
+ classes = cn("text-muted-foreground mt-4 text-sm", @attrs.delete(:class))
150
+ @attrs.merge(data_slot: "table-caption", class: classes)
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Tabs
6
+ # Wired to shadcn--tabs Stimulus controller
7
+ class Tabs < Base
8
+ def initialize(value: nil, orientation: :horizontal, **attrs)
9
+ @value = value
10
+ @orientation = orientation
11
+ @attrs = attrs
12
+ end
13
+
14
+ def view_template(&block)
15
+ div(**build_attrs, &block)
16
+ end
17
+
18
+ private
19
+
20
+ def build_attrs
21
+ classes = cn(
22
+ "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
23
+ @attrs.delete(:class)
24
+ )
25
+ result = @attrs.merge(
26
+ data_slot: "tabs",
27
+ data_orientation: @orientation,
28
+ data_controller: "shadcn--tabs",
29
+ data_shadcn__tabs_orientation_value: @orientation,
30
+ class: classes
31
+ )
32
+ result[:data_shadcn__tabs_value_value] = @value if @value
33
+ result
34
+ end
35
+ end
36
+
37
+ class TabsList < Base
38
+ VARIANTS = ClassVariants.build(
39
+ base: [
40
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground",
41
+ "group-data-[orientation=horizontal]/tabs:h-9",
42
+ "group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
43
+ "data-[variant=line]:rounded-none"
44
+ ].join(" "),
45
+ variants: {
46
+ variant: {
47
+ default: "bg-muted",
48
+ line: "gap-1 bg-transparent"
49
+ }
50
+ },
51
+ defaults: { variant: :default }
52
+ )
53
+
54
+ def initialize(variant: :default, **attrs)
55
+ @variant = variant
56
+ @attrs = attrs
57
+ end
58
+
59
+ def view_template(&block)
60
+ div(**build_attrs, &block)
61
+ end
62
+
63
+ private
64
+
65
+ def build_attrs
66
+ classes = cn(VARIANTS.render(variant: @variant), @attrs.delete(:class))
67
+ @attrs.merge(
68
+ data_slot: "tabs-list",
69
+ data_variant: @variant,
70
+ data_shadcn__tabs_target: "list",
71
+ role: "tablist",
72
+ class: classes
73
+ )
74
+ end
75
+ end
76
+
77
+ class TabsTrigger < Base
78
+ def initialize(value:, **attrs)
79
+ @value = value
80
+ @attrs = attrs
81
+ end
82
+
83
+ def view_template(&block)
84
+ button(**build_attrs, &block)
85
+ end
86
+
87
+ private
88
+
89
+ def build_attrs
90
+ classes = cn(
91
+ # Base
92
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5",
93
+ "rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap",
94
+ "text-foreground/60 transition-all",
95
+ "group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start",
96
+ "hover:text-foreground",
97
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring",
98
+ "disabled:pointer-events-none disabled:opacity-50",
99
+ # Default variant active state
100
+ "group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm",
101
+ "group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
102
+ "dark:text-muted-foreground dark:hover:text-foreground",
103
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
104
+ # Line variant overrides
105
+ "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
106
+ "dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
107
+ # Active state
108
+ "data-[state=active]:bg-background data-[state=active]:text-foreground",
109
+ "dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
110
+ # After pseudo-element (line indicator)
111
+ "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity",
112
+ "group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5",
113
+ "group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5",
114
+ "group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
115
+ @attrs.delete(:class)
116
+ )
117
+ @attrs.merge(
118
+ data_slot: "tabs-trigger",
119
+ data_value: @value,
120
+ data_shadcn__tabs_target: "trigger",
121
+ data_action: "click->shadcn--tabs#select keydown->shadcn--tabs#keydown",
122
+ role: "tab",
123
+ type: "button",
124
+ class: classes
125
+ )
126
+ end
127
+ end
128
+
129
+ class TabsContent < Base
130
+ def initialize(value:, **attrs)
131
+ @value = value
132
+ @attrs = attrs
133
+ end
134
+
135
+ def view_template(&block)
136
+ div(**build_attrs, &block)
137
+ end
138
+
139
+ private
140
+
141
+ def build_attrs
142
+ classes = cn("flex-1 outline-none", @attrs.delete(:class))
143
+ @attrs.merge(
144
+ data_slot: "tabs-content",
145
+ data_value: @value,
146
+ data_shadcn__tabs_target: "content",
147
+ role: "tabpanel",
148
+ hidden: true,
149
+ class: classes
150
+ )
151
+ end
152
+ end
153
+ end
154
+ end