| 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 — 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 | " — 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 %>" > README.md</p>\n' |
| 509 | ' <p class="text-gray-700">git add . && 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 | " · <%= commit[:subject] %>\n" |
| 531 | ' · <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.") |