aeno 0.0.3

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 (140) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +230 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/aeno/application.css +1 -0
  6. data/app/assets/stylesheets/aeno/base.css +43 -0
  7. data/app/assets/stylesheets/aeno/reset.css +397 -0
  8. data/app/assets/stylesheets/aeno/source.css +15 -0
  9. data/app/assets/stylesheets/aeno/theme.css +6 -0
  10. data/app/assets/stylesheets/aeno/themes/slate.css +163 -0
  11. data/app/assets/stylesheets/aeno/themes/zinc.css +163 -0
  12. data/app/assets/stylesheets/aeno/utilities.css +23 -0
  13. data/app/components/aeno/application_view_component.rb +219 -0
  14. data/app/components/aeno/blocks/component_preview/component.html.erb +7 -0
  15. data/app/components/aeno/blocks/component_preview/component.rb +10 -0
  16. data/app/components/aeno/blocks/component_preview/styles.css +10 -0
  17. data/app/components/aeno/form_builder.rb +87 -0
  18. data/app/components/aeno/pages/showcase/index/component.html.erb +53 -0
  19. data/app/components/aeno/pages/showcase/index/component.rb +7 -0
  20. data/app/components/aeno/pages/showcase/index/styles.css +27 -0
  21. data/app/components/aeno/pages/showcase/placeholder/component.html.erb +7 -0
  22. data/app/components/aeno/pages/showcase/placeholder/component.rb +10 -0
  23. data/app/components/aeno/pages/showcase/show/component.html.erb +38 -0
  24. data/app/components/aeno/pages/showcase/show/component.rb +48 -0
  25. data/app/components/aeno/primitives/button/component.rb +66 -0
  26. data/app/components/aeno/primitives/button/controller.js +7 -0
  27. data/app/components/aeno/primitives/button/styles.css +153 -0
  28. data/app/components/aeno/primitives/card/component.html.erb +3 -0
  29. data/app/components/aeno/primitives/card/component.rb +42 -0
  30. data/app/components/aeno/primitives/card/styles.css +28 -0
  31. data/app/components/aeno/primitives/conversation/component.html.erb +28 -0
  32. data/app/components/aeno/primitives/conversation/component.rb +15 -0
  33. data/app/components/aeno/primitives/conversation/controller.js +18 -0
  34. data/app/components/aeno/primitives/conversation/message/component.html.erb +24 -0
  35. data/app/components/aeno/primitives/conversation/message/component.rb +35 -0
  36. data/app/components/aeno/primitives/conversation/streaming_indicator/component.html.erb +21 -0
  37. data/app/components/aeno/primitives/conversation/streaming_indicator/component.rb +18 -0
  38. data/app/components/aeno/primitives/conversation/styles.css +221 -0
  39. data/app/components/aeno/primitives/conversation/user_message_box/component.html.erb +1 -0
  40. data/app/components/aeno/primitives/conversation/user_message_box/component.rb +4 -0
  41. data/app/components/aeno/primitives/drawer/component.html.erb +43 -0
  42. data/app/components/aeno/primitives/drawer/component.rb +33 -0
  43. data/app/components/aeno/primitives/drawer/controller.js +104 -0
  44. data/app/components/aeno/primitives/drawer/styles.css +90 -0
  45. data/app/components/aeno/primitives/dropdown/checkbox.rb +22 -0
  46. data/app/components/aeno/primitives/dropdown/component.html.erb +38 -0
  47. data/app/components/aeno/primitives/dropdown/component.rb +53 -0
  48. data/app/components/aeno/primitives/dropdown/controller.js +153 -0
  49. data/app/components/aeno/primitives/dropdown/item.rb +29 -0
  50. data/app/components/aeno/primitives/dropdown/label.rb +7 -0
  51. data/app/components/aeno/primitives/dropdown/radio_group.rb +16 -0
  52. data/app/components/aeno/primitives/dropdown/radio_item.rb +24 -0
  53. data/app/components/aeno/primitives/dropdown/separator.rb +7 -0
  54. data/app/components/aeno/primitives/dropdown/styles.css +155 -0
  55. data/app/components/aeno/primitives/empty/component.html.erb +15 -0
  56. data/app/components/aeno/primitives/empty/component.rb +18 -0
  57. data/app/components/aeno/primitives/empty/styles.css +40 -0
  58. data/app/components/aeno/primitives/input_attachments/component.html.erb +60 -0
  59. data/app/components/aeno/primitives/input_attachments/component.rb +52 -0
  60. data/app/components/aeno/primitives/input_attachments/controller.js +357 -0
  61. data/app/components/aeno/primitives/input_attachments/styles.css +102 -0
  62. data/app/components/aeno/primitives/input_color/component.html.erb +24 -0
  63. data/app/components/aeno/primitives/input_color/component.rb +42 -0
  64. data/app/components/aeno/primitives/input_color/styles.css +64 -0
  65. data/app/components/aeno/primitives/input_password/component.html.erb +43 -0
  66. data/app/components/aeno/primitives/input_password/component.rb +20 -0
  67. data/app/components/aeno/primitives/input_password/controller.js +17 -0
  68. data/app/components/aeno/primitives/input_password/styles.css +61 -0
  69. data/app/components/aeno/primitives/input_select/component.html.erb +43 -0
  70. data/app/components/aeno/primitives/input_select/component.rb +21 -0
  71. data/app/components/aeno/primitives/input_select/option.rb +14 -0
  72. data/app/components/aeno/primitives/input_select/styles.css +30 -0
  73. data/app/components/aeno/primitives/input_slider/component.html.erb +33 -0
  74. data/app/components/aeno/primitives/input_slider/component.rb +35 -0
  75. data/app/components/aeno/primitives/input_slider/styles.css +74 -0
  76. data/app/components/aeno/primitives/input_tagging/component.html.erb +73 -0
  77. data/app/components/aeno/primitives/input_tagging/component.rb +40 -0
  78. data/app/components/aeno/primitives/input_tagging/controller.js +326 -0
  79. data/app/components/aeno/primitives/input_tagging/styles.css +148 -0
  80. data/app/components/aeno/primitives/input_text/component.html.erb +25 -0
  81. data/app/components/aeno/primitives/input_text/component.rb +20 -0
  82. data/app/components/aeno/primitives/input_text/styles.css +38 -0
  83. data/app/components/aeno/primitives/input_text_area/component.html.erb +23 -0
  84. data/app/components/aeno/primitives/input_text_area/component.rb +19 -0
  85. data/app/components/aeno/primitives/input_text_area/styles.css +30 -0
  86. data/app/components/aeno/primitives/input_text_area_ai/component.html.erb +51 -0
  87. data/app/components/aeno/primitives/input_text_area_ai/component.rb +47 -0
  88. data/app/components/aeno/primitives/input_text_area_ai/controller.js +198 -0
  89. data/app/components/aeno/primitives/input_text_area_ai/styles.css +91 -0
  90. data/app/components/aeno/primitives/input_wrapper/component.html.erb +20 -0
  91. data/app/components/aeno/primitives/input_wrapper/component.rb +31 -0
  92. data/app/components/aeno/primitives/input_wrapper/styles.css +72 -0
  93. data/app/components/aeno/primitives/layouts/agentic/component.html.erb +4 -0
  94. data/app/components/aeno/primitives/layouts/agentic/component.rb +9 -0
  95. data/app/components/aeno/primitives/layouts/agentic/styles.css +23 -0
  96. data/app/components/aeno/primitives/layouts/app/aside.rb +9 -0
  97. data/app/components/aeno/primitives/layouts/app/component.html.erb +14 -0
  98. data/app/components/aeno/primitives/layouts/app/component.rb +11 -0
  99. data/app/components/aeno/primitives/layouts/app/sidebar.rb +9 -0
  100. data/app/components/aeno/primitives/layouts/app/styles.css +46 -0
  101. data/app/components/aeno/primitives/page/component.html.erb +24 -0
  102. data/app/components/aeno/primitives/page/component.rb +23 -0
  103. data/app/components/aeno/primitives/page/styles.css +55 -0
  104. data/app/components/aeno/primitives/sidebar/component.html.erb +25 -0
  105. data/app/components/aeno/primitives/sidebar/component.rb +14 -0
  106. data/app/components/aeno/primitives/sidebar/footer.rb +7 -0
  107. data/app/components/aeno/primitives/sidebar/group.rb +18 -0
  108. data/app/components/aeno/primitives/sidebar/header.rb +7 -0
  109. data/app/components/aeno/primitives/sidebar/item.rb +19 -0
  110. data/app/components/aeno/primitives/sidebar/styles.css +95 -0
  111. data/app/components/aeno/primitives/spinner/component.rb +36 -0
  112. data/app/components/aeno/primitives/spinner/styles.css +81 -0
  113. data/app/components/aeno/primitives/table/cell.rb +7 -0
  114. data/app/components/aeno/primitives/table/column.rb +7 -0
  115. data/app/components/aeno/primitives/table/component.html.erb +8 -0
  116. data/app/components/aeno/primitives/table/component.rb +14 -0
  117. data/app/components/aeno/primitives/table/header.rb +13 -0
  118. data/app/components/aeno/primitives/table/row.rb +11 -0
  119. data/app/components/aeno/primitives/table/styles.css +39 -0
  120. data/app/controllers/aeno/application_controller.rb +15 -0
  121. data/app/controllers/aeno/showcase_controller.rb +40 -0
  122. data/app/controllers/aeno/theme_controller.rb +10 -0
  123. data/app/helpers/aeno/application_helper.rb +28 -0
  124. data/app/javascript/aeno/application.js +3 -0
  125. data/app/javascript/aeno/controllers/application.js +5 -0
  126. data/app/javascript/aeno/controllers/index.js +5 -0
  127. data/app/javascript/aeno/controllers/loader.js +62 -0
  128. data/app/jobs/aeno/application_job.rb +4 -0
  129. data/app/models/aeno/application_record.rb +5 -0
  130. data/app/views/layouts/aeno/application.html.erb +55 -0
  131. data/config/importmap.rb +20 -0
  132. data/config/routes.rb +5 -0
  133. data/lib/aeno/configuration.rb +56 -0
  134. data/lib/aeno/engine.rb +43 -0
  135. data/lib/aeno/engine_helpers.rb +44 -0
  136. data/lib/aeno/theme.rb +326 -0
  137. data/lib/aeno/version.rb +3 -0
  138. data/lib/aeno.rb +11 -0
  139. data/lib/tasks/aeno_tasks.rake +39 -0
  140. metadata +310 -0
