main
py 1,001 lines 46.3 KB
Raw
1 #!/usr/bin/env python3
2 """Write all truncated ERB views for the siGit si Rails app."""
3
4 import os
5
6 # Reusable SVG path fragments
7 GIT_PATH = (
8 "M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61"
9 "c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 "
10 "5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0"
11 "C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77"
12 "a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7"
13 "A3.37 3.37 0 0 0 9 18.13V22"
14 )
15
16 REPO_PATH_16 = (
17 "M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5"
18 "a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8"
19 "a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5"
20 "Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"
21 )
22
23 COMMIT_PATH_16 = (
24 "M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32"
25 "a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5"
26 "Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"
27 )
28
29 BRANCH_PATH_16 = (
30 "M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6"
31 "a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372"
32 "a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4"
33 "a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25"
34 "Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0"
35 "Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5"
36 "ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"
37 )
38
39 FOLDER_PATH_16 = (
40 "M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5"
41 "A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5"
42 "a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1Z"
43 )
44
45 FILE_PATH_16 = (
46 "M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513"
47 "l2.914 2.914c.329.328.513.773.513 1.237v9.586"
48 "A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25"
49 "Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5"
50 "a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5"
51 "Zm6.75.062V4.25c0 .138.112.25.25.25h2.688Z"
52 )
53
54 README_PATH_16 = (
55 "M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501"
56 "A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5"
57 "a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621"
58 "a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75"
59 "a.75.75 0 0 1-.75-.75"
60 "Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9"
61 "h3.757a3.75 3.75 0 0 1 1.994.574"
62 "ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9"
63 "h-3.495a2.25 2.25 0 0 0-2.25 2.25Z"
64 )
65
66 PLUS_PATH_16 = (
67 "M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25"
68 "a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75"
69 "A.75.75 0 0 1 7.75 2Z"
70 )
71
72 CHEVRON_PATH = "M4 6l4 4 4-4"
73
74 EMAIL_PATH_16 = (
75 "M1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5"
76 "A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25v-8.5"
77 "C0 2.784.784 2 1.75 2"
78 "ZM1.5 12.251c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V5.809"
79 "L8.38 9.397a.75.75 0 0 1-.76 0L1.5 5.809v6.442"
80 "Zm13-8.181v-.32a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25v.32L8 7.88Z"
81 )
82
83 LOCK_PATH_16 = (
84 "M4 4a4 4 0 0 1 8 0v2h.25c.966 0 1.75.784 1.75 1.75v5.5"
85 "A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-5.5"
86 "C2 6.784 2.784 6 3.75 6H4Zm8.25 3.5h-8.5a.25.25 0 0 0-.25.25v5.5"
87 "c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-5.5"
88 "a.25.25 0 0 0-.25-.25ZM10.5 6V4a2.5 2.5 0 1 0-5 0v2Z"
89 )
90
91 SHIELD_PATH_24 = (
92 "M9 12.75 11.25 15 15 9.75"
93 "m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749"
94 "c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622"
95 "0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z"
96 )
97
98 FOLDER_OPEN_PATH = (
99 "M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026"
100 "m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932"
101 "H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542"
102 "m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44"
103 "l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776"
104 )
105
106 files = {}
107
108 # ---------------------------------------------------------------------------
109 # layouts/application.html.erb
110 # ---------------------------------------------------------------------------
111 files["app/views/layouts/application.html.erb"] = (
112 "<!DOCTYPE html>\n"
113 '<html lang="en" class="h-full">\n'
114 " <head>\n"
115 ' <meta charset="utf-8">\n'
116 ' <meta name="viewport" content="width=device-width, initial-scale=1">\n'
117 ' <meta name="description" content="siGit \u2014 cloud repository hosting">\n'
118 ' <title><%= content_for?(:title) ? "#{yield(:title)} \u00b7 siGit" : "siGit" %></title>\n'
119 ' <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>\n'
120 " <%= javascript_importmap_tags %>\n"
121 " <%= csrf_meta_tags %>\n"
122 " </head>\n"
123 "\n"
124 ' <body class="h-full bg-white">\n'
125 ' <%= render "shared/navbar" %>\n'
126 "\n"
127 " <% if notice.present? %>\n"
128 ' <div class="bg-green-50 border-b border-green-200 px-4 py-3 text-sm text-green-800 text-center" role="alert">\n'
129 " <%= notice %>\n"
130 " </div>\n"
131 " <% end %>\n"
132 " <% if alert.present? %>\n"
133 ' <div class="bg-amber-50 border-b border-amber-200 px-4 py-3 text-sm text-amber-800 text-center" role="alert">\n'
134 " <%= alert %>\n"
135 " </div>\n"
136 " <% end %>\n"
137 "\n"
138 " <main>\n"
139 " <%= yield %>\n"
140 " </main>\n"
141 "\n"
142 ' <footer class="border-t border-gray-100 mt-16">\n'
143 ' <div class="max-w-6xl mx-auto px-4 sm:px-6 py-8 flex items-center justify-between text-xs text-gray-400">\n'
144 " <span>siGit si &mdash; cloud repository hosting</span>\n"
145 ' <span>Built with <a href="https://rubyonrails.org" class="hover:text-gray-600">Rails 8</a></span>\n'
146 " </div>\n"
147 " </footer>\n"
148 " </body>\n"
149 "</html>\n"
150 )
151
152 # ---------------------------------------------------------------------------
153 # shared/_navbar.html.erb
154 # ---------------------------------------------------------------------------
155 files["app/views/shared/_navbar.html.erb"] = (
156 '<header class="border-b border-gray-200 bg-white sticky top-0 z-50">\n'
157 ' <nav class="max-w-6xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-6">\n'
158 "\n"
159 ' <div class="flex items-center gap-6">\n'
160 ' <%= link_to root_path, class: "flex items-center gap-2 text-gray-900 hover:text-brand-500 transition-colors" do %>\n'
161 ' <svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor"\n'
162 ' stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n'
163 f' <path d="{GIT_PATH}"/>\n'
164 " </svg>\n"
165 ' <span class="font-semibold text-base tracking-tight">siGit</span>\n'
166 " <% end %>\n"
167 "\n"
168 " <% if signed_in? %>\n"
169 ' <div class="hidden sm:flex items-center gap-1">\n'
170 ' <%= link_to "Explore", "#", class: "nav-link px-2 py-1 rounded hover:bg-gray-50" %>\n'
171 " </div>\n"
172 " <% end %>\n"
173 " </div>\n"
174 "\n"
175 ' <div class="flex items-center gap-3">\n'
176 " <% if signed_in? %>\n"
177 ' <%= link_to new_repository_path, class: "btn-secondary text-xs py-1.5 px-3 hidden sm:inline-flex" do %>\n'
178 ' <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">\n'
179 f' <path d="{PLUS_PATH_16}"/>\n'
180 " </svg>\n"
181 " New\n"
182 " <% end %>\n"
183 "\n"
184 ' <div class="relative group">\n'
185 ' <button class="flex items-center gap-2 hover:opacity-80 transition-opacity">\n'
186 ' <img src="<%= current_user.avatar_url_or_default %>"\n'
187 ' alt="<%= current_user.display_name_or_username %>"\n'
188 ' class="w-7 h-7 rounded-full border border-gray-200">\n'
189 ' <span class="text-sm font-medium text-gray-700 hidden sm:block"><%= current_user.username %></span>\n'
190 ' <svg class="w-3.5 h-3.5 text-gray-400" fill="none" viewBox="0 0 16 16"\n'
191 ' stroke="currentColor" stroke-width="2">\n'
192 f' <path stroke-linecap="round" stroke-linejoin="round" d="{CHEVRON_PATH}"/>\n'
193 " </svg>\n"
194 " </button>\n"
195 "\n"
196 ' <div class="absolute right-0 top-full mt-1 w-48 bg-white border border-gray-200\n'
197 " rounded shadow-lg py-1 opacity-0 invisible\n"
198 " group-hover:opacity-100 group-hover:visible\n"
199 ' transition-all duration-150 z-50">\n'
200 ' <div class="px-3 py-2 border-b border-gray-100">\n'
201 ' <p class="text-xs font-medium text-gray-900"><%= current_user.display_name_or_username %></p>\n'
202 ' <p class="text-xs text-gray-500"><%= current_user.email %></p>\n'
203 " </div>\n"
204 " <%= link_to user_profile_path(current_user.username),\n"
205 ' class: "block px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50" do %>Your repositories<% end %>\n'
206 " <%= link_to settings_path,\n"
207 ' class: "block px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50" do %>Settings<% end %>\n'
208 ' <div class="border-t border-gray-100 mt-1">\n'
209 " <%= button_to signout_path, method: :delete,\n"
210 ' class: "block w-full text-left px-3 py-1.5 text-sm text-red-600 hover:bg-red-50 transition-colors" do %>\n'
211 " Sign out\n"
212 " <% end %>\n"
213 " </div>\n"
214 " </div>\n"
215 " </div>\n"
216 " <% else %>\n"
217 ' <%= link_to "Sign in", signin_path, class: "btn-primary text-xs py-1.5 px-4" %>\n'
218 " <% end %>\n"
219 " </div>\n"
220 " </nav>\n"
221 "</header>\n"
222 )
223
224 # ---------------------------------------------------------------------------
225 # pages/home.html.erb
226 # ---------------------------------------------------------------------------
227 files["app/views/pages/home.html.erb"] = (
228 '<% content_for :title, "Welcome" %>\n'
229 "\n"
230 '<div class="min-h-screen">\n'
231 ' <section class="border-b border-gray-100 bg-white">\n'
232 ' <div class="max-w-4xl mx-auto px-4 sm:px-6 py-24 text-center">\n'
233 ' <h1 class="text-4xl sm:text-5xl font-semibold text-gray-900 tracking-tight mb-6 leading-tight">\n'
234 " Code, together.<br>\n"
235 ' <span class="text-brand-500">Simply.</span>\n'
236 " </h1>\n"
237 ' <p class="text-lg text-gray-500 max-w-xl mx-auto mb-10 font-light leading-relaxed">\n'
238 " siGit is a minimal cloud repository host built for clarity and speed.\n"
239 " Host your Git repositories without noise.\n"
240 " </p>\n"
241 ' <%= link_to signin_path, class: "btn-primary px-8 py-3 text-base" do %>\n'
242 ' <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor"\n'
243 ' stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n'
244 f' <path d="{GIT_PATH}"/>\n'
245 " </svg>\n"
246 " Sign in with smbCloud\n"
247 " <% end %>\n"
248 " </div>\n"
249 " </section>\n"
250 "\n"
251 ' <section class="max-w-4xl mx-auto px-4 sm:px-6 py-20">\n'
252 ' <div class="grid grid-cols-1 sm:grid-cols-3 gap-12">\n'
253 "\n"
254 " <div>\n"
255 ' <div class="w-8 h-8 mb-4 text-brand-500">\n'
256 ' <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">\n'
257 ' <path stroke-linecap="round" stroke-linejoin="round"\n'
258 f' d="{FOLDER_OPEN_PATH}"/>\n'
259 " </svg>\n"
260 " </div>\n"
261 ' <h3 class="font-semibold text-gray-900 mb-2">Repository Hosting</h3>\n'
262 ' <p class="text-sm text-gray-500 leading-relaxed">Create and manage Git repositories\n'
263 " with a clean, distraction-free interface.</p>\n"
264 " </div>\n"
265 "\n"
266 " <div>\n"
267 ' <div class="w-8 h-8 mb-4 text-brand-500">\n'
268 ' <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">\n'
269 ' <path stroke-linecap="round" stroke-linejoin="round"\n'
270 ' d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5"/>\n'
271 " </svg>\n"
272 " </div>\n"
273 ' <h3 class="font-semibold text-gray-900 mb-2">Code Browsing</h3>\n'
274 ' <p class="text-sm text-gray-500 leading-relaxed">Browse your code with syntax\n'
275 " highlighting, file tree navigation, and commit history.</p>\n"
276 " </div>\n"
277 "\n"
278 " <div>\n"
279 ' <div class="w-8 h-8 mb-4 text-brand-500">\n'
280 ' <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">\n'
281 ' <path stroke-linecap="round" stroke-linejoin="round"\n'
282 ' d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026'
283 "-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591"
284 'l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 0 1 21.75 8.25Z"/>\n'
285 " </svg>\n"
286 " </div>\n"
287 ' <h3 class="font-semibold text-gray-900 mb-2">Secure Auth</h3>\n'
288 ' <p class="text-sm text-gray-500 leading-relaxed">Authentication via smbCloud Auth\n'
289 " &mdash; secure and token-based. No passwords stored by siGit.</p>\n"
290 " </div>\n"
291 "\n"
292 " </div>\n"
293 " </section>\n"
294 "</div>\n"
295 )
296
297 # ---------------------------------------------------------------------------
298 # pages/dashboard.html.erb
299 # ---------------------------------------------------------------------------
300 files["app/views/pages/dashboard.html.erb"] = (
301 '<% content_for :title, "Dashboard" %>\n'
302 "\n"
303 '<div class="max-w-6xl mx-auto px-4 sm:px-6 py-8">\n'
304 ' <div class="grid grid-cols-1 lg:grid-cols-4 gap-8">\n'
305 "\n"
306 ' <aside class="lg:col-span-1">\n'
307 ' <div class="text-center sm:text-left">\n'
308 ' <img src="<%= current_user.avatar_url_or_default %>"\n'
309 ' alt="<%= current_user.display_name_or_username %>"\n'
310 ' class="w-20 h-20 rounded-full border border-gray-200 mx-auto sm:mx-0 mb-4">\n'
311 ' <h2 class="font-semibold text-gray-900 text-base"><%= current_user.display_name_or_username %></h2>\n'
312 ' <p class="text-sm text-gray-500"><%= current_user.username %></p>\n'
313 " </div>\n"
314 ' <div class="mt-6">\n'
315 ' <%= link_to new_repository_path, class: "btn-secondary w-full justify-center" do %>\n'
316 ' <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">\n'
317 f' <path d="{PLUS_PATH_16}"/>\n'
318 " </svg>\n"
319 " New repository\n"
320 " <% end %>\n"
321 " </div>\n"
322 " </aside>\n"
323 "\n"
324 ' <main class="lg:col-span-3">\n'
325 ' <div class="flex items-center justify-between mb-4">\n'
326 ' <h1 class="text-base font-semibold text-gray-900">Your repositories</h1>\n'
327 " <%= link_to user_profile_path(current_user.username),\n"
328 ' class: "text-xs text-brand-500 hover:underline" do %>View all<% end %>\n'
329 " </div>\n"
330 "\n"
331 " <% if @repositories.any? %>\n"
332 ' <div class="card divide-y divide-gray-100">\n'
333 " <% @repositories.each do |repo| %>\n"
334 ' <div class="px-4 py-3 hover:bg-gray-50 transition-colors">\n'
335 ' <div class="flex items-start justify-between gap-4">\n'
336 ' <div class="min-w-0 flex-1">\n'
337 " <%= link_to repository_path(current_user.username, repo.name),\n"
338 ' class: "font-medium text-brand-500 hover:underline text-sm" do %><%= repo.name %><% end %>\n'
339 " <% if repo.description.present? %>\n"
340 ' <p class="text-xs text-gray-500 mt-0.5 truncate"><%= repo.description %></p>\n'
341 " <% end %>\n"
342 " </div>\n"
343 ' <div class="flex items-center gap-3 text-xs text-gray-400 shrink-0">\n'
344 " <% if repo.is_private %>\n"
345 ' <span class="badge-gray">Private</span>\n'
346 " <% end %>\n"
347 " <span><%= time_ago_in_words(repo.updated_at) %> ago</span>\n"
348 " </div>\n"
349 " </div>\n"
350 " </div>\n"
351 " <% end %>\n"
352 " </div>\n"
353 " <% else %>\n"
354 ' <div class="card px-6 py-12 text-center">\n'
355 ' <svg class="w-12 h-12 mx-auto text-gray-300 mb-4" viewBox="0 0 24 24"\n'
356 ' fill="none" stroke="currentColor" stroke-width="1">\n'
357 ' <path stroke-linecap="round" stroke-linejoin="round"\n'
358 f' d="{FOLDER_OPEN_PATH}"/>\n'
359 " </svg>\n"
360 ' <p class="text-sm text-gray-500 mb-4">No repositories yet.</p>\n'
361 ' <%= link_to new_repository_path, class: "btn-primary" do %>Create your first repository<% end %>\n'
362 " </div>\n"
363 " <% end %>\n"
364 " </main>\n"
365 "\n"
366 " </div>\n"
367 "</div>\n"
368 )
369
370 # ---------------------------------------------------------------------------
371 # users/show.html.erb
372 # ---------------------------------------------------------------------------
373 files["app/views/users/show.html.erb"] = (
374 "<% content_for :title, @profile_user.display_name_or_username %>\n"
375 "\n"
376 '<div class="max-w-6xl mx-auto px-4 sm:px-6 py-10">\n'
377 ' <div class="grid grid-cols-1 lg:grid-cols-4 gap-10">\n'
378 "\n"
379 ' <aside class="lg:col-span-1">\n'
380 ' <img src="<%= @profile_user.avatar_url_or_default %>"\n'
381 ' alt="<%= @profile_user.display_name_or_username %>"\n'
382 ' class="w-full max-w-[200px] rounded-full border border-gray-200 mb-4">\n'
383 ' <h1 class="text-xl font-semibold text-gray-900"><%= @profile_user.display_name_or_username %></h1>\n'
384 ' <p class="text-gray-500 text-sm"><%= @profile_user.username %></p>\n'
385 " <% if @profile_user.email.present? %>\n"
386 ' <div class="flex items-center gap-2 mt-3 text-sm text-gray-500">\n'
387 ' <svg class="w-4 h-4 shrink-0" viewBox="0 0 16 16" fill="currentColor">\n'
388 f' <path d="{EMAIL_PATH_16}"/>\n'
389 " </svg>\n"
390 " <%= @profile_user.email %>\n"
391 " </div>\n"
392 " <% end %>\n"
393 " </aside>\n"
394 "\n"
395 ' <main class="lg:col-span-3">\n'
396 ' <div class="flex items-center justify-between mb-4">\n'
397 ' <h2 class="text-base font-semibold text-gray-900">\n'
398 " Repositories\n"
399 ' <span class="badge-gray ml-1 align-middle"><%= @repositories.count %></span>\n'
400 " </h2>\n"
401 " <% if signed_in? && current_user == @profile_user %>\n"
402 ' <%= link_to new_repository_path, class: "btn-secondary text-xs" do %>New<% end %>\n'
403 " <% end %>\n"
404 " </div>\n"
405 "\n"
406 " <% if @repositories.any? %>\n"
407 ' <div class="space-y-3">\n'
408 " <% @repositories.each do |repo| %>\n"
409 ' <div class="card px-5 py-4 hover:border-gray-300 transition-colors">\n'
410 ' <div class="flex items-start justify-between gap-4">\n'
411 ' <div class="min-w-0 flex-1">\n'
412 ' <div class="flex items-center gap-2 mb-1">\n'
413 " <%= link_to repo.name,\n"
414 " repository_path(@profile_user.username, repo.name),\n"
415 ' class: "font-semibold text-brand-500 hover:underline" %>\n'
416 " <% if repo.is_private %>\n"
417 ' <span class="badge-gray">Private</span>\n'
418 " <% end %>\n"
419 " </div>\n"
420 " <% if repo.description.present? %>\n"
421 ' <p class="text-sm text-gray-500"><%= repo.description %></p>\n'
422 " <% end %>\n"
423 " </div>\n"
424 ' <div class="text-xs text-gray-400 shrink-0">\n'
425 " <%= time_ago_in_words(repo.updated_at) %> ago\n"
426 " </div>\n"
427 " </div>\n"
428 " </div>\n"
429 " <% end %>\n"
430 " </div>\n"
431 " <% else %>\n"
432 ' <div class="card px-6 py-12 text-center">\n'
433 ' <p class="text-sm text-gray-400">No public repositories.</p>\n'
434 " </div>\n"
435 " <% end %>\n"
436 " </main>\n"
437 "\n"
438 " </div>\n"
439 "</div>\n"
440 )
441
442 # ---------------------------------------------------------------------------
443 # repositories/show.html.erb
444 # ---------------------------------------------------------------------------
445 files["app/views/repositories/show.html.erb"] = (
446 "<% content_for :title, @repository.full_name %>\n"
447 "\n"
448 '<div class="border-b border-gray-200 bg-white">\n'
449 ' <div class="max-w-6xl mx-auto px-4 sm:px-6 pt-6 pb-0">\n'
450 "\n"
451 ' <div class="flex items-center gap-2 text-sm mb-4">\n'
452 ' <svg class="w-4 h-4 text-gray-400" viewBox="0 0 16 16" fill="currentColor">\n'
453 f' <path d="{REPO_PATH_16}"/>\n'
454 " </svg>\n"
455 " <%= link_to @owner.username, user_profile_path(@owner.username),\n"
456 ' class: "text-brand-500 hover:underline font-medium" %>\n'
457 ' <span class="text-gray-400">/</span>\n'
458 " <%= link_to @repository.name, repository_path(@owner.username, @repository.name),\n"
459 ' class: "text-brand-500 hover:underline font-semibold" %>\n'
460 " <% if @repository.is_private %>\n"
461 ' <span class="badge-gray ml-1">Private</span>\n'
462 " <% end %>\n"
463 " </div>\n"
464 "\n"
465 " <% if @repository.description.present? %>\n"
466 ' <p class="text-sm text-gray-600 mb-4"><%= @repository.description %></p>\n'
467 " <% end %>\n"
468 "\n"
469 ' <div class="flex items-center gap-0 -mb-px">\n'
470 " <%= link_to repository_path(@owner.username, @repository.name),\n"
471 ' class: "tab-item active flex items-center gap-1.5" do %>\n'
472 ' <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">\n'
473 f' <path d="{REPO_PATH_16}"/>\n'
474 " </svg>\n"
475 " Code\n"
476 " <% end %>\n"
477 " <% if @repository.initialized? %>\n"
478 " <%= link_to repository_commits_path(@owner.username, @repository.name,\n"
479 " @branch || @repository.default_branch),\n"
480 ' class: "tab-item flex items-center gap-1.5" do %>\n'
481 ' <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">\n'
482 f' <path d="{COMMIT_PATH_16}"/>\n'
483 " </svg>\n"
484 " Commits\n"
485 " <% if @commit_count&.> 0 %>\n"
486 ' <span class="badge-gray text-xs"><%= number_with_delimiter(@commit_count) %></span>\n'
487 " <% end %>\n"
488 " <% end %>\n"
489 " <% end %>\n"
490 " </div>\n"
491 " </div>\n"
492 "</div>\n"
493 "\n"
494 '<div class="max-w-6xl mx-auto px-4 sm:px-6 py-6">\n'
495 " <% if @empty %>\n"
496 ' <div class="card p-8 text-center max-w-2xl mx-auto">\n'
497 ' <svg class="w-16 h-16 mx-auto text-gray-200 mb-5" viewBox="0 0 24 24"\n'
498 ' fill="none" stroke="currentColor" stroke-width="1"\n'
499 ' stroke-linecap="round" stroke-linejoin="round">\n'
500 f' <path d="{GIT_PATH}"/>\n'
501 " </svg>\n"
502 ' <h2 class="text-lg font-semibold text-gray-900 mb-2">This repository is empty</h2>\n'
503 ' <p class="text-sm text-gray-500 mb-6">Get started by pushing your first commit.</p>\n'
504 ' <div class="code-container text-left p-4 max-w-lg mx-auto text-xs font-mono space-y-0.5">\n'
505 ' <p class="text-gray-400 mb-1"># Clone and push</p>\n'
506 ' <p class="text-gray-700">git clone git@sigitsi.com:<%= @owner.username %>/<%= @repository.name %></p>\n'
507 ' <p class="text-gray-700">cd <%= @repository.name %></p>\n'
508 ' <p class="text-gray-700">echo "# <%= @repository.name %>" &gt; README.md</p>\n'
509 ' <p class="text-gray-700">git add . &amp;&amp; git commit -m "Initial commit"</p>\n'
510 ' <p class="text-gray-700">git push origin main</p>\n'
511 " </div>\n"
512 " </div>\n"
513 " <% else %>\n"
514 "\n"
515 ' <div class="card rounded-b-none border-b-0 px-4 py-2.5\n'
516 ' flex items-center justify-between gap-4 bg-gray-50">\n'
517 ' <div class="flex items-center gap-3">\n'
518 ' <span class="btn-secondary py-1 px-3 text-xs font-mono flex items-center gap-1.5 cursor-default">\n'
519 ' <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">\n'
520 f' <path d="{BRANCH_PATH_16}"/>\n'
521 " </svg>\n"
522 " <%= @branch %>\n"
523 " </span>\n"
524 " <% if @recent_commits.first %>\n"
525 " <% commit = @recent_commits.first %>\n"
526 ' <span class="text-xs text-gray-500">\n'
527 " <%= link_to commit[:short_sha],\n"
528 " repository_commit_path(@owner.username, @repository.name, commit[:sha]),\n"
529 ' class: "font-mono text-brand-500 hover:underline" %>\n'
530 " &middot; <%= commit[:subject] %>\n"
531 ' &middot; <span class="text-gray-400"><%= time_ago_in_words(commit[:authored_date]) %> ago</span>\n'
532 " </span>\n"
533 " <% end %>\n"
534 " </div>\n"
535 ' <div class="flex items-center gap-3 text-xs text-gray-500">\n'
536 " <%= link_to repository_commits_path(@owner.username, @repository.name, @branch),\n"
537 ' class: "flex items-center gap-1 hover:text-gray-700" do %>\n'
538 ' <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">\n'
539 f' <path d="{COMMIT_PATH_16}"/>\n'
540 " </svg>\n"
541 " <%= number_with_delimiter(@commit_count) %> commits\n"
542 " <% end %>\n"
543 " </div>\n"
544 " </div>\n"
545 "\n"
546 ' <div class="card rounded-t-none">\n'
547 " <% @tree.each do |entry| %>\n"
548 ' <div class="file-row">\n'
549 ' <% if entry[:type] == "tree" %>\n'
550 ' <svg class="w-4 h-4 text-brand-300 shrink-0" viewBox="0 0 16 16" fill="currentColor">\n'
551 f' <path d="{FOLDER_PATH_16}"/>\n'
552 " </svg>\n"
553 " <%= link_to entry[:name],\n"
554 " repository_tree_path(@owner.username, @repository.name, @branch, entry[:path]),\n"
555 ' class: "text-brand-500 hover:underline font-medium flex-1" %>\n'
556 " <% else %>\n"
557 ' <svg class="w-4 h-4 text-gray-400 shrink-0" viewBox="0 0 16 16" fill="currentColor">\n'
558 f' <path d="{FILE_PATH_16}"/>\n'
559 " </svg>\n"
560 " <%= link_to entry[:name],\n"
561 " repository_blob_path(@owner.username, @repository.name, @branch, entry[:path]),\n"
562 ' class: "text-gray-800 hover:text-brand-500 hover:underline flex-1" %>\n'
563 " <% end %>\n"
564 " </div>\n"
565 " <% end %>\n"
566 " </div>\n"
567 "\n"
568 " <% if @readme_html %>\n"
569 ' <div class="card mt-6">\n'
570 ' <div class="px-4 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">\n'
571 ' <svg class="w-4 h-4 text-gray-400" viewBox="0 0 16 16" fill="currentColor">\n'
572 f' <path d="{README_PATH_16}"/>\n'
573 " </svg>\n"
574 ' <span class="text-xs font-medium text-gray-600"><%= @readme_filename %></span>\n'
575 " </div>\n"
576 ' <div class="px-6 py-6 prose-readme">\n'
577 " <%= @readme_html %>\n"
578 " </div>\n"
579 " </div>\n"
580 " <% end %>\n"
581 "\n"
582 " <% end %>\n"
583 "</div>\n"
584 )
585
586 # ---------------------------------------------------------------------------
587 # repositories/new.html.erb
588 # ---------------------------------------------------------------------------
589 files["app/views/repositories/new.html.erb"] = (
590 '<% content_for :title, "New repository" %>\n'
591 "\n"
592 '<div class="max-w-2xl mx-auto px-4 sm:px-6 py-12">\n'
593 ' <h1 class="text-xl font-semibold text-gray-900 mb-2">Create a new repository</h1>\n'
594 ' <p class="text-sm text-gray-500 mb-8">A repository contains all project files, including revision history.</p>\n'
595 "\n"
596 ' <%= form_with model: @repository, url: "/new", method: :post, class: "space-y-6" do |f| %>\n'
597 " <% if @repository.errors.any? %>\n"
598 ' <div class="border border-red-200 bg-red-50 rounded px-4 py-3">\n'
599 ' <p class="text-sm font-medium text-red-800 mb-1">Please fix the following errors:</p>\n'
600 ' <ul class="list-disc pl-4 space-y-0.5">\n'
601 " <% @repository.errors.full_messages.each do |msg| %>\n"
602 ' <li class="text-sm text-red-700"><%= msg %></li>\n'
603 " <% end %>\n"
604 " </ul>\n"
605 " </div>\n"
606 " <% end %>\n"
607 "\n"
608 ' <div class="flex items-end gap-2">\n'
609 ' <div class="flex-1 min-w-0">\n'
610 ' <label class="form-label">Owner</label>\n'
611 ' <div class="flex items-center gap-2 form-input bg-gray-50 text-gray-600 cursor-not-allowed">\n'
612 ' <img src="<%= current_user.avatar_url_or_default %>" class="w-4 h-4 rounded-full">\n'
613 " <span><%= current_user.username %></span>\n"
614 " </div>\n"
615 " </div>\n"
616 ' <div class="flex items-center pb-2.5 text-gray-400 font-light text-lg">/</div>\n'
617 ' <div class="flex-1 min-w-0">\n'
618 ' <%= f.label :name, "Repository name", class: "form-label" %>\n'
619 ' <%= f.text_field :name, class: "form-input", placeholder: "my-project", autofocus: true,\n'
620 ' pattern: "[a-zA-Z0-9._-]+",\n'
621 ' title: "Letters, numbers, hyphens, underscores, and dots only" %>\n'
622 " </div>\n"
623 " </div>\n"
624 "\n"
625 " <div>\n"
626 ' <%= f.label :description, "Description", class: "form-label" %>\n'
627 ' <span class="text-xs text-gray-400 ml-1">(optional)</span>\n'
628 ' <%= f.text_area :description, class: "form-input", rows: 2,\n'
629 ' placeholder: "Short description of your project" %>\n'
630 " </div>\n"
631 "\n"
632 ' <div class="border border-gray-200 rounded divide-y divide-gray-100">\n'
633 ' <label class="flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-gray-50">\n'
634 ' <%= f.radio_button :is_private, false, class: "mt-0.5" %>\n'
635 " <div>\n"
636 ' <div class="flex items-center gap-2">\n'
637 ' <svg class="w-4 h-4 text-gray-600" viewBox="0 0 16 16" fill="currentColor">\n'
638 f' <path d="{REPO_PATH_16}"/>\n'
639 " </svg>\n"
640 ' <span class="text-sm font-medium text-gray-900">Public</span>\n'
641 " </div>\n"
642 ' <p class="text-xs text-gray-500 mt-0.5">Anyone can see this repository.</p>\n'
643 " </div>\n"
644 " </label>\n"
645 ' <label class="flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-gray-50">\n'
646 ' <%= f.radio_button :is_private, true, class: "mt-0.5" %>\n'
647 " <div>\n"
648 ' <div class="flex items-center gap-2">\n'
649 ' <svg class="w-4 h-4 text-gray-600" viewBox="0 0 16 16" fill="currentColor">\n'
650 f' <path d="{LOCK_PATH_16}"/>\n'
651 " </svg>\n"
652 ' <span class="text-sm font-medium text-gray-900">Private</span>\n'
653 " </div>\n"
654 ' <p class="text-xs text-gray-500 mt-0.5">Only you can see this repository.</p>\n'
655 " </div>\n"
656 " </label>\n"
657 " </div>\n"
658 "\n"
659 " <div>\n"
660 ' <%= f.label :default_branch, "Default branch", class: "form-label" %>\n'
661 ' <%= f.text_field :default_branch, class: "form-input max-w-xs", value: "main" %>\n'
662 " </div>\n"
663 "\n"
664 ' <div class="pt-2 border-t border-gray-100 flex items-center gap-3">\n'
665 ' <%= f.submit "Create repository", class: "btn-primary cursor-pointer" %>\n'
666 ' <%= link_to "Cancel", root_path, class: "btn-ghost" %>\n'
667 " </div>\n"
668 " <% end %>\n"
669 "</div>\n"
670 )
671
672 # ---------------------------------------------------------------------------
673 # repositories/tree.html.erb
674 # ---------------------------------------------------------------------------
675 files["app/views/repositories/tree.html.erb"] = (
676 '<% content_for :title, "#{@current_path} \u00b7 #{@repository.full_name}" %>\n'
677 "\n"
678 '<div class="border-b border-gray-200 bg-white">\n'
679 ' <div class="max-w-6xl mx-auto px-4 sm:px-6 pt-6 pb-0">\n'
680 "\n"
681 ' <div class="flex items-center gap-2 text-sm mb-3">\n'
682 " <%= link_to @owner.username, user_profile_path(@owner.username),\n"
683 ' class: "text-brand-500 hover:underline font-medium" %>\n'
684 ' <span class="text-gray-400">/</span>\n'
685 " <%= link_to @repository.name, repository_path(@owner.username, @repository.name),\n"
686 ' class: "text-brand-500 hover:underline font-semibold" %>\n'
687 " </div>\n"
688 "\n"
689 ' <nav class="flex items-center gap-1.5 text-sm mb-4 flex-wrap">\n'
690 " <%= link_to @repository.name, repository_path(@owner.username, @repository.name),\n"
691 ' class: "breadcrumb-item font-medium" %>\n'
692 ' <span class="text-gray-400">/</span>\n'
693 " <% @path_parts.each_with_index do |part, i| %>\n"
694 ' <% partial_path = @path_parts[0..i].join("/") %>\n'
695 " <% if i < @path_parts.length - 1 %>\n"
696 " <%= link_to part,\n"
697 " repository_tree_path(@owner.username, @repository.name, @branch, partial_path),\n"
698 ' class: "breadcrumb-item" %>\n'
699 ' <span class="text-gray-400">/</span>\n'
700 " <% else %>\n"
701 ' <span class="font-medium text-gray-900"><%= part %></span>\n'
702 " <% end %>\n"
703 " <% end %>\n"
704 " </nav>\n"
705 "\n"
706 ' <div class="flex items-center gap-0 -mb-px">\n'
707 " <%= link_to repository_path(@owner.username, @repository.name),\n"
708 ' class: "tab-item flex items-center gap-1.5" do %>\n'
709 ' <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">\n'
710 f' <path d="{REPO_PATH_16}"/>\n'
711 " </svg>\n"
712 " Code\n"
713 " <% end %>\n"
714 " </div>\n"
715 " </div>\n"
716 "</div>\n"
717 "\n"
718 '<div class="max-w-6xl mx-auto px-4 sm:px-6 py-6">\n'
719 ' <div class="card">\n'
720 " <% @tree.each do |entry| %>\n"
721 ' <div class="file-row">\n'
722 ' <% if entry[:type] == "tree" %>\n'
723 ' <svg class="w-4 h-4 text-brand-300 shrink-0" viewBox="0 0 16 16" fill="currentColor">\n'
724 f' <path d="{FOLDER_PATH_16}"/>\n'
725 " </svg>\n"
726 " <%= link_to entry[:name],\n"
727 " repository_tree_path(@owner.username, @repository.name, @branch, entry[:path]),\n"
728 ' class: "text-brand-500 hover:underline font-medium flex-1" %>\n'
729 " <% else %>\n"
730 ' <svg class="w-4 h-4 text-gray-400 shrink-0" viewBox="0 0 16 16" fill="currentColor">\n'
731 f' <path d="{FILE_PATH_16}"/>\n'
732 " </svg>\n"
733 " <%= link_to entry[:name],\n"
734 " repository_blob_path(@owner.username, @repository.name, @branch, entry[:path]),\n"
735 ' class: "text-gray-800 hover:text-brand-500 hover:underline flex-1" %>\n'
736 " <% end %>\n"
737 " </div>\n"
738 " <% end %>\n"
739 " </div>\n"
740 "</div>\n"
741 )
742
743 # ---------------------------------------------------------------------------
744 # config/tailwind.config.js
745 # ---------------------------------------------------------------------------
746 files["config/tailwind.config.js"] = (
747 "const defaultTheme = require('tailwindcss/defaultTheme')\n"
748 "\n"
749 "module.exports = {\n"
750 " content: [\n"
751 " './public/*.html',\n"
752 " './app/helpers/**/*.rb',\n"
753 " './app/javascript/**/*.js',\n"
754 " './app/views/**/*.{erb,haml,html,slim}'\n"
755 " ],\n"
756 " theme: {\n"
757 " extend: {\n"
758 " fontFamily: {\n"
759 " sans: [\n"
760 " 'Inter', 'ui-sans-serif', 'system-ui', '-apple-system',\n"
761 " 'BlinkMacSystemFont', 'Segoe UI', 'Roboto',\n"
762 " ...defaultTheme.fontFamily.sans\n"
763 " ],\n"
764 " mono: [\n"
765 " 'JetBrains Mono', 'ui-monospace', 'SFMono-Regular',\n"
766 " 'Menlo', 'Monaco', 'Consolas',\n"
767 " ...defaultTheme.fontFamily.mono\n"
768 " ],\n"
769 " },\n"
770 " colors: {\n"
771 " brand: {\n"
772 " 50: '#EEF4FF',\n"
773 " 100: '#D6E6FF',\n"
774 " 200: '#B3CEFF',\n"
775 " 300: '#80ADFF',\n"
776 " 400: '#4D8CFF',\n"
777 " 500: '#0057B8',\n"
778 " 600: '#004A9E',\n"
779 " 700: '#003D84',\n"
780 " 800: '#002F6A',\n"
781 " 900: '#002050',\n"
782 " },\n"
783 " },\n"
784 " maxWidth: {\n"
785 " '8xl': '88rem',\n"
786 " },\n"
787 " },\n"
788 " },\n"
789 " plugins: [],\n"
790 "}\n"
791 )
792
793 # ---------------------------------------------------------------------------
794 # app/assets/stylesheets/application.tailwind.css
795 # ---------------------------------------------------------------------------
796 files["app/assets/stylesheets/application.tailwind.css"] = (
797 "@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');\n"
798 "@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');\n"
799 "\n"
800 "@tailwind base;\n"
801 "@tailwind components;\n"
802 "@tailwind utilities;\n"
803 "\n"
804 "@layer base {\n"
805 " html { @apply antialiased; }\n"
806 " body { @apply bg-white text-gray-900 font-sans text-sm leading-relaxed; }\n"
807 " a { @apply text-brand-500; }\n"
808 " code { @apply font-mono text-xs; }\n"
809 "}\n"
810 "\n"
811 "@layer components {\n"
812 " /* ---- Navigation ---- */\n"
813 " .nav-link {\n"
814 " @apply text-gray-600 hover:text-gray-900 text-sm font-medium transition-colors duration-150;\n"
815 " }\n"
816 "\n"
817 " /* ---- Buttons ---- */\n"
818 " .btn-primary {\n"
819 " @apply inline-flex items-center gap-2 px-4 py-2 bg-brand-500 text-white text-sm\n"
820 " font-medium rounded hover:bg-brand-600 transition-colors duration-150 cursor-pointer;\n"
821 " }\n"
822 " .btn-secondary {\n"
823 " @apply inline-flex items-center gap-2 px-4 py-2 bg-white text-gray-700 text-sm\n"
824 " font-medium rounded border border-gray-300 hover:bg-gray-50\n"
825 " transition-colors duration-150 cursor-pointer;\n"
826 " }\n"
827 " .btn-danger {\n"
828 " @apply inline-flex items-center gap-2 px-4 py-2 bg-red-600 text-white text-sm\n"
829 " font-medium rounded hover:bg-red-700 transition-colors duration-150 cursor-pointer;\n"
830 " }\n"
831 " .btn-ghost {\n"
832 " @apply inline-flex items-center gap-2 px-3 py-1.5 text-gray-600 text-sm\n"
833 " font-medium rounded hover:bg-gray-100 transition-colors duration-150 cursor-pointer;\n"
834 " }\n"
835 "\n"
836 " /* ---- Forms ---- */\n"
837 " .form-input {\n"
838 " @apply w-full px-3 py-2 border border-gray-300 rounded text-sm text-gray-900\n"
839 " placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-brand-500\n"
840 " focus:border-transparent transition-all duration-150;\n"
841 " }\n"
842 " .form-label {\n"
843 " @apply block text-sm font-medium text-gray-700 mb-1;\n"
844 " }\n"
845 "\n"
846 " /* ---- Cards ---- */\n"
847 " .card {\n"
848 " @apply bg-white border border-gray-200 rounded-sm;\n"
849 " }\n"
850 "\n"
851 " /* ---- Code ---- */\n"
852 " .code-container {\n"
853 " @apply bg-gray-50 border border-gray-200 rounded-sm font-mono text-xs overflow-x-auto;\n"
854 " }\n"
855 "\n"
856 " /* ---- File tree rows ---- */\n"
857 " .file-row {\n"
858 " @apply flex items-center gap-3 px-4 py-2.5 border-b border-gray-100\n"
859 " hover:bg-gray-50 transition-colors duration-100 text-sm;\n"
860 " }\n"
861 " .file-row:last-child { @apply border-b-0; }\n"
862 "\n"
863 " /* ---- Breadcrumb ---- */\n"
864 " .breadcrumb-item {\n"
865 " @apply text-brand-500 hover:underline text-sm;\n"
866 " }\n"
867 "\n"
868 " /* ---- Commit rows ---- */\n"
869 " .commit-row {\n"
870 " @apply flex items-start justify-between px-4 py-3 border-b border-gray-100\n"
871 " hover:bg-gray-50 transition-colors duration-100;\n"
872 " }\n"
873 " .commit-row:last-child { @apply border-b-0; }\n"
874 "\n"
875 " /* ---- Badges ---- */\n"
876 " .badge {\n"
877 " @apply inline-flex items-center px-2 py-0.5 rounded text-xs font-medium;\n"
878 " }\n"
879 " .badge-gray { @apply badge bg-gray-100 text-gray-600; }\n"
880 " .badge-blue { @apply badge bg-brand-50 text-brand-600; }\n"
881 "\n"
882 " /* ---- Tabs ---- */\n"
883 " .tab-item {\n"
884 " @apply px-4 py-2.5 text-sm font-medium text-gray-500 border-b-2 border-transparent\n"
885 " hover:text-gray-700 hover:border-gray-300 transition-all duration-150;\n"
886 " }\n"
887 " .tab-item.active {\n"
888 " @apply text-gray-900 border-brand-500;\n"
889 " }\n"
890 "\n"
891 " /* ---- README prose ---- */\n"
892 " .prose-readme h1 { @apply text-2xl font-semibold mb-4 mt-6 pb-2 border-b border-gray-200; }\n"
893 " .prose-readme h2 { @apply text-xl font-semibold mb-3 mt-5 pb-2 border-b border-gray-100; }\n"
894 " .prose-readme h3 { @apply text-lg font-medium mb-2 mt-4; }\n"
895 " .prose-readme p { @apply mb-4 leading-7; }\n"
896 " .prose-readme ul { @apply list-disc pl-6 mb-4 space-y-1; }\n"
897 " .prose-readme ol { @apply list-decimal pl-6 mb-4 space-y-1; }\n"
898 " .prose-readme li { @apply leading-7; }\n"
899 " .prose-readme a { @apply text-brand-500 hover:underline; }\n"
900 " .prose-readme code { @apply bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono text-gray-800; }\n"
901 " .prose-readme pre { @apply bg-gray-50 border border-gray-200 rounded-sm p-4 overflow-x-auto mb-4; }\n"
902 " .prose-readme pre code { @apply bg-transparent p-0; }\n"
903 " .prose-readme table { @apply w-full mb-4 border-collapse border border-gray-200; }\n"
904 " .prose-readme th { @apply bg-gray-50 border border-gray-200 px-3 py-2 text-left font-medium text-sm; }\n"
905 " .prose-readme td { @apply border border-gray-200 px-3 py-2 text-sm; }\n"
906 " .prose-readme blockquote { @apply border-l-4 border-gray-300 pl-4 italic text-gray-600 my-4; }\n"
907 " .prose-readme hr { @apply border-gray-200 my-6; }\n"
908 " .prose-readme img { @apply max-w-full; }\n"
909 "\n"
910 " /* ---- Diff ---- */\n"
911 " .diff-add { @apply bg-green-50 text-green-800; }\n"
912 " .diff-remove { @apply bg-red-50 text-red-800; }\n"
913 " .diff-header { @apply bg-blue-50 text-blue-700; }\n"
914 "}\n"
915 "\n"
916 "/* ---- Rouge syntax highlighting ---- */\n"
917 ".highlight .c { color: #998; font-style: italic }\n"
918 ".highlight .err { color: #a61717; background-color: #e3d2d2 }\n"
919 ".highlight .k { color: #000; font-weight: bold }\n"
920 ".highlight .o { font-weight: bold }\n"
921 ".highlight .cm { color: #998; font-style: italic }\n"
922 ".highlight .cp { color: #999; font-weight: bold }\n"
923 ".highlight .c1 { color: #998; font-style: italic }\n"
924 ".highlight .cs { color: #999; font-weight: bold; font-style: italic }\n"
925 ".highlight .gd { color: #000; background-color: #fdd }\n"
926 ".highlight .ge { font-style: italic }\n"
927 ".highlight .gr { color: #a00 }\n"
928 ".highlight .gh { color: #999 }\n"
929 ".highlight .gi { color: #000; background-color: #dfd }\n"
930 ".highlight .go { color: #888 }\n"
931 ".highlight .gp { color: #555 }\n"
932 ".highlight .gs { font-weight: bold }\n"
933 ".highlight .gu { color: #aaa }\n"
934 ".highlight .gt { color: #a00 }\n"
935 ".highlight .kc { color: #000; font-weight: bold }\n"
936 ".highlight .kd { color: #000; font-weight: bold }\n"
937 ".highlight .kp { color: #000; font-weight: bold }\n"
938 ".highlight .kr { color: #000; font-weight: bold }\n"
939 ".highlight .kt { color: #458; font-weight: bold }\n"
940 ".highlight .m { color: #099 }\n"
941 ".highlight .s { color: #d14 }\n"
942 ".highlight .na { color: #008080 }\n"
943 ".highlight .nb { color: #0086B3 }\n"
944 ".highlight .nc { color: #458; font-weight: bold }\n"
945 ".highlight .no { color: #008080 }\n"
946 ".highlight .ni { color: #800080 }\n"
947 ".highlight .ne { color: #900; font-weight: bold }\n"
948 ".highlight .nf { color: #900; font-weight: bold }\n"
949 ".highlight .nn { color: #555 }\n"
950 ".highlight .nt { color: #000080 }\n"
951 ".highlight .nv { color: #008080 }\n"
952 ".highlight .ow { font-weight: bold }\n"
953 ".highlight .w { color: #bbb }\n"
954 ".highlight .mf { color: #099 }\n"
955 ".highlight .mh { color: #099 }\n"
956 ".highlight .mi { color: #099 }\n"
957 ".highlight .mo { color: #099 }\n"
958 ".highlight .sb { color: #d14 }\n"
959 ".highlight .sc { color: #d14 }\n"
960 ".highlight .sd { color: #d14 }\n"
961 ".highlight .s2 { color: #d14 }\n"
962 ".highlight .se { color: #d14 }\n"
963 ".highlight .sh { color: #d14 }\n"
964 ".highlight .si { color: #d14 }\n"
965 ".highlight .sx { color: #d14 }\n"
966 ".highlight .sr { color: #009926 }\n"
967 ".highlight .s1 { color: #d14 }\n"
968 ".highlight .ss { color: #990073 }\n"
969 ".highlight .bp { color: #999 }\n"
970 ".highlight .vc { color: #008080 }\n"
971 ".highlight .vg { color: #008080 }\n"
972 ".highlight .vi { color: #008080 }\n"
973 ".highlight .il { color: #099 }\n"
974 )
975
976 # ---------------------------------------------------------------------------
977 # config/importmap.rb
978 # ---------------------------------------------------------------------------
979 files["config/importmap.rb"] = (
980 "# Pin npm packages by running ./bin/importmap\n"
981 "\n"
982 'pin "application", preload: true\n'
983 "\n"
984 'pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true\n'
985 'pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true\n'
986 'pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true\n'
987 "\n"
988 'pin_all_from "app/javascript/controllers", under: "controllers"\n'
989 )
990
991 # ---------------------------------------------------------------------------
992 # Write all files
993 # ---------------------------------------------------------------------------
994 for path, content in files.items():
995 os.makedirs(os.path.dirname(path), exist_ok=True)
996 with open(path, "w", encoding="utf-8") as fh:
997 fh.write(content)
998 lines = content.count("\n")
999 print(f" wrote {path} ({lines} lines)")
1000
1001 print("\nDone.")