senren-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +33 -0
- data/CONTRIBUTING.md +63 -0
- data/LICENSE +21 -0
- data/README.md +135 -0
- data/Rakefile +22 -0
- data/docs/visual_style.md +51 -0
- data/lib/generators/senren/component/component_generator.rb +62 -0
- data/lib/generators/senren/component/templates/component.html.erb.tt +3 -0
- data/lib/generators/senren/component/templates/component.rb.tt +13 -0
- data/lib/generators/senren/component/templates/component_test.rb.tt +16 -0
- data/lib/generators/senren/component/templates/controller.js.tt +23 -0
- data/lib/generators/senren/component/templates/system_test.rb.tt +7 -0
- data/lib/generators/senren/install/install_generator.rb +67 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +45 -0
- data/lib/generators/senren/install/templates/conventions.md.tt +66 -0
- data/lib/generators/senren/install/templates/installed_components.yml.tt +4 -0
- data/lib/generators/senren/install/templates/senren.css.tt +164 -0
- data/lib/senren/rails/component_copier.rb +111 -0
- data/lib/senren/rails/doctor.rb +86 -0
- data/lib/senren/rails/engine.rb +16 -0
- data/lib/senren/rails/host_paths.rb +36 -0
- data/lib/senren/rails/installer.rb +83 -0
- data/lib/senren/rails/llms_writer.rb +149 -0
- data/lib/senren/rails/registry.rb +161 -0
- data/lib/senren/rails/skill_writer.rb +166 -0
- data/lib/senren/rails/version.rb +7 -0
- data/lib/senren/rails.rb +39 -0
- data/lib/tasks/senren.rake +74 -0
- data/registry/components.yml +1053 -0
- data/registry/groups.yml +25 -0
- data/registry/recipes.yml +79 -0
- data/templates/components/accordion/accordion_component.html.erb +16 -0
- data/templates/components/accordion/accordion_component.rb +31 -0
- data/templates/components/activity_feed/activity_feed_component.html.erb +22 -0
- data/templates/components/activity_feed/activity_feed_component.rb +19 -0
- data/templates/components/alert/alert_component.html.erb +9 -0
- data/templates/components/alert/alert_component.rb +18 -0
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +34 -0
- data/templates/components/alert_dialog/alert_dialog_component.rb +21 -0
- data/templates/components/api_key_field/api_key_field_component.html.erb +13 -0
- data/templates/components/api_key_field/api_key_field_component.rb +20 -0
- data/templates/components/app_shell/app_shell_component.html.erb +28 -0
- data/templates/components/app_shell/app_shell_component.rb +24 -0
- data/templates/components/aspect_ratio/aspect_ratio_component.html.erb +3 -0
- data/templates/components/aspect_ratio/aspect_ratio_component.rb +14 -0
- data/templates/components/avatar/avatar_component.html.erb +27 -0
- data/templates/components/avatar/avatar_component.rb +30 -0
- data/templates/components/badge/badge_component.html.erb +1 -0
- data/templates/components/badge/badge_component.rb +16 -0
- data/templates/components/billing_plan_card/billing_plan_card_component.html.erb +28 -0
- data/templates/components/billing_plan_card/billing_plan_card_component.rb +27 -0
- data/templates/components/breadcrumb/breadcrumb_component.html.erb +23 -0
- data/templates/components/breadcrumb/breadcrumb_component.rb +30 -0
- data/templates/components/bulk_action_bar/bulk_action_bar_component.html.erb +12 -0
- data/templates/components/bulk_action_bar/bulk_action_bar_component.rb +24 -0
- data/templates/components/button/button_component.html.erb +6 -0
- data/templates/components/button/button_component.rb +29 -0
- data/templates/components/calendar/calendar_component.html.erb +21 -0
- data/templates/components/calendar/calendar_component.rb +30 -0
- data/templates/components/card/card_component.html.erb +13 -0
- data/templates/components/card/card_component.rb +17 -0
- data/templates/components/carousel/carousel_component.html.erb +68 -0
- data/templates/components/carousel/carousel_component.rb +34 -0
- data/templates/components/checkbox/checkbox_component.html.erb +8 -0
- data/templates/components/checkbox/checkbox_component.rb +19 -0
- data/templates/components/checkbox_group/checkbox_group_component.html.erb +10 -0
- data/templates/components/checkbox_group/checkbox_group_component.rb +30 -0
- data/templates/components/clipboard/clipboard_component.html.erb +7 -0
- data/templates/components/clipboard/clipboard_component.rb +17 -0
- data/templates/components/codeblock/codeblock_component.html.erb +11 -0
- data/templates/components/codeblock/codeblock_component.rb +31 -0
- data/templates/components/collapsible/collapsible_component.html.erb +9 -0
- data/templates/components/collapsible/collapsible_component.rb +19 -0
- data/templates/components/combobox/combobox_component.html.erb +19 -0
- data/templates/components/combobox/combobox_component.rb +38 -0
- data/templates/components/command/command_component.html.erb +22 -0
- data/templates/components/command/command_component.rb +38 -0
- data/templates/components/context_menu/context_menu_component.html.erb +11 -0
- data/templates/components/context_menu/context_menu_component.rb +11 -0
- data/templates/components/data_table/data_table_component.html.erb +50 -0
- data/templates/components/data_table/data_table_component.rb +42 -0
- data/templates/components/date_picker/date_picker_component.html.erb +5 -0
- data/templates/components/date_picker/date_picker_component.rb +21 -0
- data/templates/components/dialog/dialog_component.html.erb +38 -0
- data/templates/components/dialog/dialog_component.rb +22 -0
- data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +12 -0
- data/templates/components/dropdown_menu/dropdown_menu_component.rb +36 -0
- data/templates/components/empty_state/empty_state_component.html.erb +18 -0
- data/templates/components/empty_state/empty_state_component.rb +22 -0
- data/templates/components/filter_bar/filter_bar_component.html.erb +5 -0
- data/templates/components/filter_bar/filter_bar_component.rb +15 -0
- data/templates/components/form/form_component.html.erb +3 -0
- data/templates/components/form/form_component.rb +18 -0
- data/templates/components/hover_card/hover_card_component.html.erb +10 -0
- data/templates/components/hover_card/hover_card_component.rb +11 -0
- data/templates/components/input/input_component.html.erb +1 -0
- data/templates/components/input/input_component.rb +28 -0
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +35 -0
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +26 -0
- data/templates/components/label/label_component.html.erb +4 -0
- data/templates/components/label/label_component.rb +19 -0
- data/templates/components/link/link_component.html.erb +1 -0
- data/templates/components/link/link_component.rb +25 -0
- data/templates/components/masked_input/masked_input_component.html.erb +1 -0
- data/templates/components/masked_input/masked_input_component.rb +18 -0
- data/templates/components/native_select/native_select_component.html.erb +14 -0
- data/templates/components/native_select/native_select_component.rb +52 -0
- data/templates/components/page_header/page_header_component.html.erb +20 -0
- data/templates/components/page_header/page_header_component.rb +19 -0
- data/templates/components/pagination/pagination_component.html.erb +11 -0
- data/templates/components/pagination/pagination_component.rb +24 -0
- data/templates/components/popover/popover_component.html.erb +9 -0
- data/templates/components/popover/popover_component.rb +11 -0
- data/templates/components/progress/progress_component.html.erb +11 -0
- data/templates/components/progress/progress_component.rb +26 -0
- data/templates/components/radio_button/radio_button_component.html.erb +8 -0
- data/templates/components/radio_button/radio_button_component.rb +19 -0
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +32 -0
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +30 -0
- data/templates/components/search_input/search_input_component.html.erb +14 -0
- data/templates/components/search_input/search_input_component.rb +18 -0
- data/templates/components/select/select_component.html.erb +1 -0
- data/templates/components/select/select_component.rb +19 -0
- data/templates/components/separator/separator_component.html.erb +1 -0
- data/templates/components/separator/separator_component.rb +12 -0
- data/templates/components/settings_section/settings_section_component.html.erb +20 -0
- data/templates/components/settings_section/settings_section_component.rb +18 -0
- data/templates/components/sheet/sheet_component.html.erb +37 -0
- data/templates/components/sheet/sheet_component.rb +27 -0
- data/templates/components/shortcut_key/shortcut_key_component.html.erb +6 -0
- data/templates/components/shortcut_key/shortcut_key_component.rb +15 -0
- data/templates/components/sidebar/sidebar_component.html.erb +14 -0
- data/templates/components/sidebar/sidebar_component.rb +37 -0
- data/templates/components/skeleton/skeleton_component.html.erb +1 -0
- data/templates/components/skeleton/skeleton_component.rb +13 -0
- data/templates/components/stat_card/stat_card_component.html.erb +20 -0
- data/templates/components/stat_card/stat_card_component.rb +31 -0
- data/templates/components/switch/switch_component.html.erb +11 -0
- data/templates/components/switch/switch_component.rb +19 -0
- data/templates/components/table/table_component.html.erb +26 -0
- data/templates/components/table/table_component.rb +35 -0
- data/templates/components/tabs/tabs_component.html.erb +18 -0
- data/templates/components/tabs/tabs_component.rb +35 -0
- data/templates/components/team_member_list/team_member_list_component.html.erb +22 -0
- data/templates/components/team_member_list/team_member_list_component.rb +26 -0
- data/templates/components/textarea/textarea_component.html.erb +1 -0
- data/templates/components/textarea/textarea_component.rb +23 -0
- data/templates/components/theme_toggle/theme_toggle_component.html.erb +4 -0
- data/templates/components/theme_toggle/theme_toggle_component.rb +15 -0
- data/templates/components/tooltip/tooltip_component.html.erb +9 -0
- data/templates/components/tooltip/tooltip_component.rb +16 -0
- data/templates/components/top_nav/top_nav_component.html.erb +21 -0
- data/templates/components/top_nav/top_nav_component.rb +44 -0
- data/templates/components/typography/typography_component.html.erb +1 -0
- data/templates/components/typography/typography_component.rb +24 -0
- data/templates/controllers/accordion_controller.js +27 -0
- data/templates/controllers/alert_dialog_controller.js +38 -0
- data/templates/controllers/api_key_field_controller.js +36 -0
- data/templates/controllers/calendar_controller.js +16 -0
- data/templates/controllers/carousel_controller.js +50 -0
- data/templates/controllers/clipboard_controller.js +17 -0
- data/templates/controllers/collapsible_controller.js +13 -0
- data/templates/controllers/combobox_controller.js +64 -0
- data/templates/controllers/command_controller.js +80 -0
- data/templates/controllers/context_menu_controller.js +36 -0
- data/templates/controllers/data_table_controller.js +34 -0
- data/templates/controllers/date_picker_controller.js +17 -0
- data/templates/controllers/dialog_controller.js +50 -0
- data/templates/controllers/dropdown_menu_controller.js +92 -0
- data/templates/controllers/hover_card_controller.js +17 -0
- data/templates/controllers/invite_member_dialog_controller.js +28 -0
- data/templates/controllers/masked_input_controller.js +30 -0
- data/templates/controllers/popover_controller.js +42 -0
- data/templates/controllers/rich_text_editor_lite_controller.js +443 -0
- data/templates/controllers/select_controller.js +10 -0
- data/templates/controllers/sheet_controller.js +34 -0
- data/templates/controllers/sidebar_controller.js +10 -0
- data/templates/controllers/tabs_controller.js +41 -0
- data/templates/controllers/theme_toggle_controller.js +24 -0
- data/templates/controllers/tooltip_controller.js +10 -0
- metadata +257 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
module Rails
|
|
5
|
+
# Loads, validates, and queries the Senren component registry.
|
|
6
|
+
#
|
|
7
|
+
# reg = Senren::Rails::Registry.load!
|
|
8
|
+
# reg.find("button") # => Component struct
|
|
9
|
+
# reg.dependencies("dialog") # => [<button>]
|
|
10
|
+
# reg.group("forms") # => [<form>, <input>, ...]
|
|
11
|
+
class Registry
|
|
12
|
+
include Enumerable
|
|
13
|
+
|
|
14
|
+
REQUIRED_KEYS = %w[category client can_have_client files depends_on pairs_with variants accessibility ai].freeze
|
|
15
|
+
VALID_CATEGORIES = %w[actions forms overlays navigation layout data saas rich].freeze
|
|
16
|
+
|
|
17
|
+
Component = Struct.new(
|
|
18
|
+
:name, :category, :client, :can_have_client, :controller, :stub,
|
|
19
|
+
:files, :depends_on, :pairs_with, :variants, :accessibility,
|
|
20
|
+
:use_for, :avoid,
|
|
21
|
+
keyword_init: true
|
|
22
|
+
) do
|
|
23
|
+
def stub? = stub == true
|
|
24
|
+
def client? = client == true
|
|
25
|
+
|
|
26
|
+
def to_h_full
|
|
27
|
+
to_h.merge(stub: stub?, client: client?)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attr_reader :components, :groups, :recipes
|
|
32
|
+
|
|
33
|
+
def self.load!(
|
|
34
|
+
components_path: Senren::Rails.registry_path,
|
|
35
|
+
groups_path: Senren::Rails.groups_path,
|
|
36
|
+
recipes_path: Senren::Rails.recipes_path
|
|
37
|
+
)
|
|
38
|
+
new(
|
|
39
|
+
YAML.safe_load_file(components_path, aliases: false),
|
|
40
|
+
YAML.safe_load_file(groups_path, aliases: false),
|
|
41
|
+
YAML.safe_load_file(recipes_path, aliases: false)
|
|
42
|
+
).tap(&:validate!)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(components_yaml, groups_yaml, recipes_yaml)
|
|
46
|
+
@components = parse_components(components_yaml)
|
|
47
|
+
@groups = (groups_yaml || {}).fetch('groups', [])
|
|
48
|
+
@recipes = (recipes_yaml || {}).fetch('recipes', {})
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def find(name)
|
|
52
|
+
@components[name.to_s]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fetch(name)
|
|
56
|
+
find(name) or raise ArgumentError, "Unknown component: #{name.inspect}. " \
|
|
57
|
+
"Known: #{@components.keys.sort.join(', ')}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def all
|
|
61
|
+
@components.values
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def each(&block)
|
|
65
|
+
all.each(&block)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def find_each(&block)
|
|
69
|
+
each(&block)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def names
|
|
73
|
+
@components.keys
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def group(category_id)
|
|
77
|
+
@components.values.select { |c| c.category == category_id.to_s }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def recipe(id)
|
|
81
|
+
@recipes.fetch(id.to_s) { raise ArgumentError, "Unknown recipe: #{id}" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns the transitive dependency closure for one or more components,
|
|
85
|
+
# in install order (deps first), de-duplicated.
|
|
86
|
+
def dependencies(*requested)
|
|
87
|
+
requested = requested.flatten.map(&:to_s)
|
|
88
|
+
result = []
|
|
89
|
+
visiting = []
|
|
90
|
+
|
|
91
|
+
visit = lambda do |name|
|
|
92
|
+
return if result.include?(name)
|
|
93
|
+
raise "Circular dependency: #{(visiting + [name]).join(' -> ')}" if visiting.include?(name)
|
|
94
|
+
|
|
95
|
+
visiting << name
|
|
96
|
+
comp = fetch(name)
|
|
97
|
+
comp.depends_on.each { |dep| visit.call(dep) }
|
|
98
|
+
visiting.pop
|
|
99
|
+
result << name
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
requested.each { |name| visit.call(name) }
|
|
103
|
+
result
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate!
|
|
107
|
+
errors = []
|
|
108
|
+
|
|
109
|
+
validate_components(errors)
|
|
110
|
+
validate_recipes(errors)
|
|
111
|
+
|
|
112
|
+
raise "Registry validation failed:\n - #{errors.join("\n - ")}" if errors.any?
|
|
113
|
+
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def validate_components(errors)
|
|
120
|
+
@components.each do |name, comp|
|
|
121
|
+
errors << "#{name}: invalid category #{comp.category.inspect}" unless VALID_CATEGORIES.include?(comp.category)
|
|
122
|
+
comp.depends_on.each do |dep|
|
|
123
|
+
errors << "#{name}: depends_on unknown component #{dep.inspect}" unless @components.key?(dep)
|
|
124
|
+
end
|
|
125
|
+
errors << "#{name}: client=true but can_have_client=false" if comp.client && !comp.can_have_client
|
|
126
|
+
errors << "#{name}: client=true requires a controller identifier" if comp.client && comp.controller.nil?
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_recipes(errors)
|
|
131
|
+
@recipes.each do |id, recipe|
|
|
132
|
+
recipe.fetch('components', []).each do |component|
|
|
133
|
+
errors << "recipe #{id}: unknown component #{component.inspect}" unless @components.key?(component)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def parse_components(yaml)
|
|
139
|
+
raw = (yaml || {}).fetch('components', {})
|
|
140
|
+
raw.each_with_object({}) do |(name, data), hash|
|
|
141
|
+
ai = data.fetch('ai', {})
|
|
142
|
+
hash[name] = Component.new(
|
|
143
|
+
name: name,
|
|
144
|
+
category: data.fetch('category'),
|
|
145
|
+
client: data.fetch('client', false),
|
|
146
|
+
can_have_client: data.fetch('can_have_client', false),
|
|
147
|
+
controller: data['controller'],
|
|
148
|
+
stub: data.fetch('stub', false),
|
|
149
|
+
files: Array(data['files']),
|
|
150
|
+
depends_on: Array(data['depends_on']),
|
|
151
|
+
pairs_with: Array(data['pairs_with']),
|
|
152
|
+
variants: Array(data['variants']),
|
|
153
|
+
accessibility: Array(data['accessibility']),
|
|
154
|
+
use_for: Array(ai['use_for']),
|
|
155
|
+
avoid: Array(ai['avoid'])
|
|
156
|
+
).freeze
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Senren
|
|
6
|
+
module Rails
|
|
7
|
+
# Writes the centralized .senren/skill.md file for AI agents.
|
|
8
|
+
#
|
|
9
|
+
# The file contains a generated region delimited by HTML comments:
|
|
10
|
+
#
|
|
11
|
+
# <!-- senren:skill:start -->
|
|
12
|
+
# ...generated by SkillWriter...
|
|
13
|
+
# <!-- senren:skill:end -->
|
|
14
|
+
#
|
|
15
|
+
# Anything outside that region is preserved across regeneration.
|
|
16
|
+
class SkillWriter
|
|
17
|
+
START_MARKER = '<!-- senren:skill:start -->'
|
|
18
|
+
END_MARKER = '<!-- senren:skill:end -->'
|
|
19
|
+
|
|
20
|
+
GROUP_ORDER = %w[actions forms overlays navigation layout data saas rich].freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :registry, :paths
|
|
23
|
+
|
|
24
|
+
def initialize(registry: Registry.load!, paths: HostPaths.new)
|
|
25
|
+
@registry = registry
|
|
26
|
+
@paths = paths
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sync!
|
|
30
|
+
installed = installed_names
|
|
31
|
+
body = render(installed)
|
|
32
|
+
|
|
33
|
+
paths.senren_dir.mkpath
|
|
34
|
+
existing = paths.skill_file.exist? ? paths.skill_file.read : default_outer_template
|
|
35
|
+
|
|
36
|
+
new_content = inject(existing, body)
|
|
37
|
+
paths.skill_file.write(new_content)
|
|
38
|
+
paths.skill_file
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def installed_names
|
|
44
|
+
path = paths.installed_components
|
|
45
|
+
return [] unless path.exist?
|
|
46
|
+
|
|
47
|
+
ledger = YAML.safe_load_file(path) || {}
|
|
48
|
+
Array(ledger['installed']).filter_map { |e| e['name'] }.uniq
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render(installed_names)
|
|
52
|
+
if installed_names.empty?
|
|
53
|
+
return '_No Senren components installed yet. Run `bin/rails senren:add <name>...` to install components._'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
lines = []
|
|
57
|
+
installed = installed_names.filter_map { |n| registry.find(n) }
|
|
58
|
+
|
|
59
|
+
registry.groups.each do |group|
|
|
60
|
+
gid = group['id']
|
|
61
|
+
comps = installed.select { |c| c.category == gid }
|
|
62
|
+
next if comps.empty?
|
|
63
|
+
|
|
64
|
+
lines << "## #{group['title']}"
|
|
65
|
+
lines << ''
|
|
66
|
+
lines << group['description'].to_s
|
|
67
|
+
lines << ''
|
|
68
|
+
comps.sort_by(&:name).each { |c| lines << render_component(c) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
lines.join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_component(comp)
|
|
75
|
+
out = []
|
|
76
|
+
out << "### Component: #{humanize(comp.name)}"
|
|
77
|
+
out << ''
|
|
78
|
+
out << "- **Category**: #{comp.category}"
|
|
79
|
+
out << "- **Stub**: #{comp.stub? ? 'yes (placeholder)' : 'no'}"
|
|
80
|
+
out << "- **Client behavior**: #{client_summary(comp)}"
|
|
81
|
+
out << "- **Depends on**: #{format_list(comp.depends_on)}"
|
|
82
|
+
out << "- **Pairs with**: #{format_list(comp.pairs_with)}"
|
|
83
|
+
out << "- **Variants**: #{format_list(comp.variants)}"
|
|
84
|
+
out << ''
|
|
85
|
+
out << '**Use for**:'
|
|
86
|
+
comp.use_for.each { |u| out << "- #{u}" }
|
|
87
|
+
out << ''
|
|
88
|
+
out << '**Avoid**:'
|
|
89
|
+
comp.avoid.each { |a| out << "- #{a}" }
|
|
90
|
+
out << ''
|
|
91
|
+
out << '**Accessibility**:'
|
|
92
|
+
comp.accessibility.each { |a| out << "- #{a}" }
|
|
93
|
+
out << ''
|
|
94
|
+
out << '**Rails usage**:'
|
|
95
|
+
out << ''
|
|
96
|
+
out << '```erb'
|
|
97
|
+
out << render_usage_example(comp)
|
|
98
|
+
out << '```'
|
|
99
|
+
out << ''
|
|
100
|
+
"#{out.join("\n")}\n"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def render_usage_example(comp)
|
|
104
|
+
ruby_class = "Senren::#{ruby_class_for(comp.name)}Component"
|
|
105
|
+
if comp.variants.any?
|
|
106
|
+
variant = comp.variants.first
|
|
107
|
+
%(<%= render #{ruby_class}.new(variant: :#{variant}) do %>\n ...\n<% end %>)
|
|
108
|
+
else
|
|
109
|
+
%(<%= render #{ruby_class}.new do %>\n ...\n<% end %>)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def client_summary(comp)
|
|
114
|
+
return 'none' unless comp.client?
|
|
115
|
+
|
|
116
|
+
path = "app/javascript/controllers/senren/#{comp.name}_controller.js"
|
|
117
|
+
"Stimulus `#{comp.controller}` (`#{path}`)"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def format_list(items)
|
|
121
|
+
return '_none_' if items.empty?
|
|
122
|
+
|
|
123
|
+
items.map { |i| "`#{i}`" }.join(', ')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def humanize(name)
|
|
127
|
+
name.split('_').map { |w| w[0].upcase + w[1..] }.join
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def ruby_class_for(name)
|
|
131
|
+
humanize(name)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def inject(existing, generated_body)
|
|
135
|
+
if existing.include?(START_MARKER) && existing.include?(END_MARKER)
|
|
136
|
+
before = existing.split(START_MARKER, 2).first
|
|
137
|
+
after = existing.split(END_MARKER, 2).last
|
|
138
|
+
"#{before}#{START_MARKER}\n\n#{generated_body}\n\n#{END_MARKER}#{after}"
|
|
139
|
+
else
|
|
140
|
+
"#{existing.rstrip}\n\n#{START_MARKER}\n\n#{generated_body}\n\n#{END_MARKER}\n"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def default_outer_template
|
|
145
|
+
<<~MD
|
|
146
|
+
# Senren Skill File
|
|
147
|
+
|
|
148
|
+
This file is the central guide for AI coding agents working in this app.
|
|
149
|
+
|
|
150
|
+
## Hard Rules
|
|
151
|
+
|
|
152
|
+
- Use Senren components before writing custom HTML.
|
|
153
|
+
- Use ViewComponent for reusable UI; Turbo for server state; Stimulus for local behavior only.
|
|
154
|
+
- Do not introduce React, Vue, Alpine, or any external state framework.
|
|
155
|
+
- Do not hard-code colors; use semantic Tailwind tokens like `bg-background`, `text-foreground`, `bg-primary`.
|
|
156
|
+
|
|
157
|
+
## Installed Components
|
|
158
|
+
|
|
159
|
+
The block between the markers below is generated by `bin/rails senren:skill:sync`.
|
|
160
|
+
Do not edit the generated region by hand. You may add notes outside the markers.
|
|
161
|
+
|
|
162
|
+
MD
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/senren/rails.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'senren/rails/version'
|
|
4
|
+
|
|
5
|
+
module Senren
|
|
6
|
+
module Rails
|
|
7
|
+
GEM_ROOT = File.expand_path('../..', __dir__).freeze
|
|
8
|
+
|
|
9
|
+
autoload :Registry, 'senren/rails/registry'
|
|
10
|
+
autoload :ComponentCopier, 'senren/rails/component_copier'
|
|
11
|
+
autoload :SkillWriter, 'senren/rails/skill_writer'
|
|
12
|
+
autoload :LlmsWriter, 'senren/rails/llms_writer'
|
|
13
|
+
autoload :Installer, 'senren/rails/installer'
|
|
14
|
+
autoload :Doctor, 'senren/rails/doctor'
|
|
15
|
+
autoload :HostPaths, 'senren/rails/host_paths'
|
|
16
|
+
|
|
17
|
+
def self.gem_root
|
|
18
|
+
GEM_ROOT
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.registry_path
|
|
22
|
+
File.join(GEM_ROOT, 'registry', 'components.yml')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.groups_path
|
|
26
|
+
File.join(GEM_ROOT, 'registry', 'groups.yml')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.recipes_path
|
|
30
|
+
File.join(GEM_ROOT, 'registry', 'recipes.yml')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.templates_root
|
|
34
|
+
File.join(GEM_ROOT, 'templates')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
require 'senren/rails/engine' if defined?(Rails::Engine)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'senren/rails'
|
|
4
|
+
|
|
5
|
+
namespace :senren do
|
|
6
|
+
desc 'Install one or more Senren components: rake senren:add[button,dialog] or bin/rails senren:add button dialog'
|
|
7
|
+
task :add, [:names] => :environment do |_t, args|
|
|
8
|
+
names = parse_names(args)
|
|
9
|
+
options = parse_options
|
|
10
|
+
abort 'Usage: bin/rails senren:add NAME [NAME...] [--client | --no-client]' if names.empty?
|
|
11
|
+
|
|
12
|
+
paths = Senren::Rails::HostPaths.new
|
|
13
|
+
registry = Senren::Rails::Registry.load!
|
|
14
|
+
copier = Senren::Rails::ComponentCopier.new(registry: registry, paths: paths)
|
|
15
|
+
|
|
16
|
+
installed = copier.install(names, client_override: options[:client_override], force: options[:force])
|
|
17
|
+
|
|
18
|
+
Senren::Rails::SkillWriter.new(registry: registry, paths: paths).sync!
|
|
19
|
+
Senren::Rails::LlmsWriter.new(registry: registry, paths: paths).generate!
|
|
20
|
+
|
|
21
|
+
puts "Installed: #{installed.join(', ')}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
namespace :skill do
|
|
25
|
+
desc 'Rebuild .senren/skill.md from the registry and installed_components ledger.'
|
|
26
|
+
task sync: :environment do
|
|
27
|
+
paths = Senren::Rails::HostPaths.new
|
|
28
|
+
registry = Senren::Rails::Registry.load!
|
|
29
|
+
file = Senren::Rails::SkillWriter.new(registry: registry, paths: paths).sync!
|
|
30
|
+
puts "Wrote #{file}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
namespace :llms do
|
|
35
|
+
desc 'Regenerate public/llms.txt and public/llms-full.txt.'
|
|
36
|
+
task generate: :environment do
|
|
37
|
+
paths = Senren::Rails::HostPaths.new
|
|
38
|
+
registry = Senren::Rails::Registry.load!
|
|
39
|
+
files = Senren::Rails::LlmsWriter.new(registry: registry, paths: paths).generate!
|
|
40
|
+
files.each { |f| puts "Wrote #{f}" }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
desc "Run health checks against the host app's Senren installation."
|
|
45
|
+
task doctor: :environment do
|
|
46
|
+
ok = Senren::Rails::Doctor.new(paths: Senren::Rails::HostPaths.new).run!
|
|
47
|
+
exit(1) unless ok
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Helpers ---------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
def parse_names(args)
|
|
54
|
+
raw = []
|
|
55
|
+
raw.concat(args.extras)
|
|
56
|
+
raw << args[:names] if args[:names]
|
|
57
|
+
raw.concat(ARGV.drop_while { |a| !a.start_with?('senren:') }.drop(1))
|
|
58
|
+
raw
|
|
59
|
+
.flatten
|
|
60
|
+
.flat_map { |s| s.to_s.split(/[,\s]+/) }
|
|
61
|
+
.reject { |s| s.empty? || s.start_with?('-') }
|
|
62
|
+
.uniq
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_options
|
|
66
|
+
client_override =
|
|
67
|
+
if ARGV.include?('--client')
|
|
68
|
+
true
|
|
69
|
+
elsif ARGV.include?('--no-client')
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
{ client_override: client_override, force: ARGV.include?('--force') }
|
|
74
|
+
end
|