WebOrbiton
v1.0.0.0

StocketBase

413 lines · 17.6 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/includes/config.php';
  6. require_once __DIR__ . '/includes/database.php';
  7. require_once __DIR__ . '/includes/auth.php';
  8. require_once __DIR__ . '/includes/csrf.php';
  9. require_once __DIR__ . '/includes/font-library.php';
  10. require_once __DIR__ . '/includes/ai.php';
  11. ​
  12. Auth::boot();
  13. Auth::requireLogin();
  14. ​
  15. $currentUser = Auth::user();
  16. $currentRole = Auth::role();
  17. $interfaceAvailableFonts = FontLibrary::available();
  18. $interfaceAiAvailable = Ai::availableFor($currentRole);
  19. ​
  20. $activeTab = $_GET['tab'] ?? 'appearance';
  21. $allowedTabs = ['appearance', 'shortcuts'];
  22. if (!in_array($activeTab, $allowedTabs, true)) {
  23. $activeTab = 'appearance';
  24. }
  25. ​
  26. $dashActivePage = 'interface';
  27. $dashPageTitle = 'Interface';
  28. ​
  29. require __DIR__ . '/includes/dash-header.php';
  30. ​
  31. ?>
  32. ​
  33. <h1 class="dash-title"><?= Icons::icon("sliders", "icon icon-lg") ?>Interface</h1>
  34. ​
  35. <div class="settings-tabs">
  36. <a href="interface.php?tab=appearance" class="<?= $activeTab === 'appearance' ? 'active' : '' ?>"><?= Icons::icon("palette", "icon icon-sm") ?>Appearance</a>
  37. <a href="interface.php?tab=shortcuts" class="<?= $activeTab === 'shortcuts' ? 'active' : '' ?>"><?= Icons::icon("keyboard", "icon icon-sm") ?>Shortcuts</a>
  38. </div>
  39. ​
  40. <?php if ($activeTab === 'appearance'): ?>
  41. <div class="settings-section">
  42. <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 20px;">Personal preferences for how the dashboard and product editor look and behave on this device — text size, fonts, editor helpers. Everything here is stored in this browser only and applies just to you; it's never saved to the server or shared with other team members.</p>
  43. ​
  44. <label>Sidebar text size</label>
  45. <select id="appearance-sidebar-scale" style="width:100%;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--panel);color:var(--text);margin:6px 0 18px;">
  46. <option value="80">80%</option>
  47. <option value="90">90%</option>
  48. <option value="100">100% (default)</option>
  49. <option value="110">110%</option>
  50. <option value="120">120%</option>
  51. <option value="130">130%</option>
  52. <option value="140">140%</option>
  53. <option value="150">150%</option>
  54. </select>
  55. ​
  56. <label>Main content text size</label>
  57. <select id="appearance-main-scale" style="width:100%;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--panel);color:var(--text);margin:6px 0 18px;">
  58. <option value="80">80%</option>
  59. <option value="90">90%</option>
  60. <option value="100">100% (default)</option>
  61. <option value="110">110%</option>
  62. <option value="120">120%</option>
  63. <option value="130">130%</option>
  64. <option value="140">140%</option>
  65. <option value="150">150%</option>
  66. </select>
  67. ​
  68. <label>Editor interface font</label>
  69. <select id="appearance-ui-font" style="width:100%;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--panel);color:var(--text);margin:6px 0 18px;">
  70. <option value="">Default</option>
  71. <?php foreach ($interfaceAvailableFonts as $interfaceFont): ?>
  72. <option value="<?= htmlspecialchars($interfaceFont['display_name'], ENT_QUOTES) ?>"><?= htmlspecialchars($interfaceFont['display_name']) ?></option>
  73. <?php endforeach; ?>
  74. </select>
  75. ​
  76. <label>Content font (product editor text)</label>
  77. <select id="appearance-content-font" style="width:100%;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--panel);color:var(--text);margin:6px 0 18px;">
  78. <option value="">Default</option>
  79. <?php foreach ($interfaceAvailableFonts as $interfaceFont): ?>
  80. <option value="<?= htmlspecialchars($interfaceFont['display_name'], ENT_QUOTES) ?>"><?= htmlspecialchars($interfaceFont['display_name']) ?></option>
  81. <?php endforeach; ?>
  82. </select>
  83. ​
  84. <label style="margin-top:18px;"><input type="checkbox" id="appearance-glass"> Glass effect (translucent buttons and blurred backgrounds). Turn off for solid colors.</label>
  85. <label><input type="checkbox" id="appearance-minimal"> Minimalism (removes all transitions, animations and gradients; also turns the glass effect off). Recommended for older computers and devices.</label>
  86. <label><input type="checkbox" id="appearance-block-icons"> Show block type icons in the product editor</label>
  87. <label><input type="checkbox" id="appearance-spellcheck"> Show browser spell check underlines in the product editor</label>
  88. <label><input type="checkbox" id="appearance-paste-cleanup"> Always strip formatting when pasting into the product editor (Ctrl+Shift+V always does this regardless)</label>
  89. ​
  90. <?php if ($interfaceAiAvailable): ?>
  91. <label><input type="checkbox" id="appearance-ai-helper"> Show the AI helper button in the product and page editors</label>
  92. <?php endif; ?>
  93. ​
  94. <div class="publish-actions">
  95. <button type="button" id="appearance-reset" class="dash-btn dash-btn-small"><?= Icons::icon('trash', 'icon icon-sm') ?>Reset to defaults</button>
  96. </div>
  97. </div>
  98. ​
  99. <script>
  100. (function() {
  101. var SIDEBAR_KEY = 'stocketbase_dashboard_sidebar_scale';
  102. var MAIN_KEY = 'stocketbase_dashboard_main_scale';
  103. var UI_FONT_KEY = 'stocketbase_dashboard_ui_font';
  104. var CONTENT_FONT_KEY = 'stocketbase_dashboard_content_font';
  105. var BLOCK_ICONS_KEY = 'stocketbase_dashboard_block_icons';
  106. var SPELLCHECK_KEY = 'stocketbase_dashboard_spellcheck';
  107. var PASTE_CLEANUP_KEY = 'stocketbase_dashboard_paste_cleanup';
  108. ​
  109. var sidebarSelect = document.getElementById('appearance-sidebar-scale');
  110. var mainSelect = document.getElementById('appearance-main-scale');
  111. var blockIconsCheckbox = document.getElementById('appearance-block-icons');
  112. var spellcheckCheckbox = document.getElementById('appearance-spellcheck');
  113. var pasteCleanupCheckbox = document.getElementById('appearance-paste-cleanup');
  114. ​
  115. var GLASS_KEY = 'stocketbase_dashboard_glass';
  116. var glassCheckbox = document.getElementById('appearance-glass');
  117. ​
  118. var MINIMAL_KEY = 'stocketbase_dashboard_minimal';
  119. var minimalCheckbox = document.getElementById('appearance-minimal');
  120. ​
  121. function syncGlassCheckbox() {
  122. var minimal = localStorage.getItem(MINIMAL_KEY) === '1';
  123. glassCheckbox.disabled = minimal;
  124. glassCheckbox.checked = !minimal && localStorage.getItem(GLASS_KEY) !== '0';
  125. }
  126. ​
  127. minimalCheckbox.checked = localStorage.getItem(MINIMAL_KEY) === '1';
  128. minimalCheckbox.addEventListener('change', function() {
  129. if (this.checked) {
  130. localStorage.setItem(MINIMAL_KEY, '1');
  131. } else {
  132. localStorage.removeItem(MINIMAL_KEY);
  133. }
  134. syncGlassCheckbox();
  135. if (window.StocketBaseAppearance) {
  136. window.StocketBaseAppearance.apply();
  137. }
  138. });
  139. ​
  140. syncGlassCheckbox();
  141. glassCheckbox.addEventListener('change', function() {
  142. if (this.checked) {
  143. localStorage.removeItem(GLASS_KEY);
  144. } else {
  145. localStorage.setItem(GLASS_KEY, '0');
  146. }
  147. if (window.StocketBaseAppearance) {
  148. window.StocketBaseAppearance.apply();
  149. }
  150. });
  151. ​
  152. blockIconsCheckbox.checked = localStorage.getItem(BLOCK_ICONS_KEY) !== '0';
  153. blockIconsCheckbox.addEventListener('change', function() {
  154. localStorage.setItem(BLOCK_ICONS_KEY, this.checked ? '1' : '0');
  155. });
  156. ​
  157. spellcheckCheckbox.checked = localStorage.getItem(SPELLCHECK_KEY) !== '0';
  158. spellcheckCheckbox.addEventListener('change', function() {
  159. localStorage.setItem(SPELLCHECK_KEY, this.checked ? '1' : '0');
  160. });
  161. ​
  162. pasteCleanupCheckbox.checked = localStorage.getItem(PASTE_CLEANUP_KEY) === '1';
  163. pasteCleanupCheckbox.addEventListener('change', function() {
  164. localStorage.setItem(PASTE_CLEANUP_KEY, this.checked ? '1' : '0');
  165. });
  166. var AI_HELPER_KEY = 'stocketbase_dashboard_ai_helper';
  167. var aiHelperCheckbox = document.getElementById('appearance-ai-helper');
  168. ​
  169. if (aiHelperCheckbox) {
  170. aiHelperCheckbox.checked = localStorage.getItem(AI_HELPER_KEY) !== '0';
  171. aiHelperCheckbox.addEventListener('change', function() {
  172. localStorage.setItem(AI_HELPER_KEY, this.checked ? '1' : '0');
  173. });
  174. }
  175. var uiFontSelect = document.getElementById('appearance-ui-font');
  176. var contentFontSelect = document.getElementById('appearance-content-font');
  177. ​
  178. function loadScale(key) {
  179. var stored = parseInt(localStorage.getItem(key), 10);
  180. var valid = [80, 90, 100, 110, 120, 130, 140, 150];
  181. return valid.indexOf(stored) !== -1 ? stored : 100;
  182. }
  183. ​
  184. sidebarSelect.value = String(loadScale(SIDEBAR_KEY));
  185. mainSelect.value = String(loadScale(MAIN_KEY));
  186. uiFontSelect.value = localStorage.getItem(UI_FONT_KEY) || '';
  187. contentFontSelect.value = localStorage.getItem(CONTENT_FONT_KEY) || '';
  188. ​
  189. sidebarSelect.addEventListener('change', function() {
  190. localStorage.setItem(SIDEBAR_KEY, this.value);
  191. if (window.StocketBaseAppearance) {
  192. window.StocketBaseAppearance.apply();
  193. }
  194. });
  195. ​
  196. mainSelect.addEventListener('change', function() {
  197. localStorage.setItem(MAIN_KEY, this.value);
  198. if (window.StocketBaseAppearance) {
  199. window.StocketBaseAppearance.apply();
  200. }
  201. });
  202. ​
  203. uiFontSelect.addEventListener('change', function() {
  204. if (this.value) {
  205. localStorage.setItem(UI_FONT_KEY, this.value);
  206. } else {
  207. localStorage.removeItem(UI_FONT_KEY);
  208. }
  209. if (window.StocketBaseAppearance) {
  210. window.StocketBaseAppearance.apply();
  211. }
  212. });
  213. ​
  214. contentFontSelect.addEventListener('change', function() {
  215. if (this.value) {
  216. localStorage.setItem(CONTENT_FONT_KEY, this.value);
  217. } else {
  218. localStorage.removeItem(CONTENT_FONT_KEY);
  219. }
  220. if (window.StocketBaseAppearance) {
  221. window.StocketBaseAppearance.apply();
  222. }
  223. });
  224. ​
  225. document.getElementById('appearance-reset').addEventListener('click', function() {
  226. localStorage.removeItem(SIDEBAR_KEY);
  227. localStorage.removeItem(MAIN_KEY);
  228. localStorage.removeItem(UI_FONT_KEY);
  229. localStorage.removeItem(CONTENT_FONT_KEY);
  230. localStorage.removeItem(BLOCK_ICONS_KEY);
  231. localStorage.removeItem(SPELLCHECK_KEY);
  232. localStorage.removeItem(PASTE_CLEANUP_KEY);
  233. localStorage.removeItem(AI_HELPER_KEY);
  234. localStorage.removeItem(GLASS_KEY);
  235. localStorage.removeItem(MINIMAL_KEY);
  236. minimalCheckbox.checked = false;
  237. syncGlassCheckbox();
  238. if (aiHelperCheckbox) {
  239. aiHelperCheckbox.checked = true;
  240. }
  241. sidebarSelect.value = '100';
  242. mainSelect.value = '100';
  243. uiFontSelect.value = '';
  244. contentFontSelect.value = '';
  245. blockIconsCheckbox.checked = true;
  246. spellcheckCheckbox.checked = true;
  247. pasteCleanupCheckbox.checked = false;
  248. if (window.StocketBaseAppearance) {
  249. window.StocketBaseAppearance.apply();
  250. }
  251. });
  252. })();
  253. </script>
  254. <?php endif; ?>
  255. ​
  256. <?php if ($activeTab === 'shortcuts'): ?>
  257. <style>
  258. .shortcut-row {
  259. display: flex;
  260. align-items: center;
  261. gap: 14px;
  262. padding: 10px 0;
  263. border-bottom: 1px solid var(--border);
  264. }
  265. ​
  266. .shortcut-row:last-child {
  267. border-bottom: none;
  268. }
  269. ​
  270. .shortcut-row-label {
  271. flex: 1;
  272. font-size: 13.5px;
  273. }
  274. ​
  275. .shortcut-combo-btn {
  276. font-family: inherit;
  277. font-size: 12.5px;
  278. font-weight: 600;
  279. padding: 6px 14px;
  280. border-radius: 8px;
  281. border: 1px solid var(--border);
  282. background: var(--panel-raised);
  283. color: var(--text);
  284. cursor: pointer;
  285. min-width: 150px;
  286. text-align: center;
  287. }
  288. ​
  289. .shortcut-combo-btn:hover {
  290. border-color: var(--border-strong);
  291. }
  292. ​
  293. .shortcut-combo-btn.capturing {
  294. border-color: var(--accent);
  295. color: var(--accent);
  296. }
  297. </style>
  298. ​
  299. <div class="settings-section">
  300. <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 20px;">Keyboard shortcuts for the product editor. Stored in this browser only.</p>
  301. ​
  302. <div id="shortcuts-list"></div>
  303. ​
  304. <div class="publish-actions" style="margin-top:16px;">
  305. <button type="button" id="shortcuts-reset-all" class="dash-btn dash-btn-small"><?= Icons::icon('trash', 'icon icon-sm') ?>Reset all to defaults</button>
  306. </div>
  307. </div>
  308. ​
  309. <?= Asset::js('assets/shortcuts.js') ?>
  310. <script>
  311. (function() {
  312. var listRoot = document.getElementById('shortcuts-list');
  313. var captureRow = null;
  314. ​
  315. function comboLabel(combo) {
  316. return combo.split('+').join(' + ');
  317. }
  318. ​
  319. function render() {
  320. var shortcuts = window.StocketBaseShortcuts.getShortcuts();
  321. listRoot.innerHTML = '';
  322. ​
  323. Object.keys(shortcuts).forEach(function(action) {
  324. var entry = shortcuts[action];
  325. ​
  326. var row = document.createElement('div');
  327. row.className = 'shortcut-row';
  328. row.dataset.action = action;
  329. ​
  330. var labelEl = document.createElement('span');
  331. labelEl.className = 'shortcut-row-label';
  332. labelEl.textContent = entry.label;
  333. ​
  334. var comboBtn = document.createElement('button');
  335. comboBtn.type = 'button';
  336. comboBtn.className = 'shortcut-combo-btn';
  337. comboBtn.textContent = comboLabel(entry.combo);
  338. comboBtn.title = 'Click, then press a new key combination';
  339. ​
  340. var resetBtn = document.createElement('button');
  341. resetBtn.type = 'button';
  342. resetBtn.className = 'dash-btn-small';
  343. resetBtn.textContent = 'Reset';
  344. resetBtn.disabled = entry.isDefault;
  345. resetBtn.addEventListener('click', function() {
  346. window.StocketBaseShortcuts.resetShortcut(action);
  347. render();
  348. });
  349. ​
  350. comboBtn.addEventListener('click', function() {
  351. startCapture(action, comboBtn);
  352. });
  353. ​
  354. row.appendChild(labelEl);
  355. row.appendChild(comboBtn);
  356. row.appendChild(resetBtn);
  357. listRoot.appendChild(row);
  358. });
  359. }
  360. ​
  361. function startCapture(action, btn) {
  362. if (captureRow) {
  363. captureRow.classList.remove('capturing');
  364. }
  365. captureRow = btn;
  366. btn.classList.add('capturing');
  367. btn.textContent = 'Press a key…';
  368. ​
  369. function onKeydown(event) {
  370. event.preventDefault();
  371. event.stopPropagation();
  372. ​
  373. var combo = window.StocketBaseShortcuts.comboFromEvent(event);
  374. if (!combo) {
  375. return;
  376. }
  377. ​
  378. window.StocketBaseShortcuts.setShortcut(action, combo);
  379. cleanup();
  380. render();
  381. }
  382. ​
  383. function onBlurAway(event) {
  384. if (event.target !== btn) {
  385. cleanup();
  386. render();
  387. }
  388. }
  389. ​
  390. function cleanup() {
  391. document.removeEventListener('keydown', onKeydown, true);
  392. document.removeEventListener('mousedown', onBlurAway, true);
  393. btn.classList.remove('capturing');
  394. captureRow = null;
  395. }
  396. ​
  397. document.addEventListener('keydown', onKeydown, true);
  398. setTimeout(function() {
  399. document.addEventListener('mousedown', onBlurAway, true);
  400. }, 0);
  401. }
  402. ​
  403. document.getElementById('shortcuts-reset-all').addEventListener('click', function() {
  404. window.StocketBaseShortcuts.resetAllShortcuts();
  405. render();
  406. });
  407. ​
  408. render();
  409. })();
  410. </script>
  411. <?php endif; ?>
  412. ​
  413. <?php require __DIR__ . '/includes/dash-footer.php'; ?>