feat(ui): add custom context menu and navigation shortcuts
d4f1426
Browse files
R
rin
·
2026-10-17 07:47 UTC
5 files changed
+1098
-74
assets/input.css
+179
-0
| ... | ... | @@ -1136,6 +1136,185 @@ kbd, .kbd { |
| 1136 | 1136 | color: hsl(var(--accent-foreground)); |
| 1137 | 1137 | } |
| 1138 | 1138 | |
| 1139 | +/* Custom context menu */ | |
| 1140 | +@keyframes contextMenuIn { | |
| 1141 | + from { | |
| 1142 | + opacity: 0; | |
| 1143 | + transform: translateY(-3px) scale(0.98); | |
| 1144 | + } | |
| 1145 | + to { | |
| 1146 | + opacity: 1; | |
| 1147 | + transform: translateY(0) scale(1); | |
| 1148 | + } | |
| 1149 | +} | |
| 1150 | +.context-menu { | |
| 1151 | + position: fixed; | |
| 1152 | + z-index: 120; | |
| 1153 | + width: max-content; | |
| 1154 | + min-width: 15rem; | |
| 1155 | + max-width: min(20rem, calc(100vw - 1rem)); | |
| 1156 | + max-height: min(72vh, 36rem); | |
| 1157 | + overflow-y: auto; | |
| 1158 | + overscroll-behavior: contain; | |
| 1159 | + padding: 0.28rem; | |
| 1160 | + background-color: hsl(var(--popover)); | |
| 1161 | + color: hsl(var(--popover-foreground)); | |
| 1162 | + border: 1px solid hsl(var(--border)); | |
| 1163 | + border-radius: var(--radius); | |
| 1164 | + box-shadow: 0 18px 44px -12px rgba(0, 0, 0, 0.42), 0 2px 8px -2px rgba(0, 0, 0, 0.18); | |
| 1165 | + opacity: 0; | |
| 1166 | + visibility: hidden; | |
| 1167 | + pointer-events: none; | |
| 1168 | + transform: translateY(-3px) scale(0.98); | |
| 1169 | + transform-origin: top left; | |
| 1170 | + transition: opacity 110ms ease, transform 130ms cubic-bezier(0.16, 1, 0.3, 1), visibility 110ms ease; | |
| 1171 | +} | |
| 1172 | +.context-menu.open { | |
| 1173 | + opacity: 1; | |
| 1174 | + visibility: visible; | |
| 1175 | + pointer-events: auto; | |
| 1176 | + transform: translateY(0) scale(1); | |
| 1177 | + animation: contextMenuIn 130ms cubic-bezier(0.16, 1, 0.3, 1); | |
| 1178 | +} | |
| 1179 | +body[data-repo-path] .context-menu { | |
| 1180 | + max-height: min(80vh, 52rem); | |
| 1181 | +} | |
| 1182 | +.context-menu-section[hidden], | |
| 1183 | +.context-menu-item[hidden] { | |
| 1184 | + display: none; | |
| 1185 | +} | |
| 1186 | +.context-menu-section + .context-menu-section { | |
| 1187 | + margin-top: 0.24rem; | |
| 1188 | + padding-top: 0.24rem; | |
| 1189 | + border-top: 1px solid hsl(var(--border)); | |
| 1190 | +} | |
| 1191 | +.context-menu-label { | |
| 1192 | + display: flex; | |
| 1193 | + align-items: center; | |
| 1194 | + gap: 0.5rem; | |
| 1195 | + padding: 0.22rem 0.5rem 0.14rem; | |
| 1196 | + font-size: 0.64rem; | |
| 1197 | + font-weight: 600; | |
| 1198 | + letter-spacing: 0.055em; | |
| 1199 | + text-transform: uppercase; | |
| 1200 | + color: hsl(var(--muted-foreground)); | |
| 1201 | +} | |
| 1202 | +.context-menu-meta { | |
| 1203 | + margin-left: auto; | |
| 1204 | + max-width: 9.5rem; | |
| 1205 | + overflow: hidden; | |
| 1206 | + text-overflow: ellipsis; | |
| 1207 | + white-space: nowrap; | |
| 1208 | + font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| 1209 | + font-size: 0.6rem; | |
| 1210 | + letter-spacing: 0; | |
| 1211 | + text-transform: none; | |
| 1212 | + opacity: 0.85; | |
| 1213 | +} | |
| 1214 | +.context-menu-item { | |
| 1215 | + display: flex; | |
| 1216 | + width: 100%; | |
| 1217 | + min-height: 1.95rem; | |
| 1218 | + align-items: center; | |
| 1219 | + gap: 0.55rem; | |
| 1220 | + padding: 0.33rem 0.5rem; | |
| 1221 | + border: 0; | |
| 1222 | + border-radius: calc(var(--radius) - 2px); | |
| 1223 | + background: transparent; | |
| 1224 | + color: hsl(var(--popover-foreground)); | |
| 1225 | + font: inherit; | |
| 1226 | + font-size: 0.8rem; | |
| 1227 | + line-height: 1.25; | |
| 1228 | + text-align: left; | |
| 1229 | + text-decoration: none; | |
| 1230 | + cursor: pointer; | |
| 1231 | + user-select: none; | |
| 1232 | + transition: background-color 90ms ease, color 90ms ease; | |
| 1233 | +} | |
| 1234 | +.context-menu-item .icon { | |
| 1235 | + flex: none; | |
| 1236 | + color: hsl(var(--muted-foreground)); | |
| 1237 | + transition: color 90ms ease; | |
| 1238 | +} | |
| 1239 | +.context-menu-item > span:not(.context-menu-check):not(.context-menu-kbd) { | |
| 1240 | + min-width: 0; | |
| 1241 | + overflow: hidden; | |
| 1242 | + text-overflow: ellipsis; | |
| 1243 | + white-space: nowrap; | |
| 1244 | +} | |
| 1245 | +.context-menu-item:hover, | |
| 1246 | +.context-menu-item:focus-visible, | |
| 1247 | +.context-menu-item.is-active { | |
| 1248 | + background-color: hsl(var(--accent)); | |
| 1249 | + color: hsl(var(--accent-foreground)); | |
| 1250 | + outline: none; | |
| 1251 | +} | |
| 1252 | +.context-menu-item:hover .icon, | |
| 1253 | +.context-menu-item:focus-visible .icon, | |
| 1254 | +.context-menu-item.is-active .icon { | |
| 1255 | + color: currentColor; | |
| 1256 | +} | |
| 1257 | +.context-menu-kbd { | |
| 1258 | + margin-left: auto; | |
| 1259 | + flex: none; | |
| 1260 | + min-width: auto; | |
| 1261 | + height: 1.05rem; | |
| 1262 | + padding: 0 0.24rem; | |
| 1263 | + font-size: 0.6rem; | |
| 1264 | + color: hsl(var(--muted-foreground)); | |
| 1265 | + background-color: hsl(var(--muted)); | |
| 1266 | + border: 1px solid hsl(var(--border)); | |
| 1267 | + border-radius: 0.22rem; | |
| 1268 | +} | |
| 1269 | +.context-menu-check { | |
| 1270 | + display: inline-flex; | |
| 1271 | + margin-left: auto; | |
| 1272 | + flex: none; | |
| 1273 | + opacity: 0; | |
| 1274 | + color: hsl(var(--foreground)); | |
| 1275 | +} | |
| 1276 | +.context-menu-item.is-current .context-menu-check { | |
| 1277 | + opacity: 1; | |
| 1278 | +} | |
| 1279 | + | |
| 1280 | +/* Shortcuts dialog */ | |
| 1281 | +.shortcuts-list { | |
| 1282 | + display: flex; | |
| 1283 | + max-height: min(70vh, 44rem); | |
| 1284 | + flex-direction: column; | |
| 1285 | + padding-top: 0.45rem; | |
| 1286 | + padding-bottom: 0.55rem; | |
| 1287 | + overflow-y: auto; | |
| 1288 | + overscroll-behavior: contain; | |
| 1289 | +} | |
| 1290 | +.shortcuts-group-label { | |
| 1291 | + padding: 0.65rem 0 0.2rem; | |
| 1292 | + font-size: 0.64rem; | |
| 1293 | + font-weight: 600; | |
| 1294 | + letter-spacing: 0.055em; | |
| 1295 | + text-transform: uppercase; | |
| 1296 | + color: hsl(var(--muted-foreground)); | |
| 1297 | +} | |
| 1298 | +.shortcuts-group-label:first-child { | |
| 1299 | + padding-top: 0.15rem; | |
| 1300 | +} | |
| 1301 | +.shortcut-row { | |
| 1302 | + display: flex; | |
| 1303 | + min-height: 2rem; | |
| 1304 | + align-items: center; | |
| 1305 | + justify-content: space-between; | |
| 1306 | + gap: 1rem; | |
| 1307 | + padding: 0.32rem 0; | |
| 1308 | + border-bottom: 1px solid hsl(var(--border)); | |
| 1309 | + font-size: 0.82rem; | |
| 1310 | +} | |
| 1311 | +.shortcut-keys { | |
| 1312 | + display: flex; | |
| 1313 | + flex: none; | |
| 1314 | + align-items: center; | |
| 1315 | + gap: 0.2rem; | |
| 1316 | +} | |
| 1317 | + | |
| 1139 | 1318 | /* Toast */ |
| 1140 | 1319 | @keyframes toastIn { |
| 1141 | 1320 | from { |
internal/i18n/i18n.go
+92
-22
| ... | ... | @@ -36,6 +36,7 @@ var translations = map[Lang]map[string]string{ |
| 36 | 36 | "nav.lang": "Язык", |
| 37 | 37 | "common.copy": "Копировать", |
| 38 | 38 | "common.copied": "Скопировано", |
| 39 | + "common.copy_failed": "Не удалось скопировать", | |
| 39 | 40 | "common.cancel": "Отмена", |
| 40 | 41 | "common.save": "Сохранить", |
| 41 | 42 | "common.delete": "Удалить", |
| ... | ... | @@ -327,17 +328,51 @@ var translations = map[Lang]map[string]string{ |
| 327 | 328 | "admin.role.user": "Участник", |
| 328 | 329 | |
| 329 | 330 | // Командная палитра и хоткеи |
| 330 | - "palette.placeholder": "Поиск репозитория, файла или команды...", | |
| 331 | - "palette.group.nav": "Навигация", | |
| 332 | - "palette.group.themes": "Тема оформления", | |
| 333 | - "palette.group.repos": "Репозитории", | |
| 334 | - "shortcuts.title": "Горячие клавиши", | |
| 335 | - "shortcuts.palette": "Командная палитра", | |
| 336 | - "shortcuts.search": "Фокус на поиск", | |
| 337 | - "shortcuts.home": "На главную", | |
| 338 | - "shortcuts.files": "Фильтр файлов", | |
| 339 | - "shortcuts.sidebar": "Скрыть / показать сайдбар", | |
| 340 | - "nav.toggle_sidebar": "Сайдбар (Ctrl+B)", | |
| 331 | + "palette.placeholder": "Поиск репозитория, файла или команды...", | |
| 332 | + "palette.group.nav": "Навигация", | |
| 333 | + "palette.group.themes": "Тема оформления", | |
| 334 | + "palette.group.repos": "Репозитории", | |
| 335 | + "shortcuts.title": "Горячие клавиши", | |
| 336 | + "shortcuts.palette": "Командная палитра", | |
| 337 | + "shortcuts.search": "Фокус на поиск", | |
| 338 | + "shortcuts.home": "На главную", | |
| 339 | + "shortcuts.files": "Фильтр файлов", | |
| 340 | + "shortcuts.sidebar": "Скрыть / показать сайдбар", | |
| 341 | + "shortcuts.shortcuts": "Это окно шорткатов", | |
| 342 | + "shortcuts.context_menu": "Открыть контекстное меню", | |
| 343 | + "shortcuts.theme": "Переключить тему", | |
| 344 | + "shortcuts.copy_url": "Копировать адрес страницы", | |
| 345 | + "shortcuts.close": "Закрыть меню или диалог", | |
| 346 | + "shortcuts.profile": "Мой профиль", | |
| 347 | + "shortcuts.new_repo": "Новый репозиторий", | |
| 348 | + "shortcuts.unavailable": "Этот шорткат здесь недоступен", | |
| 349 | + "shortcuts.group.general": "Общее", | |
| 350 | + "shortcuts.group.navigation": "Навигация", | |
| 351 | + "shortcuts.group.repo": "Репозиторий", | |
| 352 | + "nav.toggle_sidebar": "Сайдбар (Ctrl+B)", | |
| 353 | + | |
| 354 | + // Контекстное меню | |
| 355 | + "contextmenu.title": "Контекстное меню", | |
| 356 | + "contextmenu.selection": "Выделение", | |
| 357 | + "contextmenu.copy_selection": "Копировать выделенное", | |
| 358 | + "contextmenu.search_selection": "Искать выделенное", | |
| 359 | + "contextmenu.link": "Ссылка", | |
| 360 | + "contextmenu.open_link": "Открыть в новой вкладке", | |
| 361 | + "contextmenu.copy_link": "Копировать адрес ссылки", | |
| 362 | + "contextmenu.copy_link_text": "Копировать текст ссылки", | |
| 363 | + "contextmenu.image": "Изображение", | |
| 364 | + "contextmenu.open_image": "Открыть изображение", | |
| 365 | + "contextmenu.copy_image": "Копировать адрес изображения", | |
| 366 | + "contextmenu.save_image": "Сохранить изображение", | |
| 367 | + "contextmenu.repository": "Репозиторий", | |
| 368 | + "contextmenu.repo_actions": "Действия", | |
| 369 | + "contextmenu.page": "Страница", | |
| 370 | + "contextmenu.back": "Назад", | |
| 371 | + "contextmenu.forward": "Вперёд", | |
| 372 | + "contextmenu.reload": "Обновить", | |
| 373 | + "contextmenu.copy_url": "Копировать адрес страницы", | |
| 374 | + "contextmenu.appearance": "Оформление", | |
| 375 | + "contextmenu.app": "Приложение", | |
| 341 | 376 | |
| 342 | 377 | // Флеш-сообщения |
| 343 | 378 | "flash.welcome": "Добро пожаловать в Polar, @%s!", |
| ... | ... | @@ -618,6 +653,7 @@ var translations = map[Lang]map[string]string{ |
| 618 | 653 | "nav.lang": "Language", |
| 619 | 654 | "common.copy": "Copy", |
| 620 | 655 | "common.copied": "Copied", |
| 656 | + "common.copy_failed": "Could not copy", | |
| 621 | 657 | "common.cancel": "Cancel", |
| 622 | 658 | "common.save": "Save", |
| 623 | 659 | "common.delete": "Delete", |
| ... | ... | @@ -910,17 +946,51 @@ var translations = map[Lang]map[string]string{ |
| 910 | 946 | "admin.role.user": "User", |
| 911 | 947 | |
| 912 | 948 | // Command Palette & Shortcuts |
| 913 | - "palette.placeholder": "Search repositories, files, or commands...", | |
| 914 | - "palette.group.nav": "Navigation", | |
| 915 | - "palette.group.themes": "Theme", | |
| 916 | - "palette.group.repos": "Repositories", | |
| 917 | - "shortcuts.title": "Keyboard shortcuts", | |
| 918 | - "shortcuts.palette": "Command palette", | |
| 919 | - "shortcuts.search": "Focus search", | |
| 920 | - "shortcuts.home": "Go home", | |
| 921 | - "shortcuts.files": "Filter files", | |
| 922 | - "shortcuts.sidebar": "Toggle sidebar", | |
| 923 | - "nav.toggle_sidebar": "Sidebar (Ctrl+B)", | |
| 949 | + "palette.placeholder": "Search repositories, files, or commands...", | |
| 950 | + "palette.group.nav": "Navigation", | |
| 951 | + "palette.group.themes": "Theme", | |
| 952 | + "palette.group.repos": "Repositories", | |
| 953 | + "shortcuts.title": "Keyboard shortcuts", | |
| 954 | + "shortcuts.palette": "Command palette", | |
| 955 | + "shortcuts.search": "Focus search", | |
| 956 | + "shortcuts.home": "Go home", | |
| 957 | + "shortcuts.files": "Filter files", | |
| 958 | + "shortcuts.sidebar": "Toggle sidebar", | |
| 959 | + "shortcuts.shortcuts": "Show this dialog", | |
| 960 | + "shortcuts.context_menu": "Open context menu", | |
| 961 | + "shortcuts.theme": "Cycle theme", | |
| 962 | + "shortcuts.copy_url": "Copy page URL", | |
| 963 | + "shortcuts.close": "Close menu or dialog", | |
| 964 | + "shortcuts.profile": "Your profile", | |
| 965 | + "shortcuts.new_repo": "New repository", | |
| 966 | + "shortcuts.unavailable": "This shortcut is unavailable here", | |
| 967 | + "shortcuts.group.general": "General", | |
| 968 | + "shortcuts.group.navigation": "Navigation", | |
| 969 | + "shortcuts.group.repo": "Repository", | |
| 970 | + "nav.toggle_sidebar": "Sidebar (Ctrl+B)", | |
| 971 | + | |
| 972 | + // Context menu | |
| 973 | + "contextmenu.title": "Context menu", | |
| 974 | + "contextmenu.selection": "Selection", | |
| 975 | + "contextmenu.copy_selection": "Copy selection", | |
| 976 | + "contextmenu.search_selection": "Search selection", | |
| 977 | + "contextmenu.link": "Link", | |
| 978 | + "contextmenu.open_link": "Open in new tab", | |
| 979 | + "contextmenu.copy_link": "Copy link address", | |
| 980 | + "contextmenu.copy_link_text": "Copy link text", | |
| 981 | + "contextmenu.image": "Image", | |
| 982 | + "contextmenu.open_image": "Open image in new tab", | |
| 983 | + "contextmenu.copy_image": "Copy image address", | |
| 984 | + "contextmenu.save_image": "Save image", | |
| 985 | + "contextmenu.repository": "Repository", | |
| 986 | + "contextmenu.repo_actions": "Actions", | |
| 987 | + "contextmenu.page": "Page", | |
| 988 | + "contextmenu.back": "Back", | |
| 989 | + "contextmenu.forward": "Forward", | |
| 990 | + "contextmenu.reload": "Reload", | |
| 991 | + "contextmenu.copy_url": "Copy page URL", | |
| 992 | + "contextmenu.appearance": "Appearance", | |
| 993 | + "contextmenu.app": "Application", | |
| 924 | 994 | |
| 925 | 995 | // Flash messages |
| 926 | 996 | "flash.welcome": "Welcome to Polar, @%s!", |
internal/web/static/css/app.css
+179
-0
| ... | ... | @@ -1136,6 +1136,185 @@ kbd, .kbd { |
| 1136 | 1136 | color: hsl(var(--accent-foreground)); |
| 1137 | 1137 | } |
| 1138 | 1138 | |
| 1139 | +/* Custom context menu */ | |
| 1140 | +@keyframes contextMenuIn { | |
| 1141 | + from { | |
| 1142 | + opacity: 0; | |
| 1143 | + transform: translateY(-3px) scale(0.98); | |
| 1144 | + } | |
| 1145 | + to { | |
| 1146 | + opacity: 1; | |
| 1147 | + transform: translateY(0) scale(1); | |
| 1148 | + } | |
| 1149 | +} | |
| 1150 | +.context-menu { | |
| 1151 | + position: fixed; | |
| 1152 | + z-index: 120; | |
| 1153 | + width: max-content; | |
| 1154 | + min-width: 15rem; | |
| 1155 | + max-width: min(20rem, calc(100vw - 1rem)); | |
| 1156 | + max-height: min(72vh, 36rem); | |
| 1157 | + overflow-y: auto; | |
| 1158 | + overscroll-behavior: contain; | |
| 1159 | + padding: 0.28rem; | |
| 1160 | + background-color: hsl(var(--popover)); | |
| 1161 | + color: hsl(var(--popover-foreground)); | |
| 1162 | + border: 1px solid hsl(var(--border)); | |
| 1163 | + border-radius: var(--radius); | |
| 1164 | + box-shadow: 0 18px 44px -12px rgba(0, 0, 0, 0.42), 0 2px 8px -2px rgba(0, 0, 0, 0.18); | |
| 1165 | + opacity: 0; | |
| 1166 | + visibility: hidden; | |
| 1167 | + pointer-events: none; | |
| 1168 | + transform: translateY(-3px) scale(0.98); | |
| 1169 | + transform-origin: top left; | |
| 1170 | + transition: opacity 110ms ease, transform 130ms cubic-bezier(0.16, 1, 0.3, 1), visibility 110ms ease; | |
| 1171 | +} | |
| 1172 | +.context-menu.open { | |
| 1173 | + opacity: 1; | |
| 1174 | + visibility: visible; | |
| 1175 | + pointer-events: auto; | |
| 1176 | + transform: translateY(0) scale(1); | |
| 1177 | + animation: contextMenuIn 130ms cubic-bezier(0.16, 1, 0.3, 1); | |
| 1178 | +} | |
| 1179 | +body[data-repo-path] .context-menu { | |
| 1180 | + max-height: min(80vh, 52rem); | |
| 1181 | +} | |
| 1182 | +.context-menu-section[hidden], | |
| 1183 | +.context-menu-item[hidden] { | |
| 1184 | + display: none; | |
| 1185 | +} | |
| 1186 | +.context-menu-section + .context-menu-section { | |
| 1187 | + margin-top: 0.24rem; | |
| 1188 | + padding-top: 0.24rem; | |
| 1189 | + border-top: 1px solid hsl(var(--border)); | |
| 1190 | +} | |
| 1191 | +.context-menu-label { | |
| 1192 | + display: flex; | |
| 1193 | + align-items: center; | |
| 1194 | + gap: 0.5rem; | |
| 1195 | + padding: 0.22rem 0.5rem 0.14rem; | |
| 1196 | + font-size: 0.64rem; | |
| 1197 | + font-weight: 600; | |
| 1198 | + letter-spacing: 0.055em; | |
| 1199 | + text-transform: uppercase; | |
| 1200 | + color: hsl(var(--muted-foreground)); | |
| 1201 | +} | |
| 1202 | +.context-menu-meta { | |
| 1203 | + margin-left: auto; | |
| 1204 | + max-width: 9.5rem; | |
| 1205 | + overflow: hidden; | |
| 1206 | + text-overflow: ellipsis; | |
| 1207 | + white-space: nowrap; | |
| 1208 | + font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| 1209 | + font-size: 0.6rem; | |
| 1210 | + letter-spacing: 0; | |
| 1211 | + text-transform: none; | |
| 1212 | + opacity: 0.85; | |
| 1213 | +} | |
| 1214 | +.context-menu-item { | |
| 1215 | + display: flex; | |
| 1216 | + width: 100%; | |
| 1217 | + min-height: 1.95rem; | |
| 1218 | + align-items: center; | |
| 1219 | + gap: 0.55rem; | |
| 1220 | + padding: 0.33rem 0.5rem; | |
| 1221 | + border: 0; | |
| 1222 | + border-radius: calc(var(--radius) - 2px); | |
| 1223 | + background: transparent; | |
| 1224 | + color: hsl(var(--popover-foreground)); | |
| 1225 | + font: inherit; | |
| 1226 | + font-size: 0.8rem; | |
| 1227 | + line-height: 1.25; | |
| 1228 | + text-align: left; | |
| 1229 | + text-decoration: none; | |
| 1230 | + cursor: pointer; | |
| 1231 | + user-select: none; | |
| 1232 | + transition: background-color 90ms ease, color 90ms ease; | |
| 1233 | +} | |
| 1234 | +.context-menu-item .icon { | |
| 1235 | + flex: none; | |
| 1236 | + color: hsl(var(--muted-foreground)); | |
| 1237 | + transition: color 90ms ease; | |
| 1238 | +} | |
| 1239 | +.context-menu-item > span:not(.context-menu-check):not(.context-menu-kbd) { | |
| 1240 | + min-width: 0; | |
| 1241 | + overflow: hidden; | |
| 1242 | + text-overflow: ellipsis; | |
| 1243 | + white-space: nowrap; | |
| 1244 | +} | |
| 1245 | +.context-menu-item:hover, | |
| 1246 | +.context-menu-item:focus-visible, | |
| 1247 | +.context-menu-item.is-active { | |
| 1248 | + background-color: hsl(var(--accent)); | |
| 1249 | + color: hsl(var(--accent-foreground)); | |
| 1250 | + outline: none; | |
| 1251 | +} | |
| 1252 | +.context-menu-item:hover .icon, | |
| 1253 | +.context-menu-item:focus-visible .icon, | |
| 1254 | +.context-menu-item.is-active .icon { | |
| 1255 | + color: currentColor; | |
| 1256 | +} | |
| 1257 | +.context-menu-kbd { | |
| 1258 | + margin-left: auto; | |
| 1259 | + flex: none; | |
| 1260 | + min-width: auto; | |
| 1261 | + height: 1.05rem; | |
| 1262 | + padding: 0 0.24rem; | |
| 1263 | + font-size: 0.6rem; | |
| 1264 | + color: hsl(var(--muted-foreground)); | |
| 1265 | + background-color: hsl(var(--muted)); | |
| 1266 | + border: 1px solid hsl(var(--border)); | |
| 1267 | + border-radius: 0.22rem; | |
| 1268 | +} | |
| 1269 | +.context-menu-check { | |
| 1270 | + display: inline-flex; | |
| 1271 | + margin-left: auto; | |
| 1272 | + flex: none; | |
| 1273 | + opacity: 0; | |
| 1274 | + color: hsl(var(--foreground)); | |
| 1275 | +} | |
| 1276 | +.context-menu-item.is-current .context-menu-check { | |
| 1277 | + opacity: 1; | |
| 1278 | +} | |
| 1279 | + | |
| 1280 | +/* Shortcuts dialog */ | |
| 1281 | +.shortcuts-list { | |
| 1282 | + display: flex; | |
| 1283 | + max-height: min(70vh, 44rem); | |
| 1284 | + flex-direction: column; | |
| 1285 | + padding-top: 0.45rem; | |
| 1286 | + padding-bottom: 0.55rem; | |
| 1287 | + overflow-y: auto; | |
| 1288 | + overscroll-behavior: contain; | |
| 1289 | +} | |
| 1290 | +.shortcuts-group-label { | |
| 1291 | + padding: 0.65rem 0 0.2rem; | |
| 1292 | + font-size: 0.64rem; | |
| 1293 | + font-weight: 600; | |
| 1294 | + letter-spacing: 0.055em; | |
| 1295 | + text-transform: uppercase; | |
| 1296 | + color: hsl(var(--muted-foreground)); | |
| 1297 | +} | |
| 1298 | +.shortcuts-group-label:first-child { | |
| 1299 | + padding-top: 0.15rem; | |
| 1300 | +} | |
| 1301 | +.shortcut-row { | |
| 1302 | + display: flex; | |
| 1303 | + min-height: 2rem; | |
| 1304 | + align-items: center; | |
| 1305 | + justify-content: space-between; | |
| 1306 | + gap: 1rem; | |
| 1307 | + padding: 0.32rem 0; | |
| 1308 | + border-bottom: 1px solid hsl(var(--border)); | |
| 1309 | + font-size: 0.82rem; | |
| 1310 | +} | |
| 1311 | +.shortcut-keys { | |
| 1312 | + display: flex; | |
| 1313 | + flex: none; | |
| 1314 | + align-items: center; | |
| 1315 | + gap: 0.2rem; | |
| 1316 | +} | |
| 1317 | + | |
| 1139 | 1318 | /* Toast */ |
| 1140 | 1319 | @keyframes toastIn { |
| 1141 | 1320 | from { |
internal/web/static/js/app.js
+463
-26
| ... | ... | @@ -15,6 +15,46 @@ |
| 15 | 15 | } |
| 16 | 16 | window.PolarToast = showToast; |
| 17 | 17 | |
| 18 | + function copyText(text, successMessage) { | |
| 19 | + var value = text == null ? '' : String(text); | |
| 20 | + if (!value) return; | |
| 21 | + var copiedMsg = successMessage || document.documentElement.getAttribute('data-copied-text') || 'Copied'; | |
| 22 | + var failedMsg = document.documentElement.getAttribute('data-copy-failed-text') || 'Could not copy'; | |
| 23 | + | |
| 24 | + function fallbackCopy() { | |
| 25 | + var previousFocus = document.activeElement; | |
| 26 | + var ta = document.createElement('textarea'); | |
| 27 | + ta.value = value; | |
| 28 | + ta.setAttribute('readonly', ''); | |
| 29 | + ta.style.position = 'fixed'; | |
| 30 | + ta.style.left = '-9999px'; | |
| 31 | + ta.style.top = '0'; | |
| 32 | + document.body.appendChild(ta); | |
| 33 | + ta.focus(); | |
| 34 | + ta.select(); | |
| 35 | + ta.setSelectionRange(0, value.length); | |
| 36 | + var ok = false; | |
| 37 | + try { | |
| 38 | + ok = document.execCommand('copy'); | |
| 39 | + } catch (err) { | |
| 40 | + ok = false; | |
| 41 | + } | |
| 42 | + if (ta.parentNode) ta.parentNode.removeChild(ta); | |
| 43 | + if (previousFocus && previousFocus.focus) { | |
| 44 | + try { previousFocus.focus({ preventScroll: true }); } catch (err2) {} | |
| 45 | + } | |
| 46 | + showToast(ok ? copiedMsg : failedMsg); | |
| 47 | + } | |
| 48 | + | |
| 49 | + if (navigator.clipboard && navigator.clipboard.writeText) { | |
| 50 | + navigator.clipboard.writeText(value).then(function () { | |
| 51 | + showToast(copiedMsg); | |
| 52 | + }, fallbackCopy); | |
| 53 | + } else { | |
| 54 | + fallbackCopy(); | |
| 55 | + } | |
| 56 | + } | |
| 57 | + | |
| 18 | 58 | function setTheme(theme, silent) { |
| 19 | 59 | var allowed = ['light', 'dark', 'system']; |
| 20 | 60 | if (allowed.indexOf(theme) === -1) theme = 'system'; |
| ... | ... | @@ -29,6 +69,7 @@ |
| 29 | 69 | el.classList.toggle('active', isCurrent); |
| 30 | 70 | el.setAttribute('aria-checked', isCurrent ? 'true' : 'false'); |
| 31 | 71 | }); |
| 72 | + updateContextMenuThemeState(); | |
| 32 | 73 | } |
| 33 | 74 | window.PolarSetTheme = setTheme; |
| 34 | 75 | |
| ... | ... | @@ -67,10 +108,354 @@ |
| 67 | 108 | }); |
| 68 | 109 | } |
| 69 | 110 | |
| 111 | + var contextMenuIndex = -1; | |
| 112 | + var contextMenuState = null; | |
| 113 | + | |
| 114 | + function contextElement(target) { | |
| 115 | + if (!target) return null; | |
| 116 | + if (target.nodeType === 1) return target; | |
| 117 | + return target.parentElement || null; | |
| 118 | + } | |
| 119 | + | |
| 120 | + function isEditableTarget(target) { | |
| 121 | + if (!target || !target.tagName) return false; | |
| 122 | + var tag = target.tagName.toUpperCase(); | |
| 123 | + return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable; | |
| 124 | + } | |
| 125 | + | |
| 126 | + function getContextMenuItems() { | |
| 127 | + var menu = document.getElementById('context-menu'); | |
| 128 | + if (!menu) return []; | |
| 129 | + return Array.prototype.filter.call(menu.querySelectorAll('.context-menu-item'), function (item) { | |
| 130 | + var section = item.closest('[data-context-section]'); | |
| 131 | + return !section || !section.hidden; | |
| 132 | + }); | |
| 133 | + } | |
| 134 | + | |
| 135 | + function focusContextMenuItem(index) { | |
| 136 | + var items = getContextMenuItems(); | |
| 137 | + if (!items.length) return; | |
| 138 | + contextMenuIndex = (index + items.length) % items.length; | |
| 139 | + items.forEach(function (item, idx) { | |
| 140 | + item.classList.toggle('is-active', idx === contextMenuIndex); | |
| 141 | + }); | |
| 142 | + try { | |
| 143 | + items[contextMenuIndex].focus({ preventScroll: true }); | |
| 144 | + } catch (err) { | |
| 145 | + items[contextMenuIndex].focus(); | |
| 146 | + } | |
| 147 | + } | |
| 148 | + | |
| 149 | + function updateContextMenuThemeState() { | |
| 150 | + var menu = document.getElementById('context-menu'); | |
| 151 | + if (!menu) return; | |
| 152 | + var theme = document.documentElement.getAttribute('data-theme') || 'system'; | |
| 153 | + menu.querySelectorAll('[data-theme-value]').forEach(function (item) { | |
| 154 | + var isCurrent = item.getAttribute('data-theme-value') === theme; | |
| 155 | + item.classList.toggle('is-current', isCurrent); | |
| 156 | + item.setAttribute('aria-checked', isCurrent ? 'true' : 'false'); | |
| 157 | + }); | |
| 158 | + } | |
| 159 | + | |
| 160 | + function closeContextMenu() { | |
| 161 | + var menu = document.getElementById('context-menu'); | |
| 162 | + if (!menu || !menu.classList.contains('open')) return; | |
| 163 | + var restoreFocus = contextMenuState && contextMenuState.restoreFocus; | |
| 164 | + menu.classList.remove('open'); | |
| 165 | + menu.setAttribute('aria-hidden', 'true'); | |
| 166 | + menu.style.left = ''; | |
| 167 | + menu.style.top = ''; | |
| 168 | + contextMenuState = null; | |
| 169 | + contextMenuIndex = -1; | |
| 170 | + menu.querySelectorAll('.context-menu-item.is-active').forEach(function (item) { | |
| 171 | + item.classList.remove('is-active'); | |
| 172 | + }); | |
| 173 | + if (restoreFocus && restoreFocus !== document.body && restoreFocus.isConnected) { | |
| 174 | + try { restoreFocus.focus({ preventScroll: true }); } catch (err) {} | |
| 175 | + } | |
| 176 | + } | |
| 177 | + | |
| 178 | + function openContextMenuAt(x, y, target, restoreFocus) { | |
| 179 | + var menu = document.getElementById('context-menu'); | |
| 180 | + var el = contextElement(target); | |
| 181 | + if (!menu || !el) return; | |
| 182 | + | |
| 183 | + closeAllDropdowns(); | |
| 184 | + | |
| 185 | + var link = el.closest ? el.closest('a[href]') : null; | |
| 186 | + if (link && (!link.getAttribute('href') || /^javascript:/i.test(link.getAttribute('href')))) { | |
| 187 | + link = null; | |
| 188 | + } | |
| 189 | + | |
| 190 | + var image = el.closest ? el.closest('img') : null; | |
| 191 | + var imageURL = image ? (image.currentSrc || image.src || '') : ''; | |
| 192 | + var imageIsData = /^data:/i.test(imageURL); | |
| 193 | + var imageIsTemporary = /^(data|blob):/i.test(imageURL); | |
| 194 | + | |
| 195 | + var selectedText = ''; | |
| 196 | + try { | |
| 197 | + var selection = window.getSelection(); | |
| 198 | + if (selection && !selection.isCollapsed) selectedText = selection.toString().trim(); | |
| 199 | + } catch (err) {} | |
| 200 | + | |
| 201 | + var searchText = selectedText.replace(/\s+/g, ' ').trim().slice(0, 160); | |
| 202 | + var linkText = ''; | |
| 203 | + if (link) { | |
| 204 | + linkText = (link.innerText || link.textContent || link.getAttribute('aria-label') || link.getAttribute('title') || '') | |
| 205 | + .replace(/\s+/g, ' ').trim(); | |
| 206 | + } | |
| 207 | + | |
| 208 | + contextMenuState = { | |
| 209 | + linkHref: link ? link.href : '', | |
| 210 | + linkText: linkText, | |
| 211 | + imageURL: imageURL, | |
| 212 | + imageIsData: imageIsData, | |
| 213 | + selectedText: selectedText, | |
| 214 | + searchText: searchText, | |
| 215 | + restoreFocus: restoreFocus || document.activeElement | |
| 216 | + }; | |
| 217 | + | |
| 218 | + ['selection', 'link', 'image'].forEach(function (name) { | |
| 219 | + var section = menu.querySelector('[data-context-section="' + name + '"]'); | |
| 220 | + if (section) { | |
| 221 | + section.hidden = name === 'selection' ? !selectedText : name === 'link' ? !link : !imageURL; | |
| 222 | + } | |
| 223 | + }); | |
| 224 | + | |
| 225 | + var linkTextItem = menu.querySelector('[data-context-action="link-text"]'); | |
| 226 | + if (linkTextItem) linkTextItem.hidden = !linkText; | |
| 227 | + var imageOpenItem = menu.querySelector('[data-context-action="image-open"]'); | |
| 228 | + if (imageOpenItem) imageOpenItem.hidden = !imageURL || imageIsData; | |
| 229 | + var imageCopyItem = menu.querySelector('[data-context-action="image-copy"]'); | |
| 230 | + if (imageCopyItem) imageCopyItem.hidden = !imageURL || imageIsTemporary; | |
| 231 | + | |
| 232 | + updateContextMenuThemeState(); | |
| 233 | + menu.classList.add('open'); | |
| 234 | + menu.setAttribute('aria-hidden', 'false'); | |
| 235 | + menu.scrollTop = 0; | |
| 236 | + menu.style.left = '0px'; | |
| 237 | + menu.style.top = '0px'; | |
| 238 | + | |
| 239 | + var menuWidth = menu.offsetWidth; | |
| 240 | + var menuHeight = menu.offsetHeight; | |
| 241 | + var gap = 8; | |
| 242 | + var vw = window.innerWidth || document.documentElement.clientWidth; | |
| 243 | + var vh = window.innerHeight || document.documentElement.clientHeight; | |
| 244 | + var left = x; | |
| 245 | + var top = y; | |
| 246 | + if (left + menuWidth > vw - gap) left = x - menuWidth; | |
| 247 | + if (left < gap) left = gap; | |
| 248 | + if (left + menuWidth > vw - gap) left = Math.max(gap, vw - menuWidth - gap); | |
| 249 | + if (top + menuHeight > vh - gap) top = y - menuHeight; | |
| 250 | + if (top < gap) top = gap; | |
| 251 | + if (top + menuHeight > vh - gap) top = Math.max(gap, vh - menuHeight - gap); | |
| 252 | + menu.style.left = Math.round(left) + 'px'; | |
| 253 | + menu.style.top = Math.round(top) + 'px'; | |
| 254 | + | |
| 255 | + contextMenuIndex = -1; | |
| 256 | + setTimeout(function () { | |
| 257 | + if (menu.classList.contains('open')) focusContextMenuItem(0); | |
| 258 | + }, 20); | |
| 259 | + } | |
| 260 | + | |
| 261 | + function openContextMenuForKeyboard() { | |
| 262 | + var target = document.activeElement; | |
| 263 | + if (!target || target === document.body) target = document.body; | |
| 264 | + var rect = target.getBoundingClientRect ? target.getBoundingClientRect() : null; | |
| 265 | + var x = rect ? rect.left + Math.min(rect.width, 20) : 16; | |
| 266 | + var y = rect ? rect.bottom + 4 : 16; | |
| 267 | + openContextMenuAt(x, y, target, target); | |
| 268 | + } | |
| 269 | + | |
| 270 | + function runContextMenuAction(item) { | |
| 271 | + var action = item.getAttribute('data-context-action'); | |
| 272 | + var state = contextMenuState || {}; | |
| 273 | + | |
| 274 | + if (action === 'theme') { | |
| 275 | + setTheme(item.getAttribute('data-theme-value'), false); | |
| 276 | + closeContextMenu(); | |
| 277 | + return; | |
| 278 | + } | |
| 279 | + | |
| 280 | + closeContextMenu(); | |
| 281 | + | |
| 282 | + if (action === 'copy-selection') { | |
| 283 | + copyText(state.selectedText); | |
| 284 | + } else if (action === 'search-selection') { | |
| 285 | + openPalette(state.searchText || state.selectedText || ''); | |
| 286 | + } else if (action === 'link-open') { | |
| 287 | + if (state.linkHref) window.open(state.linkHref, '_blank', 'noopener,noreferrer'); | |
| 288 | + } else if (action === 'link-copy') { | |
| 289 | + copyText(state.linkHref); | |
| 290 | + } else if (action === 'link-text') { | |
| 291 | + copyText(state.linkText); | |
| 292 | + } else if (action === 'image-open') { | |
| 293 | + if (state.imageURL) window.open(state.imageURL, '_blank', 'noopener,noreferrer'); | |
| 294 | + } else if (action === 'image-copy') { | |
| 295 | + copyText(state.imageURL); | |
| 296 | + } else if (action === 'image-save') { | |
| 297 | + if (state.imageURL) { | |
| 298 | + var extension = 'img'; | |
| 299 | + var dataMatch = state.imageURL.match(/^data:image\/([a-z0-9.+-]+)/i); | |
| 300 | + if (dataMatch) { | |
| 301 | + extension = dataMatch[1].toLowerCase().replace('jpeg', 'jpg').replace('svg+xml', 'svg'); | |
| 302 | + } else { | |
| 303 | + try { | |
| 304 | + var pathMatch = new URL(state.imageURL).pathname.match(/\.([a-z0-9]{2,5})$/i); | |
| 305 | + if (pathMatch) extension = pathMatch[1].toLowerCase(); | |
| 306 | + } catch (err) {} | |
| 307 | + } | |
| 308 | + var download = document.createElement('a'); | |
| 309 | + download.href = state.imageURL; | |
| 310 | + download.download = 'polar-image.' + extension; | |
| 311 | + document.body.appendChild(download); | |
| 312 | + download.click(); | |
| 313 | + document.body.removeChild(download); | |
| 314 | + } | |
| 315 | + } else if (action === 'page-copy') { | |
| 316 | + copyText(window.location.href); | |
| 317 | + } else if (action === 'page-reload') { | |
| 318 | + window.location.reload(); | |
| 319 | + } else if (action === 'page-back') { | |
| 320 | + window.history.back(); | |
| 321 | + } else if (action === 'page-forward') { | |
| 322 | + window.history.forward(); | |
| 323 | + } else if (action === 'palette') { | |
| 324 | + openPalette(''); | |
| 325 | + } else if (action === 'shortcuts') { | |
| 326 | + openDialog('shortcuts-dialog'); | |
| 327 | + } | |
| 328 | + } | |
| 329 | + | |
| 330 | + function cycleTheme() { | |
| 331 | + var current = document.documentElement.getAttribute('data-theme') || 'system'; | |
| 332 | + var order = ['light', 'dark', 'system']; | |
| 333 | + var next = order[(order.indexOf(current) + 1) % order.length]; | |
| 334 | + setTheme(next, false); | |
| 335 | + var item = document.querySelector('#context-menu [data-theme-value="' + next + '"]'); | |
| 336 | + var label = item ? item.querySelector('span:not(.context-menu-check)') : null; | |
| 337 | + showToast(label ? label.textContent.trim() : next); | |
| 338 | + } | |
| 339 | + | |
| 340 | + function shortcutUnavailable() { | |
| 341 | + showToast(document.documentElement.getAttribute('data-shortcut-unavailable') || 'Shortcut unavailable here'); | |
| 342 | + } | |
| 343 | + | |
| 344 | + function handleNavigationChord(key) { | |
| 345 | + var body = document.body; | |
| 346 | + var repoPath = body ? body.getAttribute('data-repo-path') : ''; | |
| 347 | + var currentUser = body ? body.getAttribute('data-current-user') : ''; | |
| 348 | + var repoFlags = { | |
| 349 | + issues: body && body.getAttribute('data-repo-issues') === 'true', | |
| 350 | + pulls: body && body.getAttribute('data-repo-pulls') === 'true', | |
| 351 | + releases: body && body.getAttribute('data-repo-releases') === 'true', | |
| 352 | + activity: body && body.getAttribute('data-repo-activity') === 'true', | |
| 353 | + canWrite: body && body.getAttribute('data-repo-can-write') === 'true' | |
| 354 | + }; | |
| 355 | + | |
| 356 | + if (key === 'h') { | |
| 357 | + window.location.href = '/'; | |
| 358 | + return true; | |
| 359 | + } | |
| 360 | + if (key === 'r') { | |
| 361 | + window.location.href = '/explore'; | |
| 362 | + return true; | |
| 363 | + } | |
| 364 | + if (key === 'o') { | |
| 365 | + if (!currentUser) { | |
| 366 | + shortcutUnavailable(); | |
| 367 | + return true; | |
| 368 | + } | |
| 369 | + window.location.href = '/' + encodeURIComponent(currentUser); | |
| 370 | + return true; | |
| 371 | + } | |
| 372 | + if (key === 'n') { | |
| 373 | + if (!currentUser) { | |
| 374 | + shortcutUnavailable(); | |
| 375 | + return true; | |
| 376 | + } | |
| 377 | + window.location.href = '/new'; | |
| 378 | + return true; | |
| 379 | + } | |
| 380 | + if (key === 's' && !repoPath) { | |
| 381 | + if (currentUser) { | |
| 382 | + window.location.href = '/settings'; | |
| 383 | + } else { | |
| 384 | + shortcutUnavailable(); | |
| 385 | + } | |
| 386 | + return true; | |
| 387 | + } | |
| 388 | + if (!repoPath) { | |
| 389 | + var isRepoChord = 'ciplab'.indexOf(key) !== -1; | |
| 390 | + if (isRepoChord) shortcutUnavailable(); | |
| 391 | + return isRepoChord; | |
| 392 | + } | |
| 393 | + if (key === 'c') { | |
| 394 | + window.location.href = repoPath; | |
| 395 | + return true; | |
| 396 | + } | |
| 397 | + if (key === 'i') { | |
| 398 | + if (!repoFlags.issues) shortcutUnavailable(); | |
| 399 | + else window.location.href = repoPath + '/issues'; | |
| 400 | + return true; | |
| 401 | + } | |
| 402 | + if (key === 'p') { | |
| 403 | + if (!repoFlags.pulls) shortcutUnavailable(); | |
| 404 | + else window.location.href = repoPath + '/pulls'; | |
| 405 | + return true; | |
| 406 | + } | |
| 407 | + if (key === 'l') { | |
| 408 | + if (!repoFlags.releases) shortcutUnavailable(); | |
| 409 | + else window.location.href = repoPath + '/releases'; | |
| 410 | + return true; | |
| 411 | + } | |
| 412 | + if (key === 'a') { | |
| 413 | + if (!repoFlags.activity) shortcutUnavailable(); | |
| 414 | + else window.location.href = repoPath + '/activity'; | |
| 415 | + return true; | |
| 416 | + } | |
| 417 | + if (key === 'b') { | |
| 418 | + window.location.href = repoPath + '/branches'; | |
| 419 | + return true; | |
| 420 | + } | |
| 421 | + if (key === 's') { | |
| 422 | + if (!repoFlags.canWrite) shortcutUnavailable(); | |
| 423 | + else window.location.href = repoPath + '/settings'; | |
| 424 | + return true; | |
| 425 | + } | |
| 426 | + return false; | |
| 427 | + } | |
| 428 | + | |
| 429 | + document.addEventListener('contextmenu', function (e) { | |
| 430 | + var el = contextElement(e.target); | |
| 431 | + if (!el || el.closest('#context-menu') || el.isContentEditable || el.closest('input, textarea, select, [data-native-context]')) { | |
| 432 | + return; | |
| 433 | + } | |
| 434 | + e.preventDefault(); | |
| 435 | + openContextMenuAt(e.clientX, e.clientY, el, document.activeElement); | |
| 436 | + }); | |
| 437 | + | |
| 438 | + document.addEventListener('pointerdown', function (e) { | |
| 439 | + var menu = document.getElementById('context-menu'); | |
| 440 | + if (!menu || !menu.classList.contains('open')) return; | |
| 441 | + if (!e.target.closest || !e.target.closest('#context-menu')) closeContextMenu(); | |
| 442 | + }, true); | |
| 443 | + | |
| 444 | + document.addEventListener('scroll', function (e) { | |
| 445 | + var target = contextElement(e.target); | |
| 446 | + if (target && target.closest && target.closest('#context-menu')) return; | |
| 447 | + closeContextMenu(); | |
| 448 | + }, true); | |
| 449 | + window.addEventListener('resize', closeContextMenu); | |
| 450 | + window.addEventListener('popstate', closeContextMenu); | |
| 451 | + window.addEventListener('hashchange', closeContextMenu); | |
| 452 | + document.addEventListener('htmx:beforeSwap', closeContextMenu); | |
| 453 | + | |
| 70 | 454 | function openDialog(id) { |
| 71 | 455 | var dlg = document.getElementById(id); |
| 72 | 456 | if (!dlg) return; |
| 73 | 457 | closeAllDropdowns(); |
| 458 | + closeContextMenu(); | |
| 74 | 459 | dlg.classList.add('open'); |
| 75 | 460 | dlg.setAttribute('aria-hidden', 'false'); |
| 76 | 461 | var firstInput = dlg.querySelector('input:not([type="hidden"]), button[data-autofocus]'); |
| ... | ... | @@ -133,6 +518,7 @@ |
| 133 | 518 | var palette = document.getElementById('command-palette'); |
| 134 | 519 | if (!palette) return; |
| 135 | 520 | closeAllDropdowns(); |
| 521 | + closeContextMenu(); | |
| 136 | 522 | palette.classList.add('open'); |
| 137 | 523 | palette.setAttribute('aria-hidden', 'false'); |
| 138 | 524 | var input = document.getElementById('command-palette-input'); |
| ... | ... | @@ -167,6 +553,21 @@ |
| 167 | 553 | } |
| 168 | 554 | |
| 169 | 555 | document.addEventListener('click', function (e) { |
| 556 | + // Context menu action | |
| 557 | + var contextAction = e.target.closest('[data-context-action]'); | |
| 558 | + if (contextAction) { | |
| 559 | + e.preventDefault(); | |
| 560 | + runContextMenuAction(contextAction); | |
| 561 | + return; | |
| 562 | + } | |
| 563 | + | |
| 564 | + // Context menu navigation links | |
| 565 | + if (e.target.closest('#context-menu a[href]')) { | |
| 566 | + closeContextMenu(); | |
| 567 | + return; | |
| 568 | + } | |
| 569 | + if (e.target.closest('#context-menu')) return; | |
| 570 | + | |
| 170 | 571 | // Toggle sidebar |
| 171 | 572 | var sbBtn = e.target.closest('[data-toggle-sidebar]'); |
| 172 | 573 | if (sbBtn) { |
| ... | ... | @@ -255,21 +656,7 @@ |
| 255 | 656 | var copyBtn = e.target.closest('[data-copy]'); |
| 256 | 657 | if (copyBtn) { |
| 257 | 658 | e.preventDefault(); |
| 258 | - var text = copyBtn.getAttribute('data-copy'); | |
| 259 | - var copiedMsg = document.documentElement.getAttribute('data-copied-text') || 'Copied'; | |
| 260 | - if (navigator.clipboard && navigator.clipboard.writeText) { | |
| 261 | - navigator.clipboard.writeText(text).then(function () { | |
| 262 | - showToast(copiedMsg); | |
| 263 | - }); | |
| 264 | - } else { | |
| 265 | - var ta = document.createElement('textarea'); | |
| 266 | - ta.value = text; | |
| 267 | - document.body.appendChild(ta); | |
| 268 | - ta.select(); | |
| 269 | - document.execCommand('copy'); | |
| 270 | - document.body.removeChild(ta); | |
| 271 | - showToast(copiedMsg); | |
| 272 | - } | |
| 659 | + copyText(copyBtn.getAttribute('data-copy')); | |
| 273 | 660 | return; |
| 274 | 661 | } |
| 275 | 662 | |
| ... | ... | @@ -310,6 +697,47 @@ |
| 310 | 697 | document.addEventListener('keydown', function (e) { |
| 311 | 698 | var palette = document.getElementById('command-palette'); |
| 312 | 699 | var isPaletteOpen = palette && palette.classList.contains('open'); |
| 700 | + var contextMenu = document.getElementById('context-menu'); | |
| 701 | + var isContextMenuOpen = contextMenu && contextMenu.classList.contains('open'); | |
| 702 | + | |
| 703 | + if (isContextMenuOpen) { | |
| 704 | + if (e.key === 'Escape') { | |
| 705 | + e.preventDefault(); | |
| 706 | + closeContextMenu(); | |
| 707 | + return; | |
| 708 | + } | |
| 709 | + if (e.key === 'ArrowDown') { | |
| 710 | + e.preventDefault(); | |
| 711 | + focusContextMenuItem(contextMenuIndex + 1); | |
| 712 | + return; | |
| 713 | + } | |
| 714 | + if (e.key === 'ArrowUp') { | |
| 715 | + e.preventDefault(); | |
| 716 | + focusContextMenuItem(contextMenuIndex - 1); | |
| 717 | + return; | |
| 718 | + } | |
| 719 | + if (e.key === 'Home') { | |
| 720 | + e.preventDefault(); | |
| 721 | + focusContextMenuItem(0); | |
| 722 | + return; | |
| 723 | + } | |
| 724 | + if (e.key === 'End') { | |
| 725 | + e.preventDefault(); | |
| 726 | + focusContextMenuItem(getContextMenuItems().length - 1); | |
| 727 | + return; | |
| 728 | + } | |
| 729 | + if (e.key === 'Tab') { | |
| 730 | + closeContextMenu(); | |
| 731 | + return; | |
| 732 | + } | |
| 733 | + if (!e.ctrlKey && !e.metaKey && !e.altKey) return; | |
| 734 | + } | |
| 735 | + | |
| 736 | + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'l') { | |
| 737 | + e.preventDefault(); | |
| 738 | + cycleTheme(); | |
| 739 | + return; | |
| 740 | + } | |
| 313 | 741 | |
| 314 | 742 | if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { |
| 315 | 743 | e.preventDefault(); |
| ... | ... | @@ -328,6 +756,7 @@ |
| 328 | 756 | } |
| 329 | 757 | |
| 330 | 758 | if (e.key === 'Escape') { |
| 759 | + closeContextMenu(); | |
| 331 | 760 | closeAllDropdowns(); |
| 332 | 761 | document.querySelectorAll('.backdrop.open').forEach(function (b) { |
| 333 | 762 | closeDialog(b); |
| ... | ... | @@ -354,33 +783,41 @@ |
| 354 | 783 | return; |
| 355 | 784 | } |
| 356 | 785 | |
| 357 | - var tag = e.target && e.target.tagName ? e.target.tagName.toUpperCase() : ''; | |
| 358 | - if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (e.target && e.target.isContentEditable)) { | |
| 786 | + if ((e.key === 'F10' && e.shiftKey) || e.key === 'ContextMenu') { | |
| 787 | + if (isEditableTarget(e.target)) return; | |
| 788 | + e.preventDefault(); | |
| 789 | + openContextMenuForKeyboard(); | |
| 359 | 790 | return; |
| 360 | 791 | } |
| 792 | + | |
| 793 | + if (isEditableTarget(e.target)) return; | |
| 361 | 794 | if (e.ctrlKey || e.metaKey || e.altKey) return; |
| 362 | 795 | |
| 796 | + var key = e.key.toLowerCase(); | |
| 363 | 797 | var now = Date.now(); |
| 364 | 798 | if (lastChordKey === 'g' && now - lastChordTime < 900) { |
| 365 | 799 | lastChordKey = ''; |
| 366 | - if (e.key.toLowerCase() === 'h') { | |
| 800 | + lastChordTime = 0; | |
| 801 | + if (handleNavigationChord(key)) { | |
| 367 | 802 | e.preventDefault(); |
| 368 | - window.location.href = '/'; | |
| 369 | - return; | |
| 370 | - } | |
| 371 | - if (e.key.toLowerCase() === 'r') { | |
| 372 | - e.preventDefault(); | |
| 373 | - window.location.href = '/explore'; | |
| 374 | 803 | return; |
| 375 | 804 | } |
| 805 | + } else if (key !== 'g') { | |
| 806 | + lastChordKey = ''; | |
| 376 | 807 | } |
| 377 | 808 | |
| 378 | - if (e.key.toLowerCase() === 'g') { | |
| 809 | + if (key === 'g') { | |
| 379 | 810 | lastChordKey = 'g'; |
| 380 | 811 | lastChordTime = now; |
| 381 | 812 | return; |
| 382 | 813 | } |
| 383 | 814 | |
| 815 | + if (key === 'y') { | |
| 816 | + e.preventDefault(); | |
| 817 | + copyText(window.location.href); | |
| 818 | + return; | |
| 819 | + } | |
| 820 | + | |
| 384 | 821 | if (e.key === '/') { |
| 385 | 822 | e.preventDefault(); |
| 386 | 823 | var searchInput = document.querySelector('[data-search-input]'); |
| ... | ... | @@ -398,7 +835,7 @@ |
| 398 | 835 | return; |
| 399 | 836 | } |
| 400 | 837 | |
| 401 | - if (e.key.toLowerCase() === 't') { | |
| 838 | + if (key === 't') { | |
| 402 | 839 | e.preventDefault(); |
| 403 | 840 | var fileFilter = document.querySelector('[data-file-filter]'); |
| 404 | 841 | if (fileFilter) { |
internal/web/views/layouts/base.html
+185
-26
| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | <!DOCTYPE html> |
| 2 | -<html lang="{{.Lang}}" data-theme="{{if .Theme}}{{.Theme}}{{else}}system{{end}}" data-copied-text="{{t .Lang "common.copied"}}"> | |
| 2 | +<html lang="{{.Lang}}" data-theme="{{if .Theme}}{{.Theme}}{{else}}system{{end}}" data-copied-text="{{t .Lang "common.copied"}}" data-copy-failed-text="{{t .Lang "common.copy_failed"}}" data-shortcut-unavailable="{{t .Lang "shortcuts.unavailable"}}"> | |
| 3 | 3 | <head> |
| 4 | 4 | <meta charset="utf-8"> |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| ... | ... | @@ -23,7 +23,7 @@ |
| 23 | 23 | <style>{{inlineCSS}}</style> |
| 24 | 24 | <link rel="stylesheet" href="/static/css/app.css?v={{assetVer}}"> |
| 25 | 25 | </head> |
| 26 | -<body> | |
| 26 | +<body{{if .CurrentUser}} data-current-user="{{.CurrentUser.Username}}"{{end}}{{if .Repo}} data-repo-path="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}" data-repo-can-write="{{if .CanWrite}}true{{else}}false{{end}}" data-repo-issues="{{if .Repo.EnableIssues}}true{{else}}false{{end}}" data-repo-pulls="{{if .Repo.EnablePulls}}true{{else}}false{{end}}" data-repo-releases="{{if .Repo.EnableReleases}}true{{else}}false{{end}}" data-repo-activity="{{if .Repo.EnableActivity}}true{{else}}false{{end}}"{{end}}> | |
| 27 | 27 | {{sprite}} |
| 28 | 28 | {{$pageBackground := ""}} |
| 29 | 29 | {{with .ProfileUser}}{{$pageBackground = imageURL .BackgroundURL}}{{end}} |
| ... | ... | @@ -328,32 +328,191 @@ |
| 328 | 328 | {{icon "close" "size-4"}} |
| 329 | 329 | </button> |
| 330 | 330 | </div> |
| 331 | - <div class="card-body flex flex-col gap-2 text-sm"> | |
| 332 | - <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> | |
| 333 | - <span>{{t .Lang "shortcuts.palette"}}</span> | |
| 334 | - <span class="flex items-center gap-1"><kbd class="kbd">Ctrl</kbd><kbd class="kbd">K</kbd></span> | |
| 335 | - </div> | |
| 336 | - <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> | |
| 337 | - <span>{{t .Lang "shortcuts.sidebar"}}</span> | |
| 338 | - <span class="flex items-center gap-1"><kbd class="kbd">Ctrl</kbd><kbd class="kbd">B</kbd></span> | |
| 339 | - </div> | |
| 340 | - <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> | |
| 341 | - <span>{{t .Lang "shortcuts.search"}}</span> | |
| 342 | - <kbd class="kbd">/</kbd> | |
| 343 | - </div> | |
| 344 | - <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> | |
| 345 | - <span>{{t .Lang "shortcuts.home"}}</span> | |
| 346 | - <span class="flex items-center gap-1"><kbd class="kbd">g</kbd><kbd class="kbd">h</kbd></span> | |
| 347 | - </div> | |
| 348 | - <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> | |
| 349 | - <span>{{t .Lang "nav.explore"}}</span> | |
| 350 | - <span class="flex items-center gap-1"><kbd class="kbd">g</kbd><kbd class="kbd">r</kbd></span> | |
| 351 | - </div> | |
| 352 | - <div class="flex items-center justify-between py-2"> | |
| 353 | - <span>{{t .Lang "shortcuts.files"}}</span> | |
| 354 | - <kbd class="kbd">t</kbd> | |
| 331 | + <div class="card-body shortcuts-list"> | |
| 332 | + <div class="shortcuts-group-label">{{t .Lang "shortcuts.group.general"}}</div> | |
| 333 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.palette"}}</span><span class="shortcut-keys"><kbd class="kbd">Ctrl</kbd><kbd class="kbd">K</kbd></span></div> | |
| 334 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.shortcuts"}}</span><kbd class="kbd">?</kbd></div> | |
| 335 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.context_menu"}}</span><span class="shortcut-keys"><kbd class="kbd">Shift</kbd><kbd class="kbd">F10</kbd></span></div> | |
| 336 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.theme"}}</span><span class="shortcut-keys"><kbd class="kbd">Ctrl</kbd><kbd class="kbd">Shift</kbd><kbd class="kbd">L</kbd></span></div> | |
| 337 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.copy_url"}}</span><kbd class="kbd">y</kbd></div> | |
| 338 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.sidebar"}}</span><span class="shortcut-keys"><kbd class="kbd">Ctrl</kbd><kbd class="kbd">B</kbd></span></div> | |
| 339 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.search"}}</span><kbd class="kbd">/</kbd></div> | |
| 340 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.close"}}</span><kbd class="kbd">Esc</kbd></div> | |
| 341 | + | |
| 342 | + <div class="shortcuts-group-label">{{t .Lang "shortcuts.group.navigation"}}</div> | |
| 343 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.home"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">h</kbd></span></div> | |
| 344 | + <div class="shortcut-row"><span>{{t .Lang "nav.explore"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">r</kbd></span></div> | |
| 345 | + {{if .CurrentUser}} | |
| 346 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.profile"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">o</kbd></span></div> | |
| 347 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.new_repo"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">n</kbd></span></div> | |
| 348 | + {{end}} | |
| 349 | + | |
| 350 | + {{if .Repo}} | |
| 351 | + <div class="shortcuts-group-label">{{t .Lang "shortcuts.group.repo"}}</div> | |
| 352 | + <div class="shortcut-row"><span>{{t .Lang "repo.tab.code"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">c</kbd></span></div> | |
| 353 | + {{if .Repo.EnableIssues}}<div class="shortcut-row"><span>{{t .Lang "repo.tab.issues"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">i</kbd></span></div>{{end}} | |
| 354 | + {{if .Repo.EnablePulls}}<div class="shortcut-row"><span>{{t .Lang "repo.tab.pulls"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">p</kbd></span></div>{{end}} | |
| 355 | + {{if .Repo.EnableReleases}}<div class="shortcut-row"><span>{{t .Lang "repo.tab.releases"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">l</kbd></span></div>{{end}} | |
| 356 | + {{if .Repo.EnableActivity}}<div class="shortcut-row"><span>{{t .Lang "repo.tab.activity"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">a</kbd></span></div>{{end}} | |
| 357 | + <div class="shortcut-row"><span>{{t .Lang "repo.tab.branches"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">b</kbd></span></div> | |
| 358 | + {{if .CanWrite}}<div class="shortcut-row"><span>{{t .Lang "repo.tab.settings"}}</span><span class="shortcut-keys"><kbd class="kbd">g</kbd><kbd class="kbd">s</kbd></span></div>{{end}} | |
| 359 | + <div class="shortcut-row"><span>{{t .Lang "shortcuts.files"}}</span><kbd class="kbd">t</kbd></div> | |
| 360 | + {{end}} | |
| 361 | + </div> | |
| 362 | + </div> | |
| 363 | + </div> | |
| 364 | + | |
| 365 | + | |
| 366 | + <!-- Custom Context Menu --> | |
| 367 | + <div id="context-menu" class="context-menu" role="menu" aria-label="{{t .Lang "contextmenu.title"}}" aria-hidden="true"> | |
| 368 | + <div class="context-menu-section" data-context-section="selection" hidden> | |
| 369 | + <div class="context-menu-label">{{t .Lang "contextmenu.selection"}}</div> | |
| 370 | + <button type="button" class="context-menu-item" data-context-action="copy-selection" role="menuitem"> | |
| 371 | + {{icon "copy" "size-3.5"}}<span>{{t .Lang "contextmenu.copy_selection"}}</span> | |
| 372 | + </button> | |
| 373 | + <button type="button" class="context-menu-item" data-context-action="search-selection" role="menuitem"> | |
| 374 | + {{icon "search" "size-3.5"}}<span>{{t .Lang "contextmenu.search_selection"}}</span> | |
| 375 | + </button> | |
| 376 | + </div> | |
| 377 | + | |
| 378 | + <div class="context-menu-section" data-context-section="link" hidden> | |
| 379 | + <div class="context-menu-label">{{t .Lang "contextmenu.link"}}</div> | |
| 380 | + <button type="button" class="context-menu-item" data-context-action="link-open" role="menuitem"> | |
| 381 | + {{icon "link" "size-3.5"}}<span>{{t .Lang "contextmenu.open_link"}}</span> | |
| 382 | + </button> | |
| 383 | + <button type="button" class="context-menu-item" data-context-action="link-copy" role="menuitem"> | |
| 384 | + {{icon "copy" "size-3.5"}}<span>{{t .Lang "contextmenu.copy_link"}}</span> | |
| 385 | + </button> | |
| 386 | + <button type="button" class="context-menu-item" data-context-action="link-text" role="menuitem"> | |
| 387 | + {{icon "file" "size-3.5"}}<span>{{t .Lang "contextmenu.copy_link_text"}}</span> | |
| 388 | + </button> | |
| 389 | + </div> | |
| 390 | + | |
| 391 | + <div class="context-menu-section" data-context-section="image" hidden> | |
| 392 | + <div class="context-menu-label">{{t .Lang "contextmenu.image"}}</div> | |
| 393 | + <button type="button" class="context-menu-item" data-context-action="image-open" role="menuitem"> | |
| 394 | + {{icon "image" "size-3.5"}}<span>{{t .Lang "contextmenu.open_image"}}</span> | |
| 395 | + </button> | |
| 396 | + <button type="button" class="context-menu-item" data-context-action="image-copy" role="menuitem"> | |
| 397 | + {{icon "copy" "size-3.5"}}<span>{{t .Lang "contextmenu.copy_image"}}</span> | |
| 398 | + </button> | |
| 399 | + <button type="button" class="context-menu-item" data-context-action="image-save" role="menuitem"> | |
| 400 | + {{icon "download" "size-3.5"}}<span>{{t .Lang "contextmenu.save_image"}}</span> | |
| 401 | + </button> | |
| 402 | + </div> | |
| 403 | + | |
| 404 | + {{if .Repo}} | |
| 405 | + <div class="context-menu-section" data-context-section="repo"> | |
| 406 | + <div class="context-menu-label"> | |
| 407 | + <span>{{t .Lang "contextmenu.repository"}}</span> | |
| 408 | + <span class="context-menu-meta">{{.Repo.OwnerUsername}}/{{.Repo.Name}}</span> | |
| 355 | 409 | </div> |
| 410 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}" role="menuitem"> | |
| 411 | + {{icon "code" "size-3.5"}}<span>{{t .Lang "repo.tab.code"}}</span> | |
| 412 | + </a> | |
| 413 | + {{if .Repo.EnableIssues}} | |
| 414 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/issues" role="menuitem"> | |
| 415 | + {{icon "issue" "size-3.5"}}<span>{{t .Lang "repo.tab.issues"}}</span> | |
| 416 | + </a> | |
| 417 | + {{end}} | |
| 418 | + {{if .Repo.EnablePulls}} | |
| 419 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/pulls" role="menuitem"> | |
| 420 | + {{icon "git-pull-request" "size-3.5"}}<span>{{t .Lang "repo.tab.pulls"}}</span> | |
| 421 | + </a> | |
| 422 | + {{end}} | |
| 423 | + {{if .Repo.EnableReleases}} | |
| 424 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/releases" role="menuitem"> | |
| 425 | + {{icon "tag" "size-3.5"}}<span>{{t .Lang "repo.tab.releases"}}</span> | |
| 426 | + </a> | |
| 427 | + {{end}} | |
| 428 | + {{if .Repo.EnableActivity}} | |
| 429 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/activity" role="menuitem"> | |
| 430 | + {{icon "activity" "size-3.5"}}<span>{{t .Lang "repo.tab.activity"}}</span> | |
| 431 | + </a> | |
| 432 | + {{end}} | |
| 433 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/branches" role="menuitem"> | |
| 434 | + {{icon "git-branch" "size-3.5"}}<span>{{t .Lang "repo.tab.branches"}}</span> | |
| 435 | + </a> | |
| 436 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/commits/{{.Repo.DefaultBranch}}" role="menuitem"> | |
| 437 | + {{icon "history" "size-3.5"}}<span>{{t .Lang "repo.tab.commits"}}</span> | |
| 438 | + </a> | |
| 356 | 439 | </div> |
| 440 | + | |
| 441 | + {{if or .CanWrite (and .CurrentUser (or .Repo.EnableIssues .Repo.EnablePulls))}} | |
| 442 | + <div class="context-menu-section" data-context-section="repo-actions"> | |
| 443 | + <div class="context-menu-label">{{t .Lang "contextmenu.repo_actions"}}</div> | |
| 444 | + {{if .CurrentUser}}{{if .Repo.EnableIssues}} | |
| 445 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/issues/new" role="menuitem"> | |
| 446 | + {{icon "issue" "size-3.5"}}<span>{{t .Lang "issues.new"}}</span> | |
| 447 | + </a> | |
| 448 | + {{end}}{{if .Repo.EnablePulls}} | |
| 449 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/pulls/new" role="menuitem"> | |
| 450 | + {{icon "git-pull-request" "size-3.5"}}<span>{{t .Lang "pulls.new"}}</span> | |
| 451 | + </a> | |
| 452 | + {{end}}{{end}} | |
| 453 | + {{if .CanWrite}} | |
| 454 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/new-file" role="menuitem"> | |
| 455 | + {{icon "plus" "size-3.5"}}<span>{{t .Lang "file.new_file"}}</span> | |
| 456 | + </a> | |
| 457 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/new-folder" role="menuitem"> | |
| 458 | + {{icon "folder" "size-3.5"}}<span>{{t .Lang "file.new_folder"}}</span> | |
| 459 | + </a> | |
| 460 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/upload" role="menuitem"> | |
| 461 | + {{icon "upload" "size-3.5"}}<span>{{t .Lang "file.upload_file"}}</span> | |
| 462 | + </a> | |
| 463 | + <a class="context-menu-item" href="/{{.Repo.OwnerUsername}}/{{.Repo.Name}}/settings" role="menuitem"> | |
| 464 | + {{icon "settings" "size-3.5"}}<span>{{t .Lang "repo.tab.settings"}}</span> | |
| 465 | + </a> | |
| 466 | + {{end}} | |
| 467 | + </div> | |
| 468 | + {{end}} | |
| 469 | + {{end}} | |
| 470 | + | |
| 471 | + <div class="context-menu-section" data-context-section="page"> | |
| 472 | + <div class="context-menu-label">{{t .Lang "contextmenu.page"}}</div> | |
| 473 | + <button type="button" class="context-menu-item" data-context-action="page-back" role="menuitem"> | |
| 474 | + {{icon "arrow-left" "size-3.5"}}<span>{{t .Lang "contextmenu.back"}}</span> | |
| 475 | + </button> | |
| 476 | + <button type="button" class="context-menu-item" data-context-action="page-forward" role="menuitem"> | |
| 477 | + {{icon "arrow-right" "size-3.5"}}<span>{{t .Lang "contextmenu.forward"}}</span> | |
| 478 | + </button> | |
| 479 | + <button type="button" class="context-menu-item" data-context-action="page-reload" role="menuitem"> | |
| 480 | + {{icon "history" "size-3.5"}}<span>{{t .Lang "contextmenu.reload"}}</span> | |
| 481 | + </button> | |
| 482 | + <button type="button" class="context-menu-item" data-context-action="page-copy" role="menuitem"> | |
| 483 | + {{icon "copy" "size-3.5"}}<span>{{t .Lang "contextmenu.copy_url"}}</span><kbd class="context-menu-kbd">y</kbd> | |
| 484 | + </button> | |
| 485 | + </div> | |
| 486 | + | |
| 487 | + <div class="context-menu-section" data-context-section="appearance"> | |
| 488 | + <div class="context-menu-label">{{t .Lang "contextmenu.appearance"}}</div> | |
| 489 | + <button type="button" class="context-menu-item" data-context-action="theme" data-theme-value="light" role="menuitemradio" aria-checked="false"> | |
| 490 | + {{icon "sun" "size-3.5"}}<span>{{t .Lang "theme.light"}}</span><span class="context-menu-check">{{icon "check" "size-3.5"}}</span> | |
| 491 | + </button> | |
| 492 | + <button type="button" class="context-menu-item" data-context-action="theme" data-theme-value="dark" role="menuitemradio" aria-checked="false"> | |
| 493 | + {{icon "moon" "size-3.5"}}<span>{{t .Lang "theme.dark"}}</span><span class="context-menu-check">{{icon "check" "size-3.5"}}</span> | |
| 494 | + </button> | |
| 495 | + <button type="button" class="context-menu-item" data-context-action="theme" data-theme-value="system" role="menuitemradio" aria-checked="false"> | |
| 496 | + {{icon "laptop" "size-3.5"}}<span>{{t .Lang "theme.system"}}</span><span class="context-menu-check">{{icon "check" "size-3.5"}}</span> | |
| 497 | + </button> | |
| 498 | + </div> | |
| 499 | + | |
| 500 | + <div class="context-menu-section" data-context-section="app"> | |
| 501 | + <div class="context-menu-label">{{t .Lang "contextmenu.app"}}</div> | |
| 502 | + <button type="button" class="context-menu-item" data-context-action="palette" role="menuitem"> | |
| 503 | + {{icon "command" "size-3.5"}}<span>{{t .Lang "shortcuts.palette"}}</span><kbd class="context-menu-kbd">Ctrl K</kbd> | |
| 504 | + </button> | |
| 505 | + <button type="button" class="context-menu-item" data-context-action="shortcuts" role="menuitem"> | |
| 506 | + {{icon "keyboard" "size-3.5"}}<span>{{t .Lang "shortcuts.title"}}</span><kbd class="context-menu-kbd">?</kbd> | |
| 507 | + </button> | |
| 508 | + {{if .CurrentUser}} | |
| 509 | + <a class="context-menu-item" href="/new" role="menuitem"> | |
| 510 | + {{icon "plus" "size-3.5"}}<span>{{t .Lang "repo.new.title"}}</span> | |
| 511 | + </a> | |
| 512 | + <a class="context-menu-item" href="/settings" role="menuitem"> | |
| 513 | + {{icon "settings" "size-3.5"}}<span>{{t .Lang "nav.settings"}}</span> | |
| 514 | + </a> | |
| 515 | + {{end}} | |
| 357 | 516 | </div> |
| 358 | 517 | </div> |
| 359 | 518 |