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,164 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Senren UI - Spring Garden tokens.
|
|
3
|
+
* HSL channels are written without hsl() wrapping so Tailwind utilities like
|
|
4
|
+
* bg-[hsl(var(--senren-background))]
|
|
5
|
+
* can compose them with opacity modifiers.
|
|
6
|
+
*
|
|
7
|
+
* The palette is intentionally colorful: pond blue, sakura pink, young leaf
|
|
8
|
+
* green, iris violet, and warm paper. Components stay SaaS-practical while
|
|
9
|
+
* avoiding the black-and-white default UI look.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--senren-background: 54 100% 97%;
|
|
14
|
+
--senren-foreground: 178 62% 12%;
|
|
15
|
+
|
|
16
|
+
--senren-muted: 83 54% 91%;
|
|
17
|
+
--senren-muted-foreground: 181 24% 34%;
|
|
18
|
+
|
|
19
|
+
--senren-card: 52 100% 99%;
|
|
20
|
+
--senren-card-foreground: 178 62% 12%;
|
|
21
|
+
|
|
22
|
+
--senren-popover: 52 100% 99%;
|
|
23
|
+
--senren-popover-foreground: 178 62% 12%;
|
|
24
|
+
|
|
25
|
+
--senren-border: 174 42% 78%;
|
|
26
|
+
--senren-input: 174 42% 78%;
|
|
27
|
+
--senren-ring: 196 86% 48%;
|
|
28
|
+
|
|
29
|
+
--senren-primary: 151 74% 29%;
|
|
30
|
+
--senren-primary-foreground: 58 100% 97%;
|
|
31
|
+
|
|
32
|
+
--senren-secondary: 334 95% 89%;
|
|
33
|
+
--senren-secondary-foreground: 178 56% 18%;
|
|
34
|
+
|
|
35
|
+
--senren-accent: 194 86% 82%;
|
|
36
|
+
--senren-accent-foreground: 188 72% 18%;
|
|
37
|
+
|
|
38
|
+
--senren-destructive: 350 78% 55%;
|
|
39
|
+
--senren-destructive-foreground: 54 100% 97%;
|
|
40
|
+
|
|
41
|
+
--senren-success: 132 52% 37%;
|
|
42
|
+
--senren-success-foreground: 54 100% 97%;
|
|
43
|
+
|
|
44
|
+
--senren-warning: 44 92% 61%;
|
|
45
|
+
--senren-warning-foreground: 38 58% 16%;
|
|
46
|
+
|
|
47
|
+
--senren-radius: 0.5rem;
|
|
48
|
+
|
|
49
|
+
--senren-palette-sky: 202 88% 44%;
|
|
50
|
+
--senren-palette-sakura: 334 95% 89%;
|
|
51
|
+
--senren-palette-pond: 194 86% 82%;
|
|
52
|
+
--senren-palette-leaf: 88 58% 57%;
|
|
53
|
+
--senren-palette-iris: 255 70% 80%;
|
|
54
|
+
--senren-palette-paper: 54 100% 97%;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.dark {
|
|
58
|
+
--senren-background: 202 58% 9%;
|
|
59
|
+
--senren-foreground: 54 100% 94%;
|
|
60
|
+
|
|
61
|
+
--senren-muted: 188 34% 17%;
|
|
62
|
+
--senren-muted-foreground: 174 28% 72%;
|
|
63
|
+
|
|
64
|
+
--senren-card: 199 50% 13%;
|
|
65
|
+
--senren-card-foreground: 54 100% 94%;
|
|
66
|
+
|
|
67
|
+
--senren-popover: 199 50% 13%;
|
|
68
|
+
--senren-popover-foreground: 54 100% 94%;
|
|
69
|
+
|
|
70
|
+
--senren-border: 188 31% 25%;
|
|
71
|
+
--senren-input: 188 31% 25%;
|
|
72
|
+
--senren-ring: 194 86% 72%;
|
|
73
|
+
|
|
74
|
+
--senren-primary: 132 55% 66%;
|
|
75
|
+
--senren-primary-foreground: 166 62% 10%;
|
|
76
|
+
|
|
77
|
+
--senren-secondary: 334 74% 74%;
|
|
78
|
+
--senren-secondary-foreground: 202 58% 9%;
|
|
79
|
+
|
|
80
|
+
--senren-accent: 194 76% 63%;
|
|
81
|
+
--senren-accent-foreground: 202 58% 9%;
|
|
82
|
+
|
|
83
|
+
--senren-destructive: 350 72% 62%;
|
|
84
|
+
--senren-destructive-foreground: 54 100% 97%;
|
|
85
|
+
|
|
86
|
+
--senren-success: 132 55% 66%;
|
|
87
|
+
--senren-success-foreground: 166 62% 10%;
|
|
88
|
+
|
|
89
|
+
--senren-warning: 44 92% 68%;
|
|
90
|
+
--senren-warning-foreground: 38 58% 16%;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Convenience aliases. Components prefer Tailwind semantic utilities, but raw HTML can use these. */
|
|
94
|
+
.senren-bg { background-color: hsl(var(--senren-background)); }
|
|
95
|
+
.senren-fg { color: hsl(var(--senren-foreground)); }
|
|
96
|
+
.senren-border { border-color: hsl(var(--senren-border)); }
|
|
97
|
+
.senren-radius { border-radius: var(--senren-radius); }
|
|
98
|
+
|
|
99
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] a {
|
|
100
|
+
color: hsl(var(--senren-primary));
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
text-decoration-line: underline;
|
|
104
|
+
text-decoration-thickness: 0.08em;
|
|
105
|
+
text-underline-offset: 0.18em;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] a:hover {
|
|
109
|
+
color: hsl(var(--senren-ring));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] ul,
|
|
113
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] ol {
|
|
114
|
+
margin-block: 0.5rem;
|
|
115
|
+
padding-inline-start: 1.35rem;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] ul {
|
|
119
|
+
list-style: disc;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] ol {
|
|
123
|
+
list-style: decimal;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] li + li {
|
|
127
|
+
margin-top: 0.25rem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] p + p {
|
|
131
|
+
margin-top: 0.5rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] h1,
|
|
135
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] h2,
|
|
136
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] h3 {
|
|
137
|
+
color: hsl(var(--senren-foreground));
|
|
138
|
+
font-weight: 700;
|
|
139
|
+
line-height: 1.2;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] h1 {
|
|
143
|
+
font-size: 1.5rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] h2 {
|
|
147
|
+
font-size: 1.25rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] h3 {
|
|
151
|
+
font-size: 1.125rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] :is(p, h1, h2, h3, li)[data-align="center"] {
|
|
155
|
+
text-align: center;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] :is(p, h1, h2, h3, li)[data-align="right"] {
|
|
159
|
+
text-align: right;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
[data-senren-component="rich_text_editor_lite"] [contenteditable] :is(p, h1, h2, h3, li)[data-align="justify"] {
|
|
163
|
+
text-align: justify;
|
|
164
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module Senren
|
|
8
|
+
module Rails
|
|
9
|
+
# Copies component files from the gem's templates/ tree into the host
|
|
10
|
+
# Rails app, and updates .senren/installed_components.yml.
|
|
11
|
+
class ComponentCopier
|
|
12
|
+
attr_reader :registry, :paths, :stdout
|
|
13
|
+
|
|
14
|
+
def initialize(registry: Registry.load!, paths: HostPaths.new, stdout: $stdout)
|
|
15
|
+
@registry = registry
|
|
16
|
+
@paths = paths
|
|
17
|
+
@stdout = stdout
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Installs a list of components (with deps), respecting the override flag.
|
|
21
|
+
# Returns the ordered list of component names actually installed.
|
|
22
|
+
def install(component_names, client_override: nil, force: false)
|
|
23
|
+
wanted = registry.dependencies(*component_names)
|
|
24
|
+
paths.ensure_dirs!
|
|
25
|
+
|
|
26
|
+
wanted.each do |name|
|
|
27
|
+
comp = registry.fetch(name)
|
|
28
|
+
install_component(comp, client_override: client_override, force: force)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
update_installed_ledger(wanted, client_override: client_override)
|
|
32
|
+
wanted
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def install_component(comp, client_override:, force:)
|
|
38
|
+
effective_client = effective_client_for(comp, client_override)
|
|
39
|
+
|
|
40
|
+
comp.files.each do |relative|
|
|
41
|
+
next if relative.include?('javascript/controllers') && !effective_client
|
|
42
|
+
|
|
43
|
+
src = source_for(comp, relative)
|
|
44
|
+
dest = paths.root.join(relative)
|
|
45
|
+
copy_file(src, dest, force: force, label: "#{comp.name}::#{File.basename(relative)}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def effective_client_for(comp, override)
|
|
50
|
+
return comp.client? if override.nil?
|
|
51
|
+
return false unless comp.can_have_client
|
|
52
|
+
|
|
53
|
+
override
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def source_for(comp, relative)
|
|
57
|
+
# Map host path back to gem template path.
|
|
58
|
+
# app/components/senren/<name>_component.rb -> templates/components/<name>/<name>_component.rb
|
|
59
|
+
# app/components/senren/<name>_component.html.erb -> templates/components/<name>/<name>_component.html.erb
|
|
60
|
+
# app/javascript/controllers/senren/<name>_controller.js -> templates/controllers/<name>_controller.js
|
|
61
|
+
base = File.basename(relative)
|
|
62
|
+
if relative.include?('app/components/senren/')
|
|
63
|
+
File.join(Senren::Rails.templates_root, 'components', comp.name, base)
|
|
64
|
+
elsif relative.include?('app/javascript/controllers/senren/')
|
|
65
|
+
File.join(Senren::Rails.templates_root, 'controllers', base)
|
|
66
|
+
else
|
|
67
|
+
raise "ComponentCopier: do not know how to map #{relative.inspect}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def copy_file(src, dest, force:, label:)
|
|
72
|
+
unless File.exist?(src)
|
|
73
|
+
stdout.puts " warn missing template: #{src} (#{label})"
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
if File.exist?(dest) && !force
|
|
77
|
+
stdout.puts " skip #{dest} (already exists)"
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
81
|
+
FileUtils.cp(src, dest)
|
|
82
|
+
stdout.puts " copy #{dest}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def update_installed_ledger(names, client_override:)
|
|
86
|
+
ledger_path = paths.installed_components
|
|
87
|
+
ledger = ledger_path.exist? ? (YAML.safe_load_file(ledger_path) || {}) : {}
|
|
88
|
+
installed = ledger['installed'] ||= []
|
|
89
|
+
|
|
90
|
+
names.each do |name|
|
|
91
|
+
existing = installed.find { |e| e['name'] == name }
|
|
92
|
+
attrs = {
|
|
93
|
+
'name' => name,
|
|
94
|
+
'version' => Senren::Rails::VERSION,
|
|
95
|
+
'installed_at' => Time.now.utc.iso8601,
|
|
96
|
+
'client' => effective_client_for(registry.fetch(name), client_override)
|
|
97
|
+
}
|
|
98
|
+
if existing
|
|
99
|
+
existing.merge!(attrs.except('installed_at'))
|
|
100
|
+
else
|
|
101
|
+
installed << attrs
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
installed.sort_by! { |e| e['name'] }
|
|
106
|
+
ledger_path.parent.mkpath
|
|
107
|
+
File.write(ledger_path, YAML.dump(ledger))
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
module Rails
|
|
5
|
+
# Runs a series of checks against the host Rails app and prints a
|
|
6
|
+
# human-readable status report.
|
|
7
|
+
class Doctor
|
|
8
|
+
Result = Struct.new(:label, :ok, :detail) do
|
|
9
|
+
def icon = ok ? '✓' : '✗'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :paths, :stdout
|
|
13
|
+
|
|
14
|
+
def initialize(paths: HostPaths.new, stdout: $stdout)
|
|
15
|
+
@paths = paths
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run!
|
|
20
|
+
results = []
|
|
21
|
+
results << check('ViewComponent gem available') { defined?(::ViewComponent) }
|
|
22
|
+
results << check('TailwindCSS stylesheet present') { paths.stylesheet_path.exist? }
|
|
23
|
+
results << check('Stimulus directory present') { paths.stimulus_dir.directory? }
|
|
24
|
+
results << check('Turbo gem available') { defined?(::Turbo) || gem_loadable?('turbo-rails') }
|
|
25
|
+
results << check('.senren directory exists') { paths.senren_dir.directory? }
|
|
26
|
+
results << check('.senren/skill.md exists') { paths.skill_file.file? }
|
|
27
|
+
results << check('.senren/registry.yml exists') { paths.registry_mirror.file? }
|
|
28
|
+
results << check('.senren/installed_components.yml exists') { paths.installed_components.file? }
|
|
29
|
+
results << check('public/llms.txt exists') { paths.llms_short.file? }
|
|
30
|
+
results << check('public/llms-full.txt exists') { paths.llms_full.file? }
|
|
31
|
+
results << check('app/components/senren exists') { paths.components_dir.directory? }
|
|
32
|
+
results << check('app/javascript/controllers/senren exists') { paths.stimulus_dir.directory? }
|
|
33
|
+
installed = installed_count
|
|
34
|
+
results << Result.new("#{installed} component(s) installed", installed >= 0, nil)
|
|
35
|
+
|
|
36
|
+
report(results)
|
|
37
|
+
results.all?(&:ok)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def check(label)
|
|
43
|
+
ok = false
|
|
44
|
+
detail = nil
|
|
45
|
+
begin
|
|
46
|
+
ok = !yield.nil?
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
detail = e.message
|
|
49
|
+
end
|
|
50
|
+
Result.new(label, ok, detail)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def gem_loadable?(name)
|
|
54
|
+
Gem::Specification.find_by_name(name)
|
|
55
|
+
true
|
|
56
|
+
rescue Gem::LoadError
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def installed_count
|
|
61
|
+
return 0 unless paths.installed_components.file?
|
|
62
|
+
|
|
63
|
+
ledger = YAML.safe_load_file(paths.installed_components) || {}
|
|
64
|
+
Array(ledger['installed']).size
|
|
65
|
+
rescue StandardError
|
|
66
|
+
-1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def report(results)
|
|
70
|
+
stdout.puts 'Senren Doctor'
|
|
71
|
+
stdout.puts ''
|
|
72
|
+
results.each do |r|
|
|
73
|
+
line = "#{r.icon} #{r.label}"
|
|
74
|
+
line += " -- #{r.detail}" if r.detail
|
|
75
|
+
stdout.puts line
|
|
76
|
+
end
|
|
77
|
+
stdout.puts ''
|
|
78
|
+
if results.all?(&:ok)
|
|
79
|
+
stdout.puts 'No issues found.'
|
|
80
|
+
else
|
|
81
|
+
stdout.puts 'Issues found. Resolve the items marked with ✗.'
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/engine'
|
|
4
|
+
|
|
5
|
+
module Senren
|
|
6
|
+
module Rails
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
isolate_namespace Senren
|
|
9
|
+
|
|
10
|
+
initializer 'senren.rails.load_tasks' do |app|
|
|
11
|
+
rake_path = File.expand_path('../../tasks/senren.rake', __dir__)
|
|
12
|
+
app.paths['lib/tasks'] << rake_path if File.exist?(rake_path)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
module Rails
|
|
5
|
+
# Resolves canonical paths inside a host Rails application.
|
|
6
|
+
# Accepts an explicit root for tests; defaults to Rails.root when present.
|
|
7
|
+
class HostPaths
|
|
8
|
+
attr_reader :root
|
|
9
|
+
|
|
10
|
+
def initialize(root = nil)
|
|
11
|
+
@root = Pathname.new(root || ::Rails.root).expand_path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def senren_dir = root.join('.senren')
|
|
15
|
+
def skill_file = senren_dir.join('skill.md')
|
|
16
|
+
def registry_mirror = senren_dir.join('registry.yml')
|
|
17
|
+
def installed_components = senren_dir.join('installed_components.yml')
|
|
18
|
+
def conventions_file = senren_dir.join('conventions.md')
|
|
19
|
+
|
|
20
|
+
def components_dir = root.join('app', 'components', 'senren')
|
|
21
|
+
def base_component_path = components_dir.join('base_component.rb')
|
|
22
|
+
|
|
23
|
+
def stylesheet_path = root.join('app', 'assets', 'stylesheets', 'senren.css')
|
|
24
|
+
|
|
25
|
+
def stimulus_dir = root.join('app', 'javascript', 'controllers', 'senren')
|
|
26
|
+
|
|
27
|
+
def llms_short = root.join('public', 'llms.txt')
|
|
28
|
+
def llms_full = root.join('public', 'llms-full.txt')
|
|
29
|
+
|
|
30
|
+
def ensure_dirs!
|
|
31
|
+
[senren_dir, components_dir, stimulus_dir,
|
|
32
|
+
stylesheet_path.dirname, llms_short.dirname].each(&:mkpath)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Senren
|
|
6
|
+
module Rails
|
|
7
|
+
# Idempotent installer that lays down the .senren directory, base
|
|
8
|
+
# component, stylesheet, and initial llms files. Reused by the install
|
|
9
|
+
# generator and by ad-hoc rake task entry points.
|
|
10
|
+
class Installer
|
|
11
|
+
attr_reader :paths, :stdout
|
|
12
|
+
|
|
13
|
+
def initialize(paths: HostPaths.new, stdout: $stdout)
|
|
14
|
+
@paths = paths
|
|
15
|
+
@stdout = stdout
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run!(force: false)
|
|
19
|
+
paths.ensure_dirs!
|
|
20
|
+
install_static_files(force: force)
|
|
21
|
+
mirror_registry
|
|
22
|
+
SkillWriter.new(paths: paths).sync!
|
|
23
|
+
LlmsWriter.new(paths: paths).generate!
|
|
24
|
+
print_next_steps
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
INSTALL_GENERATOR_TEMPLATES = File.expand_path(
|
|
29
|
+
'../../generators/senren/install/templates', __dir__
|
|
30
|
+
).freeze
|
|
31
|
+
|
|
32
|
+
STATIC_FILES = {
|
|
33
|
+
'conventions.md.tt' => :conventions_file,
|
|
34
|
+
'installed_components.yml.tt' => :installed_components,
|
|
35
|
+
'base_component.rb.tt' => :base_component_path,
|
|
36
|
+
'senren.css.tt' => :stylesheet_path
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def install_static_files(force:)
|
|
42
|
+
STATIC_FILES.each do |template_basename, paths_method|
|
|
43
|
+
src = File.join(INSTALL_GENERATOR_TEMPLATES, template_basename)
|
|
44
|
+
dest = paths.public_send(paths_method)
|
|
45
|
+
copy_template(src, dest, force: force)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def copy_template(src, dest, force:)
|
|
50
|
+
unless File.exist?(src)
|
|
51
|
+
stdout.puts " warn template missing: #{src}"
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
if File.exist?(dest) && !force
|
|
55
|
+
stdout.puts " skip #{dest}"
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
59
|
+
FileUtils.cp(src, dest)
|
|
60
|
+
stdout.puts " copy #{dest}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def mirror_registry
|
|
64
|
+
FileUtils.mkdir_p(paths.registry_mirror.dirname)
|
|
65
|
+
FileUtils.cp(Senren::Rails.registry_path, paths.registry_mirror)
|
|
66
|
+
stdout.puts " mirror #{paths.registry_mirror}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def print_next_steps
|
|
70
|
+
stdout.puts <<~MSG
|
|
71
|
+
|
|
72
|
+
Senren installed. Next steps:
|
|
73
|
+
|
|
74
|
+
bin/rails senren:add button card badge alert
|
|
75
|
+
bin/rails senren:add dialog dropdown_menu
|
|
76
|
+
bin/rails senren:doctor
|
|
77
|
+
|
|
78
|
+
Read .senren/skill.md and .senren/conventions.md to get oriented.
|
|
79
|
+
MSG
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Senren
|
|
6
|
+
module Rails
|
|
7
|
+
# Generates public/llms.txt and public/llms-full.txt for AI discoverability.
|
|
8
|
+
# The entire content of both files is owned by this writer; do not edit by hand.
|
|
9
|
+
class LlmsWriter
|
|
10
|
+
attr_reader :registry, :paths
|
|
11
|
+
|
|
12
|
+
def initialize(registry: Registry.load!, paths: HostPaths.new)
|
|
13
|
+
@registry = registry
|
|
14
|
+
@paths = paths
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def generate!
|
|
18
|
+
paths.llms_short.parent.mkpath
|
|
19
|
+
paths.llms_full.parent.mkpath
|
|
20
|
+
atomic_write(paths.llms_short, render_short)
|
|
21
|
+
atomic_write(paths.llms_full, render_full)
|
|
22
|
+
[paths.llms_short, paths.llms_full]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def installed
|
|
28
|
+
path = paths.installed_components
|
|
29
|
+
return [] unless path.exist?
|
|
30
|
+
|
|
31
|
+
ledger = YAML.safe_load_file(path) || {}
|
|
32
|
+
Array(ledger['installed']).filter_map { |e| registry.find(e['name']) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render_short
|
|
36
|
+
names = installed.map(&:name).sort
|
|
37
|
+
<<~TXT
|
|
38
|
+
# Senren UI
|
|
39
|
+
|
|
40
|
+
Generated by senren-ui. Do not edit by hand. Run
|
|
41
|
+
`bin/rails senren:llms:generate` to regenerate.
|
|
42
|
+
|
|
43
|
+
Senren UI is the local Rails UI system used by this application.
|
|
44
|
+
Use `.senren/skill.md` as the primary AI Agent guide.
|
|
45
|
+
|
|
46
|
+
## Hard Rules
|
|
47
|
+
|
|
48
|
+
- Use Senren components before writing custom HTML.
|
|
49
|
+
- Use ViewComponent for reusable UI.
|
|
50
|
+
- Use Turbo for server state.
|
|
51
|
+
- Use Stimulus only for local behavior.
|
|
52
|
+
- Do not introduce React, Vue, Alpine, or any external state framework.
|
|
53
|
+
- Do not hard-code colors; use semantic Tailwind tokens.
|
|
54
|
+
|
|
55
|
+
## Important Files
|
|
56
|
+
|
|
57
|
+
- `.senren/skill.md` - centralized AI agent guide
|
|
58
|
+
- `.senren/registry.yml` - mirror of installable components
|
|
59
|
+
- `.senren/installed_components.yml` - what is currently installed
|
|
60
|
+
- `.senren/conventions.md` - Senren conventions for humans and agents
|
|
61
|
+
|
|
62
|
+
## Installed Components (#{names.size})
|
|
63
|
+
|
|
64
|
+
#{names.map { |n| "- #{n}" }.join("\n")}
|
|
65
|
+
TXT
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def render_full
|
|
69
|
+
comps = installed.sort_by(&:name)
|
|
70
|
+
|
|
71
|
+
out = []
|
|
72
|
+
out << '# Senren UI - Full Snapshot'
|
|
73
|
+
out << ''
|
|
74
|
+
out << 'Generated by senren-ui. Do not edit by hand. Run'
|
|
75
|
+
out << '`bin/rails senren:llms:generate` to regenerate.'
|
|
76
|
+
out << ''
|
|
77
|
+
out << '## Hard Rules'
|
|
78
|
+
out << ''
|
|
79
|
+
out << '- Use Senren components before writing custom HTML.'
|
|
80
|
+
out << '- Server-rendered HTML first; ViewComponent for reusable UI.'
|
|
81
|
+
out << '- Hotwire (Turbo + Stimulus) is the only client runtime.'
|
|
82
|
+
out << '- TailwindCSS with semantic tokens (`bg-background`, `text-foreground`, ...).'
|
|
83
|
+
out << '- Do not introduce React, Vue, Alpine, or external state frameworks.'
|
|
84
|
+
out << '- Components copied into `app/components/senren/` are owned by this app; edit them directly.'
|
|
85
|
+
out << ''
|
|
86
|
+
out << '## Installed Component Inventory'
|
|
87
|
+
out << ''
|
|
88
|
+
|
|
89
|
+
registry.groups.each do |group|
|
|
90
|
+
group_comps = comps.select { |c| c.category == group['id'] }
|
|
91
|
+
next if group_comps.empty?
|
|
92
|
+
|
|
93
|
+
out << "### #{group['title']}"
|
|
94
|
+
out << ''
|
|
95
|
+
out << group['description'].to_s
|
|
96
|
+
out << ''
|
|
97
|
+
group_comps.each { |c| out << render_component(c) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
out << '## Recipes'
|
|
101
|
+
out << ''
|
|
102
|
+
registry.recipes.each do |id, recipe|
|
|
103
|
+
out << "### #{id}"
|
|
104
|
+
out << ''
|
|
105
|
+
out << recipe['description'].to_s
|
|
106
|
+
out << ''
|
|
107
|
+
out << 'Components:'
|
|
108
|
+
recipe['components'].each { |c| out << "- #{c}" }
|
|
109
|
+
out << ''
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
out.join("\n")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_component(comp)
|
|
116
|
+
ruby_class = "Senren::#{comp.name.split('_').map { |w| w[0].upcase + w[1..] }.join}Component"
|
|
117
|
+
s = []
|
|
118
|
+
s << "#### #{comp.name}#{' (stub)' if comp.stub?}"
|
|
119
|
+
s << ''
|
|
120
|
+
s << "Category: #{comp.category}. " \
|
|
121
|
+
"Client: #{comp.client? ? "yes (#{comp.controller})" : 'no'}. " \
|
|
122
|
+
"Variants: #{comp.variants.empty? ? 'none' : comp.variants.join(', ')}."
|
|
123
|
+
s << ''
|
|
124
|
+
s << "Use for: #{comp.use_for.join('; ')}." unless comp.use_for.empty?
|
|
125
|
+
s << "Avoid: #{comp.avoid.join('; ')}." unless comp.avoid.empty?
|
|
126
|
+
s << ''
|
|
127
|
+
s << 'Rails usage:'
|
|
128
|
+
s << ''
|
|
129
|
+
s << '```erb'
|
|
130
|
+
s << if comp.variants.any?
|
|
131
|
+
"<%= render #{ruby_class}.new(variant: :#{comp.variants.first}) do %>"
|
|
132
|
+
else
|
|
133
|
+
"<%= render #{ruby_class}.new do %>"
|
|
134
|
+
end
|
|
135
|
+
s << ' ...'
|
|
136
|
+
s << '<% end %>'
|
|
137
|
+
s << '```'
|
|
138
|
+
s << ''
|
|
139
|
+
s.join("\n")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def atomic_write(path, content)
|
|
143
|
+
tmp = "#{path}.tmp"
|
|
144
|
+
File.write(tmp, content)
|
|
145
|
+
File.rename(tmp, path)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|