diff --git a/tools/public/assets/style.css b/tools/public/assets/style.css index 7fc0beb..700ac6a 100644 --- a/tools/public/assets/style.css +++ b/tools/public/assets/style.css @@ -21,6 +21,7 @@ html, body { height: 100%; } body.app { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: radial-gradient(1200px 400px at 80% -10%, rgba(59, 79, 216, 0.08), transparent 60%), var(--bg); @@ -31,6 +32,7 @@ body.app { flex-direction: column; } [v-cloak] { display: none; } +a { text-decoration: none; } /* ── navbar ─────────────────────────────────────────────── */ .nav { @@ -60,7 +62,7 @@ body.app { /* ── layout ─────────────────────────────────────────────── */ main.container { flex: 1 0 auto; } -.narrow { max-width: 560px; } +.narrow { max-width: 560px; margin-left: auto; margin-right: auto; } .center-v { display: flex; flex-direction: column; justify-content: center; min-height: 70vh; } .page-head { margin: 2.2rem 0 1.4rem; display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; } .page-head h1 { font-size: 1.45rem; font-weight: 700; margin: 0; letter-spacing: -0.01em; } @@ -113,7 +115,8 @@ label { font-weight: 600; font-size: 13px; color: var(--ink); margin-bottom: 0.3 select.form-control { appearance: auto; } /* ── buttons ────────────────────────────────────────────── */ -.btn { border-radius: 10px; font-weight: 600; font-size: 14px; padding: 0.55rem 1.1rem; border: 1px solid transparent; } +.btn { border-radius: 10px; font-weight: 600; font-size: 14px; padding: 0.55rem 1.1rem; border: 1px solid transparent; text-decoration: none; } +a.btn:hover, a.btn:focus { text-decoration: none; } .btn:focus { box-shadow: 0 0 0 3px rgba(59, 79, 216, 0.18); } .btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; } .btn-primary:hover, .btn-primary:focus { background: var(--brand-dark); border-color: var(--brand-dark); color: #fff; } diff --git a/tools/server.js b/tools/server.js index 84b286b..5fed4f5 100644 --- a/tools/server.js +++ b/tools/server.js @@ -58,7 +58,10 @@ function readProfile(raw) { } } -function errorPage(res, status, title, message) { +function errorPage(res, status, title, message, actions = {}) { + const primary = actions.primary || { href: '/auth/login', label: 'Try again' }; + const secondary = actions.secondary === null ? null : actions.secondary || { href: '/', label: 'Back to Tools' }; + const icon = actions.icon || 'alert'; res .status(status) .type('html') @@ -71,13 +74,26 @@ function errorPage(res, status, title, message) { +
-
${ICONS.alert}
+
${ICONS[icon] || ICONS.alert}

${title}

${message}

- Try again - Back to Tools + ${primary.label} + ${secondary ? `${secondary.label}` : ''}
@@ -87,6 +103,8 @@ function errorPage(res, status, title, message) { const ICONS = { alert: '', + lock: + '', }; // ── app factory ────────────────────────────────────────────────────────────── @@ -304,8 +322,9 @@ export function createApp(overrides = {}) { return errorPage( res, 403, - 'Not permitted', - 'You are not permitted to crop images. Contact your administrator if you believe this is a mistake.' + 'No access', + 'Your account doesn\u2019t have permission to crop images. Ask a domain administrator to grant you access.', + { icon: 'lock', primary: { href: '/', label: 'Back to Tools' }, secondary: null } ); } next(); diff --git a/tools/test/cropper.test.js b/tools/test/cropper.test.js index e08f4d8..a9ef58e 100644 --- a/tools/test/cropper.test.js +++ b/tools/test/cropper.test.js @@ -237,7 +237,7 @@ test('crop gate: permitted operator loads the page; denied operator does not', a const cookies = await login(denied.base); const page = await fetch(`${denied.base}/cropper`, { headers: { cookie: cookies } }); assert.equal(page.status, 403); - assert.match(await page.text(), /not permitted/i); + assert.match(await page.text(), /permission to crop images/i); } finally { await denied.stop(); }