@@ -0,0 +1,163 @@
1
+ /* =============================================================================
2
+ Theme: Zinc
3
+ ============================================================================= */
4
+
5
+ [data-theme="zinc"] {
6
+ /* Background & Foreground */
7
+ --ui-background: #ffffff;
8
+ --ui-foreground: #18181b;
9
+
10
+ /* Generic */
11
+ --ui-border: #e4e4e7;
12
+ --ui-destructive: #dc2626;
13
+ --ui-primary: #18181b;
14
+ --ui-accent: #f4f4f5;
15
+ --ui-accent-foreground: #18181b;
16
+ --ui-popover: #ffffff;
17
+ --ui-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
18
+ --ui-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
19
+
20
+ /* Muted */
21
+ --ui-muted: #f4f4f5;
22
+ --ui-muted-foreground: #71717a;
23
+
24
+ /* Card */
25
+ --ui-card-bg: #ffffff;
26
+ --ui-card-fg: #18181b;
27
+ --ui-card-border: #e4e4e7;
28
+ --ui-card-radius: 0.75rem;
29
+ --ui-card-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
30
+
31
+ /* Area */
32
+ --ui-area-bg: #fafafa;
33
+ --ui-area-fg: #18181b;
34
+ --ui-area-border: #e4e4e7;
35
+
36
+ /* Sidebar */
37
+ --ui-sidebar-bg: #fafafa;
38
+ --ui-sidebar-fg: #18181b;
39
+ --ui-sidebar-border: #e4e4e7;
40
+ --ui-sidebar-hover: #f4f4f5;
41
+ --ui-sidebar-active: #e4e4e7;
42
+
43
+ /* Button */
44
+ --ui-button-bg: #18181b;
45
+ --ui-button-bg-hover: #27272a;
46
+ --ui-button-fg: #ffffff;
47
+ --ui-button-border: #18181b;
48
+ --ui-button-secondary-bg: #f4f4f5;
49
+ --ui-button-secondary-bg-hover: #e4e4e7;
50
+ --ui-button-secondary-fg: #18181b;
51
+ --ui-button-destructive-bg: #dc2626;
52
+ --ui-button-destructive-bg-hover: #b91c1c;
53
+ --ui-button-destructive-fg: #ffffff;
54
+ --ui-button-ghost-bg-hover: #f4f4f5;
55
+ --ui-button-ghost-fg: #18181b;
56
+ --ui-button-outline-bg-hover: #f4f4f5;
57
+ --ui-button-outline-fg: #18181b;
58
+
59
+ /* Input */
60
+ --ui-input-bg: #ffffff;
61
+ --ui-input-fg: #18181b;
62
+ --ui-input-border: #e4e4e7;
63
+ --ui-input-border-focus: #a1a1aa;
64
+ --ui-input-ring: #3b82f6;
65
+ --ui-input-placeholder: #a1a1aa;
66
+
67
+ /* Typography */
68
+ --ui-heading-color: #18181b;
69
+ --ui-body-color: #3f3f46;
70
+ --ui-muted-color: #71717a;
71
+ --ui-link-color: #2563eb;
72
+ --ui-link-hover: #1d4ed8;
73
+ --ui-ring: #3b82f6;
74
+
75
+ /* Menu */
76
+ --ui-menu-bg: #ffffff;
77
+ --ui-menu-fg: #18181b;
78
+ --ui-menu-border: #e4e4e7;
79
+ --ui-menu-radius: 0.375rem;
80
+ --ui-menu-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
81
+ --ui-menu-padding: 0.25rem;
82
+ --ui-menu-item-radius: 0.25rem;
83
+ }
84
+
85
+ /* Dark mode */
86
+ [data-theme="zinc"][data-mode="dark"] {
87
+ /* Background & Foreground */
88
+ --ui-background: #09090b;
89
+ --ui-foreground: #fafafa;
90
+
91
+ /* Generic */
92
+ --ui-border: #27272a;
93
+ --ui-destructive: #dc2626;
94
+ --ui-primary: #fafafa;
95
+ --ui-accent: #27272a;
96
+ --ui-accent-foreground: #fafafa;
97
+ --ui-popover: #18181b;
98
+ --ui-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3);
99
+ --ui-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.3);
100
+
101
+ /* Muted */
102
+ --ui-muted: #27272a;
103
+ --ui-muted-foreground: #a1a1aa;
104
+
105
+ /* Card */
106
+ --ui-card-bg: #18181b;
107
+ --ui-card-fg: #fafafa;
108
+ --ui-card-border: #27272a;
109
+ --ui-card-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.3);
110
+
111
+ /* Area */
112
+ --ui-area-bg: #18181b;
113
+ --ui-area-fg: #fafafa;
114
+ --ui-area-border: #27272a;
115
+
116
+ /* Sidebar */
117
+ --ui-sidebar-bg: #18181b;
118
+ --ui-sidebar-fg: #fafafa;
119
+ --ui-sidebar-border: #27272a;
120
+ --ui-sidebar-hover: #27272a;
121
+ --ui-sidebar-active: #3f3f46;
122
+
123
+ /* Button */
124
+ --ui-button-bg: #fafafa;
125
+ --ui-button-bg-hover: #e4e4e7;
126
+ --ui-button-fg: #18181b;
127
+ --ui-button-border: #fafafa;
128
+ --ui-button-secondary-bg: #27272a;
129
+ --ui-button-secondary-bg-hover: #3f3f46;
130
+ --ui-button-secondary-fg: #fafafa;
131
+ --ui-button-destructive-bg: #dc2626;
132
+ --ui-button-destructive-bg-hover: #ef4444;
133
+ --ui-button-destructive-fg: #ffffff;
134
+ --ui-button-ghost-bg-hover: #27272a;
135
+ --ui-button-ghost-fg: #fafafa;
136
+ --ui-button-outline-bg-hover: #27272a;
137
+ --ui-button-outline-fg: #fafafa;
138
+
139
+ /* Input */
140
+ --ui-input-bg: #18181b;
141
+ --ui-input-fg: #fafafa;
142
+ --ui-input-border: #27272a;
143
+ --ui-input-border-focus: #52525b;
144
+ --ui-input-ring: #3b82f6;
145
+ --ui-input-placeholder: #52525b;
146
+
147
+ /* Typography */
148
+ --ui-heading-color: #fafafa;
149
+ --ui-body-color: #d4d4d8;
150
+ --ui-muted-color: #a1a1aa;
151
+ --ui-link-color: #60a5fa;
152
+ --ui-link-hover: #93c5fd;
153
+ --ui-ring: #3b82f6;
154
+
155
+ /* Menu */
156
+ --ui-menu-bg: #18181b;
157
+ --ui-menu-fg: #fafafa;
158
+ --ui-menu-border: #27272a;
159
+ --ui-menu-radius: 0.375rem;
160
+ --ui-menu-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3);
161
+ --ui-menu-padding: 0.25rem;
162
+ --ui-menu-item-radius: 0.25rem;
163
+ }
@@ -0,0 +1,23 @@
1
+ /* Padding */
2
+ .pad-1 { padding: var(--ui-spacing); }
3
+ .pad-2 { padding: calc(var(--ui-spacing) * 2); }
4
+ .pad-3 { padding: calc(var(--ui-spacing) * 3); }
5
+ .pad-4 { padding: calc(var(--ui-spacing) * 4); }
6
+
7
+ /* Text size */
8
+ .text-xs { font-size: 0.75rem; }
9
+ .text-sm { font-size: 0.875rem; }
10
+ .text-base { font-size: 1rem; }
11
+ .text-lg { font-size: 1.125rem; }
12
+ .text-xl { font-size: 1.25rem; }
13
+
14
+ /* Text color */
15
+ .text-muted { color: var(--ui-muted-color); }
16
+
17
+ /* Fixed positioning */
18
+ .cg-theme-toggle {
19
+ position: fixed;
20
+ bottom: 1.5rem;
21
+ right: 1.5rem;
22
+ z-index: 50;
23
+ }
@@ -0,0 +1,219 @@
1
+ module Aeno
2
+ class ApplicationViewComponent < ViewComponentContrib::Base
3
+ extend(Dry::Initializer)
4
+ include(Turbo::StreamsHelper)
5
+ include(Turbo::FramesHelper)
6
+ include(Aeno::ApplicationHelper)
7
+ include(LucideRails::RailsHelper)
8
+
9
+ # ═══════════════════════════════════════════════════════════════════════════
10
+ # PROPS DSL
11
+ # ═══════════════════════════════════════════════════════════════════════════
12
+
13
+ def self.prop_definitions
14
+ @prop_definitions ||= {}
15
+ end
16
+
17
+ def self.prop(name, description:, values: nil, **opts)
18
+ prop_definitions[name] = { description:, values: }.compact
19
+ option(name, **opts)
20
+ end
21
+
22
+ def self.props
23
+ ancestors
24
+ .select { |a| a.respond_to?(:prop_definitions) }
25
+ .reverse
26
+ .reduce({}) { |h, a| h.merge(a.prop_definitions) }
27
+ end
28
+
29
+ # ═══════════════════════════════════════════════════════════════════════════
30
+ # EXAMPLES DSL
31
+ # ═══════════════════════════════════════════════════════════════════════════
32
+
33
+ class ExamplePreview
34
+ attr_reader :props
35
+
36
+ def initialize(props)
37
+ @props = props
38
+ end
39
+ end
40
+
41
+ class Example
42
+ attr_reader :key, :title, :description, :previews
43
+
44
+ def initialize(key, title:, description: nil)
45
+ @key = key
46
+ @title = title
47
+ @description = description
48
+ @previews = []
49
+ end
50
+
51
+ def preview(**props)
52
+ @previews << ExamplePreview.new(props)
53
+ end
54
+ end
55
+
56
+ class ExamplesBuilder
57
+ attr_reader :examples
58
+
59
+ def initialize
60
+ @examples = []
61
+ end
62
+
63
+ def example(key, title:, description: nil, &block)
64
+ ex = Example.new(key, title: title, description: description)
65
+ block.call(ex) if block
66
+ @examples << ex
67
+ end
68
+ end
69
+
70
+ def self.examples_config
71
+ @examples_config ||= { title: nil, description: nil, examples: [] }
72
+ end
73
+
74
+ def self.examples(title, description: nil, &block)
75
+ @examples_config = { title: title, description: description, examples: [] }
76
+ builder = ExamplesBuilder.new
77
+ block.call(builder) if block
78
+ @examples_config[:examples] = builder.examples
79
+ end
80
+
81
+ def self.examples_title
82
+ examples_config[:title]
83
+ end
84
+
85
+ def self.examples_description
86
+ examples_config[:description]
87
+ end
88
+
89
+ def self.examples_list
90
+ examples_config[:examples]
91
+ end
92
+
93
+ option(:css, optional: true)
94
+ option(:style, optional: true)
95
+
96
+ # Override in components: def default_style = { height: "300px" }
97
+ def default_style = {}
98
+
99
+ # Hash - for passing to child components
100
+ def style_hash
101
+ @style_hash ||= default_style.merge(style || {}).compact
102
+ end
103
+
104
+ # String - for HTML style attribute
105
+ def merged_style
106
+ @merged_style ||= style_hash
107
+ .map { |k, v| "#{k.to_s.dasherize}: #{v}" }
108
+ .join("; ")
109
+ .presence
110
+ end
111
+
112
+ class << self
113
+ def named
114
+ @named ||= self.name.sub(/::Component$/, "").underscore.split("/").join("--").gsub("_", "-")
115
+ end
116
+
117
+ PREFIXES = {
118
+ "Primitives" => "cp",
119
+ "Blocks" => "cb",
120
+ "Pages" => "cg"
121
+ }.freeze
122
+
123
+ # Component identifier for CSS scoping (e.g., "cp-card", "cb-sidebar")
124
+ # Note: Can't use @identifier - ViewComponent uses it for file paths
125
+ def css_identifier
126
+ parts = self.name.to_s.sub(/::Component$/, "").split("::")
127
+ prefix = PREFIXES[parts[-2]] || "c"
128
+ component = parts.last.underscore.gsub("_", "-")
129
+ "#{prefix}-#{component}"
130
+ end
131
+ end
132
+
133
+ # Generate scoped CSS class name (e.g., "cp-card", "cp-card--centered")
134
+ def class_for(name = nil)
135
+ return self.class.css_identifier if name.nil? || name == "base"
136
+ "#{self.class.css_identifier}--#{name}"
137
+ end
138
+
139
+ # Build classes from modifiers
140
+ # Usage: classes(variant:, size:, disabled:, full:)
141
+ # - symbol values: class_for(value) e.g. variant: :default → cp-button--default
142
+ # - true: class_for(key) e.g. disabled: true → cp-button--disabled
143
+ # - false/nil: skipped
144
+ def classes(**mods)
145
+ [
146
+ class_for,
147
+ *mods.filter_map { |k, v| v && class_for(v == true ? k.to_s : v.to_s) },
148
+ css
149
+ ].join(" ")
150
+ end
151
+
152
+ def controller_name
153
+ # Match JS autoload naming for components/controllers:
154
+ # - aeno/components/button/button_controller -> aeno--button
155
+ name = self.class.name
156
+ .sub(/^Aeno::/, "")
157
+ .sub(/::Component$/, "")
158
+ .underscore
159
+
160
+ "aeno--#{name.gsub('/', '--').gsub('_', '-')}"
161
+ end
162
+
163
+ def data_target_key
164
+ "#{controller_name}-target"
165
+ end
166
+
167
+ # Helper methods for Stimulus attributes
168
+ # These return keys suited for Rails `data:` hashes (no `data-` prefix)
169
+ def stimulus_controller
170
+ { controller: controller_name }
171
+ end
172
+
173
+ def stimulus_target(name)
174
+ { "#{controller_name}-target" => name }
175
+ end
176
+
177
+ def stimulus_action(event, method = nil)
178
+ method ||= event
179
+ { action: "#{event}->#{controller_name}##{method}" }
180
+ end
181
+
182
+ def stimulus_value(name, value)
183
+ { "#{controller_name}-#{name}-value" => value }
184
+ end
185
+
186
+ def stimulus_class(name, css_class)
187
+ { "#{controller_name}-#{name}-class" => css_class }
188
+ end
189
+
190
+ # Attribute helpers for raw tag helpers (already include `data-` prefix)
191
+ def stimulus_attr_target(name)
192
+ { "data-#{controller_name}-target" => name }
193
+ end
194
+
195
+ def merged_data
196
+ return default_data unless respond_to?(:data) && data.keys
197
+
198
+ data.merge(**default_data)
199
+ end
200
+
201
+ def default_data
202
+ { controller: controller_name }
203
+ end
204
+
205
+ # Shared helper for rendering clickable elements
206
+ # - button_to for non-GET methods (creates form)
207
+ # - link_to for GET links
208
+ # - button_tag for action buttons
209
+ def action_tag(href: nil, method: nil, data: {}, form_data: {}, **opts, &block)
210
+ if method && href
211
+ helpers.button_to(href, method: method, data: data, form: { data: form_data }, **opts, &block)
212
+ elsif href
213
+ helpers.link_to(href, data: data, **opts, &block)
214
+ else
215
+ helpers.button_tag(type: "button", data: data, **opts, &block)
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,7 @@
1
+ <div class="cb-component-preview">
2
+ <% if title %><h2 class="cb-component-preview__title"><%= title %></h2><% end %>
3
+ <% if description %><p class="cb-component-preview__description text-muted"><%= description %></p><% end %>
4
+ <%= ui("card", style: style_hash, centered: true) do %>
5
+ <%= content %>
6
+ <% end %>
7
+ </div>
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aeno::Blocks::ComponentPreview
4
+ class Component < ::Aeno::ApplicationViewComponent
5
+ option(:title, optional: true)
6
+ option(:description, optional: true)
7
+
8
+ def default_style = { height: "300px" }
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ .cb-component-preview {
2
+ &__title {
3
+ font-size: 0.875rem;
4
+ font-weight: 600;
5
+ color: var(--ui-muted-color);
6
+ margin-bottom: 0.75rem;
7
+ text-transform: uppercase;
8
+ letter-spacing: 0.05em;
9
+ }
10
+ }
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Aeno::FormBuilder < ActionView::Helpers::FormBuilder
4
+ class BaseComponent < Aeno::ApplicationViewComponent
5
+ INPUT_BASE_CLASSES = %w[
6
+ block
7
+ w-full
8
+ rounded-input
9
+ bg-input-bg
10
+ px-3
11
+ py-2
12
+ text-sm
13
+ text-input-text
14
+ placeholder:text-input-placeholder
15
+ outline-none
16
+ border
17
+ border-input-border
18
+ focus:border-input-border-focus
19
+ focus:ring-1
20
+ focus:ring-input-ring
21
+ shadow-sm
22
+ ].freeze
23
+
24
+ option(:id, optional: true)
25
+ option(:name)
26
+ option(:value, optional: true)
27
+ option(:disabled, default: proc { false })
28
+ option(:label, optional: true)
29
+ option(:type, optional: true)
30
+ option(:helper_text, optional: true)
31
+ option(:error_text, optional: true)
32
+ option(:placeholder, optional: true)
33
+ option(:required, default: proc { false })
34
+ option(:data, default: proc { {} })
35
+ end
36
+
37
+ def field(field_type, name, options = {}, &block)
38
+ klass = "::Aeno::Primitives::Input#{field_type.to_s.camelize}::Component".constantize
39
+ resolved_name = resolve_name(name)
40
+ error_text = @object&.errors&.[](name)&.first
41
+
42
+ value = options[:value] || @object&.send(name)
43
+
44
+ merged_options = options.merge(
45
+ id: options[:id] || "#{@object_name}_#{name}".parameterize(separator: "_"),
46
+ name: resolved_name,
47
+ error_text:,
48
+ value:
49
+ )
50
+
51
+ @template.render(klass.new(**merged_options), &block)
52
+ end
53
+
54
+ def text_field(name, options = {})
55
+ field(:text, name, options)
56
+ end
57
+
58
+ def password_field(name, options = {})
59
+ field(:password, name, options.merge(type: "password"))
60
+ end
61
+
62
+ def select_field(name, options = {}, &block)
63
+ field(:select, name, options, &block)
64
+ end
65
+
66
+ def text_area_field(name, options = {})
67
+ field(:text_area, name, options)
68
+ end
69
+
70
+ def tagging_field(name, options = {})
71
+ field(:tagging, name, options)
72
+ end
73
+
74
+ def attachments_field(name, options = {})
75
+ field(:attachments, name, options)
76
+ end
77
+
78
+ def text_area_ai_field(name, options = {})
79
+ field(:text_area_ai, name, options)
80
+ end
81
+
82
+ private
83
+
84
+ def resolve_name(name)
85
+ @object_name ? "#{@object_name}[#{name}]" : name
86
+ end
87
+ end
@@ -0,0 +1,53 @@
1
+ <div class="cg-showcase">
2
+ <h1>Button</h1>
3
+
4
+ <%= block("component_preview", title: "Preview") do %>
5
+ <%= ui("button", label: "Click me", variant: :default) %>
6
+ <% end %>
7
+
8
+ <%= block("component_preview", title: "Variants") do %>
9
+ <%= ui("button", label: "Default", variant: :default) %>
10
+ <%= ui("button", label: "Secondary", variant: :secondary) %>
11
+ <%= ui("button", label: "Destructive", variant: :destructive) %>
12
+ <%= ui("button", label: "Outline", variant: :outline) %>
13
+ <%= ui("button", label: "Ghost", variant: :ghost) %>
14
+ <%= ui("button", label: "Link", variant: :link) %>
15
+ <% end %>
16
+
17
+ <%= block("component_preview", title: "Sizes") do %>
18
+ <%= ui("button", label: "XSmall", size: :xsmall) %>
19
+ <%= ui("button", label: "Small", size: :small) %>
20
+ <%= ui("button", label: "Default") %>
21
+ <%= ui("button", label: "Large", size: :large) %>
22
+ <% end %>
23
+
24
+ <%= block("component_preview", title: "With Icons") do %>
25
+ <%= ui("button", label: "Settings", icon: "settings") %>
26
+ <%= ui("button", label: "Download", icon: "download", variant: :secondary) %>
27
+ <%= ui("button", label: "Delete", icon: "trash-2", variant: :destructive) %>
28
+ <% end %>
29
+
30
+ <%= ui("card") do %>
31
+ <h2>Props</h2>
32
+ <%= ui("card") do %>
33
+ <%= ui("table") do |t| %>
34
+ <% t.with_header do |h| %>
35
+ <% h.with_column do %>Name<% end %>
36
+ <% h.with_column do %>Description<% end %>
37
+ <% h.with_column do %>Values<% end %>
38
+ <% end %>
39
+ <% component_class(:button).props.each do |name, meta| %>
40
+ <% t.with_row do |r| %>
41
+ <% r.with_cell do %><code><%= name %></code><% end %>
42
+ <% r.with_cell do %><%= meta[:description] %><% end %>
43
+ <% r.with_cell do %>
44
+ <% if meta[:values] %>
45
+ <% meta[:values].each do |v| %><code><%= v %></code> <% end %>
46
+ <% end %>
47
+ <% end %>
48
+ <% end %>
49
+ <% end %>
50
+ <% end %>
51
+ <% end %>
52
+ <% end %>
53
+ </div>
@@ -0,0 +1,7 @@
1
+ module Aeno::Pages::Showcase::Index
2
+ class Component < ::Aeno::ApplicationViewComponent
3
+ def component_class(name)
4
+ "Aeno::Primitives::#{name.to_s.camelize}::Component".constantize
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ .cg-showcase {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 1.5rem;
5
+ max-width: 900px;
6
+
7
+ & h1 {
8
+ font-size: 2rem;
9
+ font-weight: 700;
10
+ }
11
+
12
+ & h2 {
13
+ font-size: 0.875rem;
14
+ font-weight: 600;
15
+ color: var(--ui-muted-color);
16
+ margin-bottom: 1rem;
17
+ text-transform: uppercase;
18
+ letter-spacing: 0.05em;
19
+ }
20
+
21
+ &__row {
22
+ display: flex;
23
+ flex-wrap: wrap;
24
+ gap: 0.5rem;
25
+ align-items: center;
26
+ }
27
+ }
@@ -0,0 +1,7 @@
1
+ <div class="cg-showcase cg-showcase--not-ready">
2
+ <div class="cg-showcase__empty">
3
+ <%= lucide_icon("construction", class: "cg-showcase__empty-icon") %>
4
+ <h1><%= component_name %></h1>
5
+ <p class="text-muted">Documentation not ready yet.</p>
6
+ </div>
7
+ </div>
@@ -0,0 +1,10 @@
1
+ module Aeno::Pages::Showcase::Placeholder
2
+ class Component < ::Aeno::ApplicationViewComponent
3
+ option :namespace
4
+ option :id
5
+
6
+ def component_name
7
+ id.to_s.titleize
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,38 @@
1
+ <div class="cg-showcase">
2
+ <h1><%= title %></h1>
3
+ <% if description %>
4
+ <p class="text-muted"><%= description %></p>
5
+ <% end %>
6
+
7
+ <% examples.each do |example| %>
8
+ <%= block("component_preview", title: example.title, description: example.description) do %>
9
+ <% example.previews.each do |preview| %>
10
+ <%= render_component(**preview.props) %>
11
+ <% end %>
12
+ <% end %>
13
+ <% end %>
14
+
15
+ <%= ui("card") do %>
16
+ <h2>Props</h2>
17
+ <%= ui("card") do %>
18
+ <%= ui("table") do |t| %>
19
+ <% t.with_header do |h| %>
20
+ <% h.with_column do %>Name<% end %>
21
+ <% h.with_column do %>Description<% end %>
22
+ <% h.with_column do %>Values<% end %>
23
+ <% end %>
24
+ <% props.each do |name, meta| %>
25
+ <% t.with_row do |r| %>
26
+ <% r.with_cell do %><code><%= name %></code><% end %>
27
+ <% r.with_cell do %><%= meta[:description] %><% end %>
28
+ <% r.with_cell do %>
29
+ <% if meta[:values] %>
30
+ <% meta[:values].each do |v| %><code><%= v %></code> <% end %>
31
+ <% end %>
32
+ <% end %>
33
+ <% end %>
34
+ <% end %>
35
+ <% end %>
36
+ <% end %>
37
+ <% end %>
38
+ </div>