senren-ui 0.1.6 → 0.3.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 +4 -4
- data/CHANGELOG.md +215 -0
- data/CONTRIBUTING.md +41 -3
- data/README.md +138 -33
- data/Rakefile +14 -1
- data/docs/components.md +10 -10
- data/docs/hot_reload.md +103 -0
- data/docs/performance_testing.md +7 -3
- data/docs/visual_style.md +80 -0
- data/lib/generators/senren/component/templates/controller.js.tt +7 -4
- data/lib/generators/senren/install/install_generator.rb +77 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +142 -4
- data/lib/generators/senren/install/templates/conventions.md.tt +57 -1
- data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
- data/lib/senren/rails/agent_rules_writer.rb +65 -19
- data/lib/senren/rails/asset_path_guard.rb +128 -0
- data/lib/senren/rails/base_component_patch.rb +64 -0
- data/lib/senren/rails/component_copier.rb +101 -50
- data/lib/senren/rails/component_installer.rb +26 -0
- data/lib/senren/rails/doctor.rb +7 -4
- data/lib/senren/rails/engine.rb +23 -0
- data/lib/senren/rails/host_paths.rb +11 -2
- data/lib/senren/rails/marker_block.rb +81 -0
- data/lib/senren/rails/registry.rb +10 -4
- data/lib/senren/rails/safe_write.rb +169 -0
- data/lib/senren/rails/skill_writer.rb +47 -11
- data/lib/senren/rails/version.rb +1 -1
- data/lib/senren/rails.rb +1 -1
- data/lib/senren-ui.rb +15 -0
- data/lib/tasks/senren.rake +43 -17
- data/registry/components.yml +45 -0
- data/registry/recipes.yml +12 -0
- data/templates/components/accordion/accordion_component.html.erb +3 -3
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +6 -3
- data/templates/components/alert_dialog/alert_dialog_component.rb +5 -1
- data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
- data/templates/components/aspect_ratio/aspect_ratio_component.rb +7 -0
- data/templates/components/avatar/avatar_component.rb +8 -1
- data/templates/components/breadcrumb/breadcrumb_component.html.erb +8 -6
- data/templates/components/button/button_component.html.erb +1 -1
- data/templates/components/button/button_component.rb +27 -1
- data/templates/components/calendar/calendar_component.html.erb +2 -2
- data/templates/components/card/card_component.html.erb +4 -6
- data/templates/components/carousel/carousel_component.html.erb +1 -1
- data/templates/components/cart/cart_component.html.erb +67 -0
- data/templates/components/cart/cart_component.rb +71 -0
- data/templates/components/checkbox/checkbox_component.rb +1 -1
- data/templates/components/clipboard/clipboard_component.html.erb +3 -3
- data/templates/components/collapsible/collapsible_component.html.erb +3 -3
- data/templates/components/combobox/combobox_component.html.erb +3 -3
- data/templates/components/command/command_component.html.erb +1 -1
- data/templates/components/command/command_component.rb +1 -1
- data/templates/components/context_menu/context_menu_component.html.erb +6 -3
- data/templates/components/date_picker/date_picker_component.html.erb +3 -3
- data/templates/components/dialog/dialog_component.html.erb +7 -3
- data/templates/components/dialog/dialog_component.rb +12 -1
- data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +6 -3
- data/templates/components/dropdown_menu/dropdown_menu_component.rb +57 -5
- data/templates/components/form/form_component.rb +9 -1
- data/templates/components/hover_card/hover_card_component.html.erb +6 -3
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +3 -2
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +1 -1
- data/templates/components/link/link_component.html.erb +1 -1
- data/templates/components/native_select/native_select_component.html.erb +2 -2
- data/templates/components/pagination/pagination_component.html.erb +2 -2
- data/templates/components/popover/popover_component.html.erb +6 -3
- data/templates/components/product_card/product_card_component.html.erb +38 -0
- data/templates/components/product_card/product_card_component.rb +49 -0
- data/templates/components/progress/progress_component.html.erb +2 -2
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +1 -1
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +1 -1
- data/templates/components/search_input/search_input_component.html.erb +2 -2
- data/templates/components/separator/separator_component.rb +7 -0
- data/templates/components/sheet/sheet_component.html.erb +13 -4
- data/templates/components/sheet/sheet_component.rb +12 -1
- data/templates/components/shortcut_key/shortcut_key_component.html.erb +2 -2
- data/templates/components/sidebar/sidebar_component.html.erb +2 -2
- data/templates/components/switch/switch_component.html.erb +1 -1
- data/templates/components/table/table_component.html.erb +2 -2
- data/templates/components/tabs/tabs_component.html.erb +3 -3
- data/templates/components/tooltip/tooltip_component.html.erb +5 -3
- data/templates/components/tooltip/tooltip_component.rb +2 -2
- data/templates/components/top_nav/top_nav_component.html.erb +2 -2
- data/templates/components/typography/typography_component.rb +7 -0
- data/templates/controllers/accordion_controller.js +1 -1
- data/templates/controllers/alert_dialog_controller.js +31 -7
- data/templates/controllers/cart_controller.js +85 -0
- data/templates/controllers/clipboard_controller.js +12 -1
- data/templates/controllers/command_controller.js +3 -4
- data/templates/controllers/context_menu_controller.js +38 -11
- data/templates/controllers/data_table_controller.js +8 -3
- data/templates/controllers/dialog_controller.js +47 -21
- data/templates/controllers/dropdown_menu_controller.js +40 -27
- data/templates/controllers/hover_card_controller.js +8 -0
- data/templates/controllers/invite_member_dialog_controller.js +6 -0
- data/templates/controllers/masked_input_controller.js +8 -1
- data/templates/controllers/popover_controller.js +25 -10
- data/templates/controllers/rich_text_editor_lite_controller.js +165 -28
- data/templates/controllers/sheet_controller.js +41 -11
- metadata +14 -17
- data/lib/senren/rails/installer.rb +0 -85
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Senren UI - optional palette presets.
|
|
3
|
+
*
|
|
4
|
+
* Link this after senren.css and set data-senren-theme on <html> to switch:
|
|
5
|
+
*
|
|
6
|
+
* <%%= stylesheet_link_tag "senren" %>
|
|
7
|
+
* <%%= stylesheet_link_tag "senren_themes" %>
|
|
8
|
+
*
|
|
9
|
+
* <html data-senren-theme="slate">
|
|
10
|
+
*
|
|
11
|
+
* Leave the attribute off for the default Spring Garden palette, which
|
|
12
|
+
* senren.css declares in :root.
|
|
13
|
+
*
|
|
14
|
+
* Every theme below is nothing but a re-declaration of the same token set that
|
|
15
|
+
* senren.css defines. No component is aware that themes exist: components read
|
|
16
|
+
* hsl(var(--senren-*)) and inherit whatever is in scope. Adding your own theme
|
|
17
|
+
* means copying one of these blocks and changing the values — there is no
|
|
18
|
+
* component to touch and no build step to run.
|
|
19
|
+
*
|
|
20
|
+
* Scoping: [data-senren-theme] on <html>, so it composes with the existing
|
|
21
|
+
* .dark class rather than replacing it. Each theme therefore ships a light and
|
|
22
|
+
* a dark block, and light/dark stays orthogonal to the palette choice.
|
|
23
|
+
*
|
|
24
|
+
* `senren` is the default and needs no block: it is what senren.css already
|
|
25
|
+
* declares in :root and .dark.
|
|
26
|
+
*
|
|
27
|
+
* test/themes_test.rb asserts every theme here declares the complete token set,
|
|
28
|
+
* so a palette cannot ship with a variable quietly missing.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/* ── slate ────────────────────────────────────────────────────────────────
|
|
32
|
+
Cool neutrals, near-black primary, tight radius. The restrained look most
|
|
33
|
+
developer tools converge on. */
|
|
34
|
+
[data-senren-theme="slate"] {
|
|
35
|
+
--senren-background: 0 0% 100%;
|
|
36
|
+
--senren-foreground: 222 47% 11%;
|
|
37
|
+
--senren-muted: 210 40% 96%;
|
|
38
|
+
--senren-muted-foreground: 215 16% 47%;
|
|
39
|
+
--senren-card: 0 0% 100%;
|
|
40
|
+
--senren-card-foreground: 222 47% 11%;
|
|
41
|
+
--senren-popover: 0 0% 100%;
|
|
42
|
+
--senren-popover-foreground: 222 47% 11%;
|
|
43
|
+
--senren-border: 214 32% 91%;
|
|
44
|
+
--senren-input: 214 32% 91%;
|
|
45
|
+
--senren-ring: 222 47% 11%;
|
|
46
|
+
--senren-primary: 222 47% 11%;
|
|
47
|
+
--senren-primary-foreground: 210 40% 98%;
|
|
48
|
+
--senren-secondary: 210 40% 96%;
|
|
49
|
+
--senren-secondary-foreground: 222 47% 11%;
|
|
50
|
+
--senren-accent: 210 40% 94%;
|
|
51
|
+
--senren-accent-foreground: 222 47% 11%;
|
|
52
|
+
--senren-destructive: 0 72% 51%;
|
|
53
|
+
--senren-destructive-foreground: 210 40% 98%;
|
|
54
|
+
--senren-success: 142 71% 36%;
|
|
55
|
+
--senren-success-foreground: 210 40% 98%;
|
|
56
|
+
--senren-warning: 38 92% 50%;
|
|
57
|
+
--senren-warning-foreground: 222 47% 11%;
|
|
58
|
+
--senren-radius: 0.375rem;
|
|
59
|
+
--senren-palette-sky: 217 91% 60%;
|
|
60
|
+
--senren-palette-sakura: 330 81% 60%;
|
|
61
|
+
--senren-palette-pond: 199 89% 48%;
|
|
62
|
+
--senren-palette-leaf: 142 71% 45%;
|
|
63
|
+
--senren-palette-iris: 258 90% 66%;
|
|
64
|
+
--senren-palette-paper: 210 40% 98%;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.dark[data-senren-theme="slate"] {
|
|
68
|
+
--senren-background: 222 47% 7%;
|
|
69
|
+
--senren-foreground: 210 40% 98%;
|
|
70
|
+
--senren-muted: 217 33% 17%;
|
|
71
|
+
--senren-muted-foreground: 215 20% 65%;
|
|
72
|
+
--senren-card: 222 47% 9%;
|
|
73
|
+
--senren-card-foreground: 210 40% 98%;
|
|
74
|
+
--senren-popover: 222 47% 9%;
|
|
75
|
+
--senren-popover-foreground: 210 40% 98%;
|
|
76
|
+
--senren-border: 217 33% 20%;
|
|
77
|
+
--senren-input: 217 33% 20%;
|
|
78
|
+
--senren-ring: 213 27% 84%;
|
|
79
|
+
--senren-primary: 210 40% 98%;
|
|
80
|
+
--senren-primary-foreground: 222 47% 11%;
|
|
81
|
+
--senren-secondary: 217 33% 17%;
|
|
82
|
+
--senren-secondary-foreground: 210 40% 98%;
|
|
83
|
+
--senren-accent: 217 33% 20%;
|
|
84
|
+
--senren-accent-foreground: 210 40% 98%;
|
|
85
|
+
--senren-destructive: 0 63% 46%;
|
|
86
|
+
--senren-destructive-foreground: 210 40% 98%;
|
|
87
|
+
--senren-success: 142 60% 40%;
|
|
88
|
+
--senren-success-foreground: 210 40% 98%;
|
|
89
|
+
--senren-warning: 38 85% 52%;
|
|
90
|
+
--senren-warning-foreground: 222 47% 11%;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ── indigo ───────────────────────────────────────────────────────────────
|
|
94
|
+
The default of a great many B2B products: white surfaces, indigo action
|
|
95
|
+
colour, cool grey text. */
|
|
96
|
+
[data-senren-theme="indigo"] {
|
|
97
|
+
--senren-background: 0 0% 100%;
|
|
98
|
+
--senren-foreground: 224 71% 4%;
|
|
99
|
+
--senren-muted: 220 14% 96%;
|
|
100
|
+
--senren-muted-foreground: 220 9% 46%;
|
|
101
|
+
--senren-card: 0 0% 100%;
|
|
102
|
+
--senren-card-foreground: 224 71% 4%;
|
|
103
|
+
--senren-popover: 0 0% 100%;
|
|
104
|
+
--senren-popover-foreground: 224 71% 4%;
|
|
105
|
+
--senren-border: 220 13% 91%;
|
|
106
|
+
--senren-input: 220 13% 91%;
|
|
107
|
+
--senren-ring: 239 84% 67%;
|
|
108
|
+
--senren-primary: 243 75% 59%;
|
|
109
|
+
--senren-primary-foreground: 0 0% 100%;
|
|
110
|
+
--senren-secondary: 226 100% 97%;
|
|
111
|
+
--senren-secondary-foreground: 243 75% 40%;
|
|
112
|
+
--senren-accent: 226 100% 94%;
|
|
113
|
+
--senren-accent-foreground: 243 75% 40%;
|
|
114
|
+
--senren-destructive: 0 84% 60%;
|
|
115
|
+
--senren-destructive-foreground: 0 0% 100%;
|
|
116
|
+
--senren-success: 160 84% 32%;
|
|
117
|
+
--senren-success-foreground: 0 0% 100%;
|
|
118
|
+
--senren-warning: 32 95% 52%;
|
|
119
|
+
--senren-warning-foreground: 224 71% 4%;
|
|
120
|
+
--senren-radius: 0.5rem;
|
|
121
|
+
--senren-palette-sky: 217 91% 60%;
|
|
122
|
+
--senren-palette-sakura: 330 81% 70%;
|
|
123
|
+
--senren-palette-pond: 199 89% 60%;
|
|
124
|
+
--senren-palette-leaf: 160 84% 39%;
|
|
125
|
+
--senren-palette-iris: 243 75% 59%;
|
|
126
|
+
--senren-palette-paper: 220 14% 96%;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.dark[data-senren-theme="indigo"] {
|
|
130
|
+
--senren-background: 229 42% 8%;
|
|
131
|
+
--senren-foreground: 220 14% 96%;
|
|
132
|
+
--senren-muted: 229 30% 16%;
|
|
133
|
+
--senren-muted-foreground: 218 11% 65%;
|
|
134
|
+
--senren-card: 229 40% 11%;
|
|
135
|
+
--senren-card-foreground: 220 14% 96%;
|
|
136
|
+
--senren-popover: 229 40% 11%;
|
|
137
|
+
--senren-popover-foreground: 220 14% 96%;
|
|
138
|
+
--senren-border: 229 25% 22%;
|
|
139
|
+
--senren-input: 229 25% 22%;
|
|
140
|
+
--senren-ring: 239 84% 72%;
|
|
141
|
+
--senren-primary: 239 84% 70%;
|
|
142
|
+
--senren-primary-foreground: 229 42% 8%;
|
|
143
|
+
--senren-secondary: 229 30% 18%;
|
|
144
|
+
--senren-secondary-foreground: 226 100% 88%;
|
|
145
|
+
--senren-accent: 229 30% 22%;
|
|
146
|
+
--senren-accent-foreground: 226 100% 92%;
|
|
147
|
+
--senren-destructive: 0 72% 55%;
|
|
148
|
+
--senren-destructive-foreground: 0 0% 100%;
|
|
149
|
+
--senren-success: 160 70% 42%;
|
|
150
|
+
--senren-success-foreground: 229 42% 8%;
|
|
151
|
+
--senren-warning: 32 90% 58%;
|
|
152
|
+
--senren-warning-foreground: 229 42% 8%;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ── emerald ──────────────────────────────────────────────────────────────
|
|
156
|
+
Green action colour on soft neutrals, with a generous radius. Common in
|
|
157
|
+
finance, health, and sustainability products. */
|
|
158
|
+
[data-senren-theme="emerald"] {
|
|
159
|
+
--senren-background: 150 20% 99%;
|
|
160
|
+
--senren-foreground: 160 40% 10%;
|
|
161
|
+
--senren-muted: 150 24% 95%;
|
|
162
|
+
--senren-muted-foreground: 160 12% 42%;
|
|
163
|
+
--senren-card: 0 0% 100%;
|
|
164
|
+
--senren-card-foreground: 160 40% 10%;
|
|
165
|
+
--senren-popover: 0 0% 100%;
|
|
166
|
+
--senren-popover-foreground: 160 40% 10%;
|
|
167
|
+
--senren-border: 152 22% 88%;
|
|
168
|
+
--senren-input: 152 22% 88%;
|
|
169
|
+
--senren-ring: 160 84% 39%;
|
|
170
|
+
--senren-primary: 160 84% 30%;
|
|
171
|
+
--senren-primary-foreground: 150 30% 98%;
|
|
172
|
+
--senren-secondary: 152 45% 92%;
|
|
173
|
+
--senren-secondary-foreground: 160 60% 20%;
|
|
174
|
+
--senren-accent: 168 60% 90%;
|
|
175
|
+
--senren-accent-foreground: 168 70% 18%;
|
|
176
|
+
--senren-destructive: 0 74% 50%;
|
|
177
|
+
--senren-destructive-foreground: 150 30% 98%;
|
|
178
|
+
--senren-success: 160 84% 32%;
|
|
179
|
+
--senren-success-foreground: 150 30% 98%;
|
|
180
|
+
--senren-warning: 40 95% 50%;
|
|
181
|
+
--senren-warning-foreground: 160 40% 10%;
|
|
182
|
+
--senren-radius: 0.75rem;
|
|
183
|
+
--senren-palette-sky: 190 80% 45%;
|
|
184
|
+
--senren-palette-sakura: 340 70% 82%;
|
|
185
|
+
--senren-palette-pond: 172 66% 78%;
|
|
186
|
+
--senren-palette-leaf: 145 63% 49%;
|
|
187
|
+
--senren-palette-iris: 250 60% 78%;
|
|
188
|
+
--senren-palette-paper: 150 24% 95%;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.dark[data-senren-theme="emerald"] {
|
|
192
|
+
--senren-background: 165 40% 6%;
|
|
193
|
+
--senren-foreground: 150 24% 95%;
|
|
194
|
+
--senren-muted: 165 28% 14%;
|
|
195
|
+
--senren-muted-foreground: 152 14% 62%;
|
|
196
|
+
--senren-card: 165 38% 9%;
|
|
197
|
+
--senren-card-foreground: 150 24% 95%;
|
|
198
|
+
--senren-popover: 165 38% 9%;
|
|
199
|
+
--senren-popover-foreground: 150 24% 95%;
|
|
200
|
+
--senren-border: 165 22% 20%;
|
|
201
|
+
--senren-input: 165 22% 20%;
|
|
202
|
+
--senren-ring: 160 84% 45%;
|
|
203
|
+
--senren-primary: 158 74% 44%;
|
|
204
|
+
--senren-primary-foreground: 165 40% 6%;
|
|
205
|
+
--senren-secondary: 165 28% 16%;
|
|
206
|
+
--senren-secondary-foreground: 152 45% 88%;
|
|
207
|
+
--senren-accent: 168 40% 20%;
|
|
208
|
+
--senren-accent-foreground: 168 60% 88%;
|
|
209
|
+
--senren-destructive: 0 66% 52%;
|
|
210
|
+
--senren-destructive-foreground: 150 30% 98%;
|
|
211
|
+
--senren-success: 158 70% 42%;
|
|
212
|
+
--senren-success-foreground: 165 40% 6%;
|
|
213
|
+
--senren-warning: 40 88% 56%;
|
|
214
|
+
--senren-warning-foreground: 165 40% 6%;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ── rose ─────────────────────────────────────────────────────────────────
|
|
218
|
+
Warm neutrals and a soft pink action colour, with the roundest corners of
|
|
219
|
+
the set. The consumer-facing end of SaaS. */
|
|
220
|
+
[data-senren-theme="rose"] {
|
|
221
|
+
--senren-background: 20 40% 99%;
|
|
222
|
+
--senren-foreground: 340 30% 12%;
|
|
223
|
+
--senren-muted: 20 30% 96%;
|
|
224
|
+
--senren-muted-foreground: 340 8% 46%;
|
|
225
|
+
--senren-card: 0 0% 100%;
|
|
226
|
+
--senren-card-foreground: 340 30% 12%;
|
|
227
|
+
--senren-popover: 0 0% 100%;
|
|
228
|
+
--senren-popover-foreground: 340 30% 12%;
|
|
229
|
+
--senren-border: 20 25% 90%;
|
|
230
|
+
--senren-input: 20 25% 90%;
|
|
231
|
+
--senren-ring: 347 77% 60%;
|
|
232
|
+
--senren-primary: 347 77% 50%;
|
|
233
|
+
--senren-primary-foreground: 20 40% 99%;
|
|
234
|
+
--senren-secondary: 350 80% 95%;
|
|
235
|
+
--senren-secondary-foreground: 347 70% 32%;
|
|
236
|
+
--senren-accent: 350 80% 92%;
|
|
237
|
+
--senren-accent-foreground: 347 70% 30%;
|
|
238
|
+
--senren-destructive: 0 78% 52%;
|
|
239
|
+
--senren-destructive-foreground: 20 40% 99%;
|
|
240
|
+
--senren-success: 150 62% 36%;
|
|
241
|
+
--senren-success-foreground: 20 40% 99%;
|
|
242
|
+
--senren-warning: 35 95% 55%;
|
|
243
|
+
--senren-warning-foreground: 340 30% 12%;
|
|
244
|
+
--senren-radius: 1rem;
|
|
245
|
+
--senren-palette-sky: 205 85% 58%;
|
|
246
|
+
--senren-palette-sakura: 350 85% 84%;
|
|
247
|
+
--senren-palette-pond: 190 70% 82%;
|
|
248
|
+
--senren-palette-leaf: 140 55% 55%;
|
|
249
|
+
--senren-palette-iris: 280 70% 78%;
|
|
250
|
+
--senren-palette-paper: 20 30% 96%;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.dark[data-senren-theme="rose"] {
|
|
254
|
+
--senren-background: 340 22% 8%;
|
|
255
|
+
--senren-foreground: 20 30% 96%;
|
|
256
|
+
--senren-muted: 340 16% 16%;
|
|
257
|
+
--senren-muted-foreground: 340 8% 64%;
|
|
258
|
+
--senren-card: 340 20% 11%;
|
|
259
|
+
--senren-card-foreground: 20 30% 96%;
|
|
260
|
+
--senren-popover: 340 20% 11%;
|
|
261
|
+
--senren-popover-foreground: 20 30% 96%;
|
|
262
|
+
--senren-border: 340 14% 22%;
|
|
263
|
+
--senren-input: 340 14% 22%;
|
|
264
|
+
--senren-ring: 347 77% 66%;
|
|
265
|
+
--senren-primary: 347 82% 62%;
|
|
266
|
+
--senren-primary-foreground: 340 22% 8%;
|
|
267
|
+
--senren-secondary: 340 16% 18%;
|
|
268
|
+
--senren-secondary-foreground: 350 80% 90%;
|
|
269
|
+
--senren-accent: 340 20% 24%;
|
|
270
|
+
--senren-accent-foreground: 350 80% 90%;
|
|
271
|
+
--senren-destructive: 0 70% 55%;
|
|
272
|
+
--senren-destructive-foreground: 20 40% 99%;
|
|
273
|
+
--senren-success: 150 55% 44%;
|
|
274
|
+
--senren-success-foreground: 340 22% 8%;
|
|
275
|
+
--senren-warning: 35 90% 60%;
|
|
276
|
+
--senren-warning-foreground: 340 22% 8%;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ── amber ────────────────────────────────────────────────────────────────
|
|
280
|
+
Sand-toned neutrals, amber action colour, near-square corners. The
|
|
281
|
+
document-and-workspace look. */
|
|
282
|
+
[data-senren-theme="amber"] {
|
|
283
|
+
--senren-background: 40 30% 98%;
|
|
284
|
+
--senren-foreground: 30 25% 12%;
|
|
285
|
+
--senren-muted: 40 24% 94%;
|
|
286
|
+
--senren-muted-foreground: 32 10% 44%;
|
|
287
|
+
--senren-card: 40 40% 99%;
|
|
288
|
+
--senren-card-foreground: 30 25% 12%;
|
|
289
|
+
--senren-popover: 40 40% 99%;
|
|
290
|
+
--senren-popover-foreground: 30 25% 12%;
|
|
291
|
+
--senren-border: 36 20% 87%;
|
|
292
|
+
--senren-input: 36 20% 87%;
|
|
293
|
+
--senren-ring: 32 95% 44%;
|
|
294
|
+
--senren-primary: 30 30% 16%;
|
|
295
|
+
--senren-primary-foreground: 40 30% 98%;
|
|
296
|
+
--senren-secondary: 40 60% 90%;
|
|
297
|
+
--senren-secondary-foreground: 30 40% 20%;
|
|
298
|
+
--senren-accent: 38 80% 88%;
|
|
299
|
+
--senren-accent-foreground: 30 45% 20%;
|
|
300
|
+
--senren-destructive: 4 74% 48%;
|
|
301
|
+
--senren-destructive-foreground: 40 30% 98%;
|
|
302
|
+
--senren-success: 140 55% 34%;
|
|
303
|
+
--senren-success-foreground: 40 30% 98%;
|
|
304
|
+
--senren-warning: 32 95% 46%;
|
|
305
|
+
--senren-warning-foreground: 40 30% 98%;
|
|
306
|
+
--senren-radius: 0.25rem;
|
|
307
|
+
--senren-palette-sky: 200 75% 50%;
|
|
308
|
+
--senren-palette-sakura: 20 80% 82%;
|
|
309
|
+
--senren-palette-pond: 190 60% 80%;
|
|
310
|
+
--senren-palette-leaf: 100 45% 50%;
|
|
311
|
+
--senren-palette-iris: 265 55% 74%;
|
|
312
|
+
--senren-palette-paper: 40 24% 94%;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.dark[data-senren-theme="amber"] {
|
|
316
|
+
--senren-background: 30 18% 8%;
|
|
317
|
+
--senren-foreground: 40 24% 94%;
|
|
318
|
+
--senren-muted: 30 14% 15%;
|
|
319
|
+
--senren-muted-foreground: 36 10% 62%;
|
|
320
|
+
--senren-card: 30 16% 11%;
|
|
321
|
+
--senren-card-foreground: 40 24% 94%;
|
|
322
|
+
--senren-popover: 30 16% 11%;
|
|
323
|
+
--senren-popover-foreground: 40 24% 94%;
|
|
324
|
+
--senren-border: 30 12% 21%;
|
|
325
|
+
--senren-input: 30 12% 21%;
|
|
326
|
+
--senren-ring: 32 95% 55%;
|
|
327
|
+
--senren-primary: 36 90% 58%;
|
|
328
|
+
--senren-primary-foreground: 30 25% 10%;
|
|
329
|
+
--senren-secondary: 30 14% 17%;
|
|
330
|
+
--senren-secondary-foreground: 40 60% 86%;
|
|
331
|
+
--senren-accent: 32 30% 22%;
|
|
332
|
+
--senren-accent-foreground: 38 80% 86%;
|
|
333
|
+
--senren-destructive: 4 68% 52%;
|
|
334
|
+
--senren-destructive-foreground: 40 30% 98%;
|
|
335
|
+
--senren-success: 140 50% 42%;
|
|
336
|
+
--senren-success-foreground: 30 18% 8%;
|
|
337
|
+
--senren-warning: 32 90% 56%;
|
|
338
|
+
--senren-warning-foreground: 30 18% 8%;
|
|
339
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
|
+
require 'senren/rails/marker_block'
|
|
5
|
+
require 'senren/rails/safe_write'
|
|
4
6
|
|
|
5
7
|
module Senren
|
|
6
8
|
module Rails
|
|
@@ -21,6 +23,7 @@ module Senren
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def sync!
|
|
26
|
+
assert_distinct_adapters!
|
|
24
27
|
paths.ensure_agent_dirs!
|
|
25
28
|
files = []
|
|
26
29
|
files << write_full_file(paths.agent_rules_file, render_source_rules)
|
|
@@ -31,8 +34,46 @@ module Senren
|
|
|
31
34
|
files
|
|
32
35
|
end
|
|
33
36
|
|
|
37
|
+
# Each adapter gets different content, so two of them resolving to the
|
|
38
|
+
# same file means the last write silently wins.
|
|
39
|
+
#
|
|
40
|
+
# `ln -s AGENTS.md CLAUDE.md` is a normal way to keep one set of agent
|
|
41
|
+
# instructions, and now that in-repo symlinks are allowed it reaches here
|
|
42
|
+
# rather than being refused as an escape.
|
|
43
|
+
#
|
|
44
|
+
# Public so ComponentInstaller can run it as a preflight. The first
|
|
45
|
+
# version of this check lived inside sync!, which runs after the copier,
|
|
46
|
+
# so `senren:add` failed with components already on disk and the ledger
|
|
47
|
+
# already written.
|
|
48
|
+
def assert_distinct_adapters!
|
|
49
|
+
collisions = adapter_targets.group_by { |path, _| SafeWrite.real_target(path) }
|
|
50
|
+
.select { |_, group| group.size > 1 }
|
|
51
|
+
return if collisions.empty?
|
|
52
|
+
|
|
53
|
+
raise ArgumentError, collision_message(collisions)
|
|
54
|
+
end
|
|
55
|
+
|
|
34
56
|
private
|
|
35
57
|
|
|
58
|
+
def adapter_targets
|
|
59
|
+
{
|
|
60
|
+
paths.codex_agents_md => 'AGENTS.md',
|
|
61
|
+
paths.claude_md => 'CLAUDE.md',
|
|
62
|
+
paths.copilot_instructions => '.github/copilot-instructions.md',
|
|
63
|
+
paths.cursor_rule_file => '.cursor/rules/senren.mdc'
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def collision_message(collisions)
|
|
68
|
+
detail = collisions.map do |target, group|
|
|
69
|
+
"#{group.map(&:last).join(' and ')} both resolve to #{target}"
|
|
70
|
+
end.join('; ')
|
|
71
|
+
|
|
72
|
+
'Senren writes different instructions to each agent adapter, so these cannot share a file: ' \
|
|
73
|
+
"#{detail}. Replace the link with a real file, or have one adapter reference the other " \
|
|
74
|
+
'by path instead of linking to it.'
|
|
75
|
+
end
|
|
76
|
+
|
|
36
77
|
def installed_names
|
|
37
78
|
path = paths.installed_components
|
|
38
79
|
return [] unless path.exist?
|
|
@@ -57,8 +98,17 @@ module Senren
|
|
|
57
98
|
- Use ViewComponent for reusable UI.
|
|
58
99
|
- Use Turbo for server state.
|
|
59
100
|
- Use Stimulus only for local behavior.
|
|
60
|
-
-
|
|
101
|
+
- Keep interactivity in Stimulus. These components render on the server,
|
|
102
|
+
so a client-side framework alongside them means two systems own the
|
|
103
|
+
same state.
|
|
61
104
|
- Use semantic Tailwind tokens; do not hard-code color families.
|
|
105
|
+
- Never add `app/components` to `config.assets.paths`. Propshaft
|
|
106
|
+
publishes every file under an asset path, so component `.rb` and
|
|
107
|
+
`.html.erb` source is precompiled into `public/assets` and served
|
|
108
|
+
over HTTP. Put sidecar assets in `app/components/assets` instead.
|
|
109
|
+
- A button inside a form needs no `type:`. ButtonComponent omits the
|
|
110
|
+
attribute so the browser submits, as plain HTML does. Pass
|
|
111
|
+
`type: :button` only for a trigger that must not submit.
|
|
62
112
|
|
|
63
113
|
## Important Files
|
|
64
114
|
|
|
@@ -82,7 +132,7 @@ module Senren
|
|
|
82
132
|
- Prefer Senren components before custom HTML.
|
|
83
133
|
- Keep reusable UI in ViewComponent.
|
|
84
134
|
- Use Turbo for server state, Stimulus for local behavior.
|
|
85
|
-
-
|
|
135
|
+
- Keep interactivity in Stimulus rather than a client-side framework.
|
|
86
136
|
- Use semantic Tailwind tokens.
|
|
87
137
|
MD
|
|
88
138
|
end
|
|
@@ -106,7 +156,7 @@ module Senren
|
|
|
106
156
|
- Prefer Senren components before custom HTML.
|
|
107
157
|
- Reusable UI must use ViewComponent.
|
|
108
158
|
- Turbo handles server state; Stimulus handles local behavior.
|
|
109
|
-
-
|
|
159
|
+
- Keep interactivity in Stimulus rather than a client-side framework.
|
|
110
160
|
- Use semantic Tailwind tokens.
|
|
111
161
|
MD
|
|
112
162
|
end
|
|
@@ -120,7 +170,7 @@ module Senren
|
|
|
120
170
|
- Prefer Senren components before custom HTML.
|
|
121
171
|
- Reusable UI uses ViewComponent.
|
|
122
172
|
- Turbo for server state, Stimulus for local behavior.
|
|
123
|
-
-
|
|
173
|
+
- Keep interactivity in Stimulus rather than a client-side framework.
|
|
124
174
|
- Use semantic Tailwind tokens.
|
|
125
175
|
MD
|
|
126
176
|
end
|
|
@@ -147,28 +197,24 @@ module Senren
|
|
|
147
197
|
|
|
148
198
|
def write_adapter_file(path, generated, prefix: '')
|
|
149
199
|
existing = path.exist? ? path.read : prefix.to_s
|
|
150
|
-
updated = inject(existing, generated)
|
|
200
|
+
updated = inject(existing, generated, label: path.to_s)
|
|
151
201
|
atomic_write(path, updated)
|
|
152
202
|
path
|
|
153
203
|
end
|
|
154
204
|
|
|
155
|
-
def inject(existing, generated)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"#{before}#{START_MARKER}\n\n#{generated.rstrip}\n\n#{END_MARKER}#{after}"
|
|
161
|
-
else
|
|
162
|
-
body = existing.rstrip
|
|
163
|
-
prefix = body.empty? ? '' : "#{body}\n\n"
|
|
164
|
-
"#{prefix}#{START_MARKER}\n\n#{generated.rstrip}\n\n#{END_MARKER}\n"
|
|
165
|
-
end
|
|
205
|
+
def inject(existing, generated, label: nil)
|
|
206
|
+
MarkerBlock.inject(
|
|
207
|
+
existing, generated,
|
|
208
|
+
start_marker: START_MARKER, end_marker: END_MARKER, label: label
|
|
209
|
+
)
|
|
166
210
|
end
|
|
167
211
|
|
|
212
|
+
# write_adapter_file reads its destination before rewriting it, so a
|
|
213
|
+
# symlinked .senren, .github or .cursor/rules did not merely redirect the
|
|
214
|
+
# write — it pulled outside content in and wrote it back out. SafeWrite
|
|
215
|
+
# refuses on the real path before either half of that happens.
|
|
168
216
|
def atomic_write(path, content)
|
|
169
|
-
|
|
170
|
-
File.write(tmp, content)
|
|
171
|
-
File.rename(tmp, path)
|
|
217
|
+
SafeWrite.write!(path, content, paths.root, path.to_s)
|
|
172
218
|
end
|
|
173
219
|
end
|
|
174
220
|
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
module Rails
|
|
5
|
+
# Refuses to let component source be served as a static asset.
|
|
6
|
+
#
|
|
7
|
+
# ViewComponent documents putting sidecar assets next to components, and the
|
|
8
|
+
# usual way to reach them is:
|
|
9
|
+
#
|
|
10
|
+
# config.assets.paths << Rails.root.join("app/components")
|
|
11
|
+
#
|
|
12
|
+
# With Propshaft that one line makes every file under app/components a
|
|
13
|
+
# servable asset, `.rb` and `.html.erb` included. Verified against Propshaft
|
|
14
|
+
# 1.3.2: 129 component source files resolved, `assets:precompile` copied all
|
|
15
|
+
# of them into public/assets/, and public/assets/.manifest.json listed each
|
|
16
|
+
# logical path next to its digested filename — so the digest is not even an
|
|
17
|
+
# obstacle. In production the web server hands them out with Rails never
|
|
18
|
+
# involved.
|
|
19
|
+
#
|
|
20
|
+
# Development is a different risk calculation, so there it warns. Production
|
|
21
|
+
# raises: a boot failure is recoverable, published source is not.
|
|
22
|
+
module AssetPathGuard
|
|
23
|
+
SOURCE_EXTENSIONS = %w[.rb .erb].freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# `production:` is injected rather than read from ::Rails so the guard can
|
|
28
|
+
# be unit tested without booting Rails, which is how the rest of this
|
|
29
|
+
# library's unit suite runs.
|
|
30
|
+
def check!(app, io: $stderr, production: production_env?)
|
|
31
|
+
offenders = offending_paths(app)
|
|
32
|
+
return true if offenders.empty?
|
|
33
|
+
|
|
34
|
+
message = message_for(offenders)
|
|
35
|
+
raise message if production
|
|
36
|
+
|
|
37
|
+
io.puts("[senren] WARNING: #{message}")
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Fails closed. `Rails.env.production?` let a conventional
|
|
42
|
+
# RAILS_ENV=staging deploy print one line of stderr and precompile the
|
|
43
|
+
# source anyway, and the same held for review apps and any custom
|
|
44
|
+
# environment name. `local?` is true only for development and test — the
|
|
45
|
+
# two environments where exposure is acceptable — so everything else is
|
|
46
|
+
# treated as deployed. Available since Rails 7.1, which is the floor.
|
|
47
|
+
def production_env?
|
|
48
|
+
return false unless defined?(::Rails) && ::Rails.respond_to?(:env)
|
|
49
|
+
return !::Rails.env.local? if ::Rails.env.respond_to?(:local?)
|
|
50
|
+
|
|
51
|
+
!%w[development test].include?(::Rails.env.to_s)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# An asset path is only a problem when component source actually sits
|
|
55
|
+
# under it, so an app that keeps sidecar assets in their own directory is
|
|
56
|
+
# left alone.
|
|
57
|
+
def offending_paths(app)
|
|
58
|
+
components = components_dir(app)
|
|
59
|
+
return [] unless components&.directory?
|
|
60
|
+
|
|
61
|
+
asset_paths(app).select { |path| publishes_source?(path, components) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Whether serving this asset path would serve component source.
|
|
65
|
+
#
|
|
66
|
+
# `source_files?` used to be asked once about app/components as a whole,
|
|
67
|
+
# which is the wrong question for a descendant path: with that shape,
|
|
68
|
+
# app/components/assets holding nothing but CSS looked identical to
|
|
69
|
+
# app/components/senren holding every component. Only the overlapping
|
|
70
|
+
# subtree can publish anything, and that subtree is the deeper of the two
|
|
71
|
+
# paths.
|
|
72
|
+
def publishes_source?(asset_path, components)
|
|
73
|
+
asset = Pathname.new(asset_path.to_s).expand_path
|
|
74
|
+
components = components.expand_path
|
|
75
|
+
return false unless overlap?(asset, components)
|
|
76
|
+
|
|
77
|
+
source_files?(asset.to_s.length >= components.to_s.length ? asset : components)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def components_dir(app)
|
|
81
|
+
app.root.join('app/components')
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def source_files?(dir)
|
|
87
|
+
SOURCE_EXTENSIONS.any? { |ext| Dir.glob(dir.join("**/*#{ext}")).any? }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def asset_paths(app)
|
|
91
|
+
app.config.respond_to?(:assets) ? Array(app.config.assets.paths) : []
|
|
92
|
+
rescue StandardError
|
|
93
|
+
[]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Two paths overlap when one contains the other, in EITHER direction, and
|
|
97
|
+
# the test has to be separator-aware:
|
|
98
|
+
#
|
|
99
|
+
# app/components is an ancestor -> publishes everything
|
|
100
|
+
# app/components/senren is a descendant -> publishes the components
|
|
101
|
+
# app/comp shares a prefix -> publishes nothing
|
|
102
|
+
#
|
|
103
|
+
# The first version tested only the ancestor direction with a bare
|
|
104
|
+
# start_with?, so it missed the descendant case — which is what a
|
|
105
|
+
# developer writes after reading this module's own remediation text — and
|
|
106
|
+
# it blocked production boots over an unrelated app/comp directory.
|
|
107
|
+
def overlap?(one, other)
|
|
108
|
+
a = "#{one}#{File::SEPARATOR}"
|
|
109
|
+
b = "#{other}#{File::SEPARATOR}"
|
|
110
|
+
|
|
111
|
+
a.start_with?(b) || b.start_with?(a)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def message_for(offenders)
|
|
115
|
+
<<~MESSAGE.strip
|
|
116
|
+
app/components is on the asset load path (#{offenders.join(', ')}).
|
|
117
|
+
|
|
118
|
+
Propshaft serves every file under an asset path, so your component
|
|
119
|
+
.rb and .html.erb source would be published — assets:precompile copies
|
|
120
|
+
them into public/assets and .manifest.json lists them by name.
|
|
121
|
+
|
|
122
|
+
Move sidecar assets into their own directory and add that instead, for
|
|
123
|
+
example app/components/assets rather than app/components.
|
|
124
|
+
MESSAGE
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
module Rails
|
|
5
|
+
# Ruby appended to a host app's existing BaseComponent when it predates the
|
|
6
|
+
# URL-aware component templates.
|
|
7
|
+
#
|
|
8
|
+
# This duplicates the helpers in
|
|
9
|
+
# lib/generators/senren/install/templates/base_component.rb.tt, because
|
|
10
|
+
# apps installed before those helpers existed never receive the template
|
|
11
|
+
# again. The two definitions are pinned together by
|
|
12
|
+
# test/security/component_url_security_test.rb: if they drift, migrated apps
|
|
13
|
+
# silently keep an older, weaker safe_url.
|
|
14
|
+
module BaseComponentPatch
|
|
15
|
+
# Single-quoted heredoc: the body is emitted verbatim, so the backslash
|
|
16
|
+
# escapes inside safe_url survive into the host app's file.
|
|
17
|
+
URL_HELPERS = <<~'RUBY'
|
|
18
|
+
|
|
19
|
+
# Added by senren:add for compatibility with URL-aware component templates.
|
|
20
|
+
require 'uri'
|
|
21
|
+
|
|
22
|
+
module Senren
|
|
23
|
+
class BaseComponent
|
|
24
|
+
SAFE_URL_PROTOCOLS = %w[http https mailto tel].freeze unless const_defined?(:SAFE_URL_PROTOCOLS)
|
|
25
|
+
SAFE_MEDIA_URL_PROTOCOLS = %w[http https].freeze unless const_defined?(:SAFE_MEDIA_URL_PROTOCOLS)
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def safe_url(value, fallback: '#', protocols: SAFE_URL_PROTOCOLS)
|
|
30
|
+
url = value.to_s.strip
|
|
31
|
+
return fallback if url.empty?
|
|
32
|
+
# Browsers treat "\" as "/" for special schemes and strip TAB/CR/LF
|
|
33
|
+
# before parsing, so "/\evil.example" and "/<TAB>/evil.example" would
|
|
34
|
+
# both slip past a plain "//" check and resolve off-origin.
|
|
35
|
+
return fallback if url.include?('\\')
|
|
36
|
+
return fallback if url.match?(/[[:cntrl:]]/)
|
|
37
|
+
return url if url.start_with?('#')
|
|
38
|
+
# Any leading "//" is protocol-relative regardless of how many slashes
|
|
39
|
+
# follow. Rejecting here rather than relying on URI.parse matters:
|
|
40
|
+
# URI.parse("///evil.example") reports no scheme and no host, so the
|
|
41
|
+
# scheme-less fallback below would otherwise hand back a URL the browser
|
|
42
|
+
# resolves to https://evil.example/.
|
|
43
|
+
return fallback if url.start_with?('//')
|
|
44
|
+
return url if url.start_with?('/')
|
|
45
|
+
|
|
46
|
+
uri = URI.parse(url)
|
|
47
|
+
return url if uri.scheme && Array(protocols).map(&:to_s).include?(uri.scheme.downcase)
|
|
48
|
+
return fallback if uri.host
|
|
49
|
+
return url unless uri.scheme
|
|
50
|
+
|
|
51
|
+
fallback
|
|
52
|
+
rescue URI::InvalidURIError
|
|
53
|
+
fallback
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def safe_media_url(value, fallback: nil)
|
|
57
|
+
safe_url(value, fallback: fallback, protocols: SAFE_MEDIA_URL_PROTOCOLS)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
RUBY
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|