|
43 | 43 | </div> |
44 | 44 | </div> |
45 | 45 | {else} |
46 | | - <div class="card"> |
47 | | - <div class="card__header"> |
48 | | - <div> |
49 | | - <h3>Server Mods</h3> |
50 | | - <p><span data-testid="mod-count">{$mod_count}</span> configured</p> |
51 | | - </div> |
52 | | - </div> |
| 46 | + <div class="mb-4"> |
| 47 | + <h1 style="font-size:var(--fs-xl);font-weight:600;margin:0" data-testid="mods-list-title"> |
| 48 | + Mods |
| 49 | + <span class="text-faint" style="font-weight:400;margin-left:0.375rem" data-testid="mod-count">({$mod_count})</span> |
| 50 | + </h1> |
| 51 | + <p class="text-sm text-muted m-0 mt-2"> |
| 52 | + Game mods that can be assigned to bans and servers. |
| 53 | + </p> |
| 54 | + </div> |
| 55 | + |
| 56 | + <div class="card" style="overflow:hidden"> |
53 | 57 | {if $mod_count > 0} |
54 | | - <table class="table" data-testid="mods-table"> |
| 58 | + <div class="table-scroll"> |
| 59 | + <table class="table table--compact" data-testid="mods-table"> |
55 | 60 | <thead> |
56 | 61 | <tr> |
57 | 62 | <th style="width:40%">Name</th> |
|
67 | 72 | {foreach from=$mod_list item=mod} |
68 | 73 | <tr id="mid_{$mod.mid}" data-testid="mod-row" data-id="{$mod.mid}"> |
69 | 74 | <td> |
70 | | - <div class="flex items-center gap-3"> |
| 75 | + <div class="flex items-center gap-2"> |
71 | 76 | <img src="images/games/{$mod.icon}" |
72 | 77 | alt="" |
73 | | - width="20" |
74 | | - height="20" |
| 78 | + width="18" |
| 79 | + height="18" |
75 | 80 | loading="lazy" |
76 | 81 | onerror="this.style.visibility='hidden'"> |
77 | 82 | <span class="font-medium">{$mod.name}</span> |
|
87 | 92 | {/if} |
88 | 93 | </td> |
89 | 94 | {if $permission_editmods || $permission_deletemods} |
90 | | - <td style="text-align:right"> |
91 | | - <div class="flex justify-end gap-2"> |
| 95 | + <td class="col-actions" style="text-align:right"> |
| 96 | + {* Icon-only row actions matching banlist / admins list: |
| 97 | + Lucide icon + `data-tooltip` + `aria-label` inside |
| 98 | + `.row-actions--icons`. Keep `data-testid` / `data-action` |
| 99 | + / `data-fallback-href` wiring unchanged. *} |
| 100 | + <div class="row-actions row-actions--icons"> |
92 | 101 | {if $permission_editmods} |
93 | | - <a class="btn btn--ghost btn--sm" |
| 102 | + <a class="btn btn--ghost btn--icon btn--sm" |
94 | 103 | href="index.php?p=admin&c=mods&o=edit&id={$mod.mid|escape:'url'}" |
95 | | - data-testid="editmod-link">Edit</a> |
| 104 | + data-testid="editmod-link" |
| 105 | + data-tooltip="Edit" |
| 106 | + aria-label="Edit mod {$mod.name|escape}"> |
| 107 | + <i data-lucide="pencil" style="width:14px;height:14px"></i> |
| 108 | + </a> |
96 | 109 | {/if} |
97 | 110 | {if $permission_deletemods} |
98 | 111 | {* #1397: data-action wires the delete button to the inline |
|
110 | 123 | beyond the bug; the fallback is a graceful degradation |
111 | 124 | for the rare case where the JSON dispatcher itself is |
112 | 125 | missing (e.g. third-party theme that stripped api.js). *} |
113 | | - <button class="btn btn--ghost btn--sm" |
| 126 | + <button class="btn btn--ghost btn--icon btn--sm" |
114 | 127 | type="button" |
115 | 128 | data-action="mod-delete" |
116 | 129 | data-mid="{$mod.mid}" |
117 | 130 | data-name="{$mod.name|escape}" |
118 | 131 | data-fallback-href="index.php?p=admin&c=mods" |
119 | 132 | data-testid="deletemod-btn" |
120 | | - aria-label="Delete mod {$mod.name|escape}">Delete</button> |
| 133 | + data-tooltip="Delete" |
| 134 | + aria-label="Delete mod {$mod.name|escape}"> |
| 135 | + <i data-lucide="trash-2" style="width:14px;height:14px;color:var(--danger)"></i> |
| 136 | + </button> |
121 | 137 | {/if} |
122 | 138 | </div> |
123 | 139 | </td> |
|
126 | 142 | {/foreach} |
127 | 143 | </tbody> |
128 | 144 | </table> |
| 145 | + </div> |
| 146 | + |
| 147 | + {* Mobile cards — paired surface for the global |
| 148 | + `@media (max-width: 768px) { .table { display: none } }` |
| 149 | + rule. Same display dance as `.admins-list-cards`. *} |
| 150 | + <div class="mods-list-cards" data-testid="mods-list-cards"> |
| 151 | + {foreach from=$mod_list item=mod} |
| 152 | + <div class="mods-list-card" data-testid="mods-list-card" data-id="{$mod.mid}"> |
| 153 | + <div class="mods-list-card__body flex items-center gap-3"> |
| 154 | + <img src="images/games/{$mod.icon}" |
| 155 | + alt="" |
| 156 | + width="28" |
| 157 | + height="28" |
| 158 | + loading="lazy" |
| 159 | + onerror="this.style.visibility='hidden'"> |
| 160 | + <div style="flex:1;min-width:0"> |
| 161 | + <div class="font-medium text-sm truncate">{$mod.name}</div> |
| 162 | + <div class="text-xs text-muted truncate" style="margin-top:0.125rem"> |
| 163 | + <span class="font-mono">{$mod.modfolder}</span> |
| 164 | + · SU {$mod.steam_universe} |
| 165 | + </div> |
| 166 | + <div style="margin-top:0.35rem"> |
| 167 | + {if $mod.enabled} |
| 168 | + <span class="pill pill--online">Enabled</span> |
| 169 | + {else} |
| 170 | + <span class="pill pill--offline">Disabled</span> |
| 171 | + {/if} |
| 172 | + </div> |
| 173 | + </div> |
| 174 | + </div> |
| 175 | + {if $permission_editmods || $permission_deletemods} |
| 176 | + <div class="row-actions row-actions--icons ban-card__actions"> |
| 177 | + {if $permission_editmods} |
| 178 | + <a class="btn btn--ghost btn--icon btn--sm" |
| 179 | + href="index.php?p=admin&c=mods&o=edit&id={$mod.mid|escape:'url'}" |
| 180 | + data-testid="editmod-link-mobile" |
| 181 | + data-tooltip="Edit" |
| 182 | + aria-label="Edit mod {$mod.name|escape}"> |
| 183 | + <i data-lucide="pencil" style="width:14px;height:14px"></i> |
| 184 | + </a> |
| 185 | + {/if} |
| 186 | + {if $permission_deletemods} |
| 187 | + <button class="btn btn--ghost btn--icon btn--sm" |
| 188 | + type="button" |
| 189 | + data-action="mod-delete" |
| 190 | + data-mid="{$mod.mid}" |
| 191 | + data-name="{$mod.name|escape}" |
| 192 | + data-fallback-href="index.php?p=admin&c=mods" |
| 193 | + data-testid="deletemod-btn-mobile" |
| 194 | + data-tooltip="Delete" |
| 195 | + aria-label="Delete mod {$mod.name|escape}"> |
| 196 | + <i data-lucide="trash-2" style="width:14px;height:14px;color:var(--danger)"></i> |
| 197 | + </button> |
| 198 | + {/if} |
| 199 | + </div> |
| 200 | + {/if} |
| 201 | + </div> |
| 202 | + {/foreach} |
| 203 | + </div> |
129 | 204 | {else} |
130 | 205 | <div class="card__body"> |
131 | 206 | <p class="text-muted">No mods configured yet.</p> |
|
258 | 333 |
|
259 | 334 | /** |
260 | 335 | * @param {string} mid |
261 | | - * @returns {Element|null} |
| 336 | + * @returns {NodeListOf<Element>} |
262 | 337 | */ |
263 | | - function rowForMid(mid) { |
264 | | - return document.querySelector('[data-testid="mod-row"][data-id="' + mid + '"]'); |
| 338 | + function rowsForMid(mid) { |
| 339 | + return document.querySelectorAll( |
| 340 | + '[data-testid="mod-row"][data-id="' + mid + '"],' |
| 341 | + + '[data-testid="mods-list-card"][data-id="' + mid + '"]' |
| 342 | + ); |
265 | 343 | } |
266 | 344 |
|
267 | 345 | /** |
|
276 | 354 | if (!el) return; |
277 | 355 | var n = Number((el.textContent || '').replace(/[^0-9]/g, '')); |
278 | 356 | if (!Number.isFinite(n) || n <= 0) return; |
279 | | - el.textContent = String(n - 1); |
| 357 | + el.textContent = '(' + (n - 1).toLocaleString() + ')'; |
280 | 358 | } |
281 | 359 |
|
282 | 360 | /** @returns {HTMLDialogElement|null} */ |
|
397 | 475 | toast('error', 'Delete failed', msg); |
398 | 476 | return; |
399 | 477 | } |
400 | | - var row = rowForMid(ctx.mid); |
401 | | - if (row && row.parentNode) row.parentNode.removeChild(row); |
| 478 | + var rows = rowsForMid(ctx.mid); |
| 479 | + for (var i = 0; i < rows.length; i++) { |
| 480 | + var row = rows[i]; |
| 481 | + if (row && row.parentNode) row.parentNode.removeChild(row); |
| 482 | + } |
402 | 483 | decrementCount(); |
403 | 484 | closeDeleteDialog(); |
404 | 485 | toast('success', 'Mod deleted', ctx.name + ' has been removed.'); |
|
0 commit comments