From 4f825e7cff1e8b1a348251d172a7aaab8655b0dd Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Jul 2026 14:22:12 -0500 Subject: [PATCH 1/4] feat(ui): retheme tokens + app chrome to mokoonyx (#822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retune theme-mokogit-{light,dark}.css token VALUES to the MokoAi mokoonyx palette (dark bg #0e1318 / card #151b22 / border #2b323b / text #e6ebf1; light accent #aec5f5 with black text on accent-filled buttons). Extend mokogit-brand.css with a MokoOnyx app-chrome layer (navbar, settings sidebars, cards, tables, buttons, dropdowns, form focus ring) β€” all via theme vars so light/dark follow automatically. Authored-by: Moko Consulting Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P --- web_src/css/mokogit-brand.css | 140 +++++++++++++++++++++ web_src/css/themes/theme-mokogit-dark.css | 47 +++---- web_src/css/themes/theme-mokogit-light.css | 98 ++++++++------- 3 files changed, 218 insertions(+), 67 deletions(-) diff --git a/web_src/css/mokogit-brand.css b/web_src/css/mokogit-brand.css index cd6bb28247..486b9094a9 100644 --- a/web_src/css/mokogit-brand.css +++ b/web_src/css/mokogit-brand.css @@ -112,3 +112,143 @@ body:has(.page-content.user.link-account) { border-radius: var(--border-radius); box-shadow: var(--mokogit-shadow-lg); } + +/* ========================================================================== + * MokoOnyx app-chrome layer + * Echoes the mokoconsulting.tech "mokoonyx" look across the main app surfaces. + * Everything below drives off theme tokens (--color-*), so light + dark follow + * their respective theme files automatically. Purely additive β€” no template, + * JS, or index.css changes; safe across upstream merges. + * ======================================================================== */ + +/* ---- Top navbar: deep-navy brand band with soft-white affordances ---- */ +#navbar, +.page-content ~ #navbar, +nav#navbar { + box-shadow: var(--mokogit-shadow-sm); +} +#navbar .item:hover, +#navbar .dropdown:hover { + background: var(--color-nav-hover-bg); +} + +/* ---- Settings sidebars (repo/user/org/admin): panel surface + clear edges ---- + * Gitea renders the settings nav as a vertical `.ui.menu`; lift it onto the + * card surface with a mokoonyx border so it reads as a distinct panel. */ +.ui.secondary.vertical.menu, +.repository .navbar .ui.menu, +.user-setting-nav .ui.vertical.menu, +.organization.settings .ui.vertical.menu { + background: var(--color-box-body); + border: 1px solid var(--color-light-border); + border-radius: var(--border-radius); +} +.ui.secondary.vertical.menu .active.item { + background: var(--color-active); + color: var(--color-text); +} +.ui.secondary.vertical.menu .item:hover { + background: var(--color-hover); +} + +/* ---- Cards / segments / boxes: mokoonyx surface + defined border ---- */ +.ui.card, +.ui.cards > .card, +.ui.segment, +.ui.attached.segment, +.repository .ui.segment { + background: var(--color-box-body); + border-color: var(--color-light-border); +} +.ui.attached.header, +.ui.segment .ui.attached.header { + background: var(--color-box-header); + border-color: var(--color-light-border); +} + +/* ---- Tables: header on box-header tone, mokoonyx row separators ---- */ +.ui.table { + background: var(--color-box-body); + border-color: var(--color-light-border); +} +.ui.table > thead > tr > th { + background: var(--color-box-header); + color: var(--color-text-light-1); + border-color: var(--color-light-border); +} +.ui.table > tbody > tr > td, +.ui.table > tbody > tr { + border-color: var(--color-light-border); +} +.ui.table > tbody > tr:hover { + background: var(--color-hover); +} + +/* ---- Buttons: primary uses accent fill with the theme's contrast text ---- + * In light this yields black text on the soft-blue accent (per theme token + * --color-primary-contrast: #0a0a0a); in dark, white on #3f8ff0. */ +.ui.primary.button, +.ui.primary.buttons .button { + background: var(--color-primary); + color: var(--color-primary-contrast); +} +.ui.primary.button:hover, +.ui.primary.buttons .button:hover { + background: var(--color-primary-hover); + color: var(--color-primary-contrast); +} +.ui.primary.button:active, +.ui.primary.buttons .button:active { + background: var(--color-primary-active); + color: var(--color-primary-contrast); +} +/* Default (non-primary) buttons: sit on the button surface with a clear edge. */ +.ui.button:not(.primary):not(.red):not(.green):not(.basic) { + background: var(--color-button); + border: 1px solid var(--color-light-border); + color: var(--color-text); +} +.ui.button:not(.primary):not(.red):not(.green):not(.basic):hover { + background: var(--color-hover-opaque); +} + +/* ---- Dropdowns: menu on card surface with defined border ---- */ +.ui.dropdown .menu { + background: var(--color-menu); + border-color: var(--color-light-border); +} +.ui.dropdown .menu > .item:hover, +.ui.dropdown .menu > .active.item { + background: var(--color-hover); + color: var(--color-text); +} + +/* ---- Form inputs + accent focus ring ---- */ +.ui.form input:not([type]), +.ui.form input[type="text"], +.ui.form input[type="email"], +.ui.form input[type="password"], +.ui.form input[type="search"], +.ui.form input[type="url"], +.ui.form input[type="number"], +.ui.form textarea, +.ui.input > input, +.ui.selection.dropdown { + background: var(--color-input-background); + border-color: var(--color-input-border); + color: var(--color-input-text); +} +.ui.form input:not([type]):focus, +.ui.form input[type="text"]:focus, +.ui.form input[type="email"]:focus, +.ui.form input[type="password"]:focus, +.ui.form input[type="search"]:focus, +.ui.form input[type="url"]:focus, +.ui.form input[type="number"]:focus, +.ui.form textarea:focus, +.ui.input > input:focus, +.ui.selection.dropdown:focus, +.ui.selection.active.dropdown { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px var(--color-primary-alpha-40); +} diff --git a/web_src/css/themes/theme-mokogit-dark.css b/web_src/css/themes/theme-mokogit-dark.css index 4355db4491..3cc7d87d84 100644 --- a/web_src/css/themes/theme-mokogit-dark.css +++ b/web_src/css/themes/theme-mokogit-dark.css @@ -213,12 +213,12 @@ gitea-theme-meta-info { --color-box-header: #151b22; --color-box-body: #10151b; --color-box-body-highlight: #151b22; - --color-text-dark: #f8f8f8; - --color-text: #d2d4d8; - --color-text-light: #c0c2c7; - --color-text-light-1: #aaadb4; - --color-text-light-2: #969aa1; - --color-text-light-3: #80858f; + --color-text-dark: #f4f7fb; + --color-text: #e6ebf1; + --color-text-light: #ccd3db; + --color-text-light-1: #b3bcc6; + --color-text-light-2: #98a2ae; + --color-text-light-3: #7e8794; --color-footer: var(--color-nav-bg); --color-timeline: #383b40; --color-input-text: var(--color-text-dark); @@ -260,8 +260,9 @@ gitea-theme-meta-info { --color-label-bg: #7a7f8a4b; --color-label-hover-bg: #7a7f8aa0; --color-label-active-bg: #7a7f8aff; - --color-accent: var(--color-primary-light-1); - --color-small-accent: var(--color-primary-light-5); + /* Links/accents on dark: mokoonyx readable light-blue (#60a5fa). */ + --color-accent: #60a5fa; + --color-small-accent: var(--color-primary-light-4); --color-highlight-fg: #87651e; --color-highlight-bg: #443a27; --color-overlay-backdrop: #080808c0; @@ -310,20 +311,22 @@ gitea-theme-meta-info { --color-syntax-traceback: #ff8686; --color-syntax-matching-bracket-bg: #00918a48; --color-syntax-nonmatching-bracket-bg: #cc484848; - /* ===== MokoOnyx surface separation (navy-tinted elevation) ===== - Establish a clear 3-step elevation so panels separate from the page: - page (darkest) -> box body -> box header/secondary (lightest). All tones - are navy-tinted (blue channel raised) to stay cohesive with the #112855 - brand nav, mirroring mokoconsulting.tech's dark surface hierarchy. */ - --color-body: #0b1017; /* page: deepest navy-black */ - --color-box-body: #121a24; /* cards/boxes: lifted, navy-tinted */ - --color-box-header: #17212e; /* box headers: lightest surface */ - --color-box-body-highlight: #17212e; - --color-secondary-bg: #17212e; - --color-card: #141d28; - --color-menu: #141d28; - --color-button: #141d28; - --color-light-border: #ffffff24; /* more visible panel edges on dark */ + /* ===== MokoOnyx dark surface separation ===== + Clear 3-step elevation: page (#0e1318) -> box body -> box header/secondary + (#151b22), with a solid #2b323b border tone for panel edges. Navy-tinted to + stay cohesive with the #112855 brand nav. */ + --color-body: #0e1318; /* page: mokoonyx body bg */ + --color-box-body: #12181f; /* cards/boxes: lifted a step off the page */ + --color-box-header: #151b22; /* box headers: mokoonyx secondary/card bg */ + --color-box-body-highlight: #151b22; + --color-secondary-bg: #151b22; + --color-card: #151b22; + --color-menu: #151b22; + --color-button: #151b22; + --color-timeline: #2b323b; + --color-input-background: #12181f; + --color-input-border: #2b323b; /* mokoonyx dark border */ + --color-light-border: #2b323b; /* solid panel edges on dark */ accent-color: var(--color-accent); color-scheme: dark; } diff --git a/web_src/css/themes/theme-mokogit-light.css b/web_src/css/themes/theme-mokogit-light.css index 55ecfcdd59..d48bfac0dc 100644 --- a/web_src/css/themes/theme-mokogit-light.css +++ b/web_src/css/themes/theme-mokogit-light.css @@ -5,34 +5,38 @@ gitea-theme-meta-info { :root { --is-dark-theme: false; - /* MokoGIT brand primary: deep navy (mokoconsulting.tech --primary #010156). Nav uses #112855 below. */ - --color-primary: #010156; - --color-primary-contrast: #ffffff; - --color-primary-dark-1: #010149; - --color-primary-dark-2: #010141; - --color-primary-dark-3: #010139; - --color-primary-dark-4: #010130; - --color-primary-dark-5: #010122; - --color-primary-dark-6: #010114; - --color-primary-dark-7: #01010a; - --color-primary-light-1: #12137a; - --color-primary-light-2: #2a2c99; - --color-primary-light-3: #4548b8; - --color-primary-light-4: #6b6ecf; - --color-primary-light-5: #b3b4de; - --color-primary-light-6: #d9daf0; - --color-primary-light-7: #f3f3fb; - --color-primary-alpha-10: #01015619; - --color-primary-alpha-20: #01015633; - --color-primary-alpha-30: #0101564b; - --color-primary-alpha-40: #01015666; - --color-primary-alpha-50: #01015680; - --color-primary-alpha-60: #01015699; - --color-primary-alpha-70: #010156b3; - --color-primary-alpha-80: #010156cc; - --color-primary-alpha-90: #010156e1; - --color-primary-hover: var(--color-primary-dark-1); - --color-primary-active: var(--color-primary-dark-2); + /* MokoGIT brand primary (mokoonyx light): soft blue accent #aec5f5 with BLACK + text on accent-filled buttons (--color-primary-contrast). The deep navy + #010156/#112855 stays available for strong brand chrome (see --color-nav-bg). + dark-* steps deepen the accent (used for hover/active + text on light bg), + light-* steps lighten it toward white for tints/badges/reactions. */ + --color-primary: #aec5f5; + --color-primary-contrast: #0a0a0a; + --color-primary-dark-1: #97b4ef; + --color-primary-dark-2: #7f9fe6; + --color-primary-dark-3: #5f83d8; + --color-primary-dark-4: #3f66c4; + --color-primary-dark-5: #2e50a5; + --color-primary-dark-6: #223d80; + --color-primary-dark-7: #182c5c; + --color-primary-light-1: #bcd0f7; + --color-primary-light-2: #cbdbf9; + --color-primary-light-3: #d9e5fb; + --color-primary-light-4: #e4edfc; + --color-primary-light-5: #eef3fd; + --color-primary-light-6: #f4f8fe; + --color-primary-light-7: #fafcff; + --color-primary-alpha-10: #aec5f519; + --color-primary-alpha-20: #aec5f533; + --color-primary-alpha-30: #aec5f54b; + --color-primary-alpha-40: #aec5f566; + --color-primary-alpha-50: #aec5f580; + --color-primary-alpha-60: #aec5f599; + --color-primary-alpha-70: #aec5f5b3; + --color-primary-alpha-80: #aec5f5cc; + --color-primary-alpha-90: #aec5f5e1; + --color-primary-hover: var(--color-primary-dark-2); + --color-primary-active: var(--color-primary-dark-3); --color-secondary: #d0d7de; --color-secondary-dark-1: #c7ced5; --color-secondary-dark-2: #b9c0c7; @@ -213,12 +217,12 @@ gitea-theme-meta-info { --color-box-header: #f1f3f5; --color-box-body: #ffffff; --color-box-body-highlight: #ecf5fd; - --color-text-dark: #01050a; - --color-text: #181c21; - --color-text-light: #30363b; - --color-text-light-1: #40474d; - --color-text-light-2: #5b6167; - --color-text-light-3: #747c84; + --color-text-dark: #101418; + --color-text: #22262a; + --color-text-light: #3a4046; + --color-text-light-1: #4a5157; + --color-text-light-2: #5f666d; + --color-text-light-3: #7a828a; --color-footer: var(--color-nav-bg); --color-timeline: #d0d7de; --color-input-text: var(--color-text-dark); @@ -260,8 +264,10 @@ gitea-theme-meta-info { --color-label-bg: #949da64b; --color-label-hover-bg: #949da6a0; --color-label-active-bg: #949da6ff; - --color-accent: var(--color-primary-light-1); - --color-small-accent: var(--color-primary-light-6); + /* Links/accents must stay a readable dark blue on the light page β€” the primary + accent is now a soft tint, so point accent at the deep-navy brand instead. */ + --color-accent: #234f88; + --color-small-accent: var(--color-primary-light-4); --color-highlight-fg: #eed200; --color-highlight-bg: #f5efc5; --color-overlay-backdrop: #080808c0; @@ -310,18 +316,20 @@ gitea-theme-meta-info { --color-syntax-traceback: #c00000; --color-syntax-matching-bracket-bg: #00b5ad38; --color-syntax-nonmatching-bracket-bg: #db282838; - /* ===== MokoOnyx surface separation (mokoconsulting.tech gray scale) ===== - Give the page a soft gray base so white cards/boxes visibly lift off it, - with a defined gray-300 border tone for clear panel edges. Mirrors the - surface hierarchy of mokoconsulting.tech (MokoOnyx --gray-100/200/300). */ - --color-body: #f0f3f6; /* page: soft gray (was #fff, blended into cards) */ - --color-box-body: #ffffff; /* cards/boxes: white, now lift off the gray page */ - --color-box-header: #e9edf1; /* box headers: gray-200 tone */ - --color-secondary-bg: #e7ebef; /* MokoOnyx gray-200 */ + /* ===== MokoOnyx light surface separation ===== + Near-white page with soft-gray cards/headers so panels lift off the page, + and a defined gray-300 border tone (#dfe3e7) for clean panel edges. */ + --color-body: #fbfcfd; /* page: near-white */ + --color-box-body: #ffffff; /* cards/boxes: white, lift off the page */ + --color-box-header: #eaedf0; /* box headers: mokoonyx gray card tone */ + --color-box-body-highlight: #eef3fd; + --color-secondary-bg: #eaedf0; /* mokoonyx gray-200 card tone */ --color-card: #ffffff; --color-menu: #ffffff; --color-button: #ffffff; - --color-light-border: #0000172e; /* stronger gray-300 border definition */ + --color-timeline: #dfe3e7; + --color-input-border: #dfe3e7; /* mokoonyx gray-300 border */ + --color-light-border: #dfe3e7; /* solid gray-300 border definition */ accent-color: var(--color-accent); color-scheme: light; } -- 2.52.0 From c77eebfe27023eacfc1fda3a49ad4fd430613381 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Jul 2026 14:22:15 -0500 Subject: [PATCH 2/4] =?UTF-8?q?feat(ui):=20toast=20success=20level=20+=20f?= =?UTF-8?q?lash=E2=86=92toast=20bridge=20(#803)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 'success' to Intent + levels.success (green var(--color-green), check icon) and export showSuccessToast; rewire info to a distinct neutral blue. New flash-toast.ts mirrors server .flash-message banners into aria-live toasts (polite success/info, assertive warning/error) and removes the banner, gated on the element existing so the no-JS banner fallback is preserved. Unit test covers showSuccessToast. Authored-by: Moko Consulting Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P --- web_src/js/features/flash-toast.ts | 62 ++++++++++++++++++++++++++++++ web_src/js/modules/toast.test.ts | 13 ++++++- web_src/js/modules/toast.ts | 9 +++++ web_src/js/types.ts | 2 +- 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 web_src/js/features/flash-toast.ts diff --git a/web_src/js/features/flash-toast.ts b/web_src/js/features/flash-toast.ts new file mode 100644 index 0000000000..05993ca6ab --- /dev/null +++ b/web_src/js/features/flash-toast.ts @@ -0,0 +1,62 @@ +import {showInfoToast, showSuccessToast, showWarningToast, showErrorToast} from '../modules/toast.ts'; +import {hideElem} from '../utils/dom.ts'; +import type {Intent} from '../types.ts'; +import type {Toast} from '../modules/toast.ts'; + +// Maps a server-rendered flash level to the matching toast function. +const toastByIntent: Record Toast | null> = { + info: showInfoToast, + success: showSuccessToast, + warning: showWarningToast, + error: showErrorToast, +}; + +// success/info are non-urgent (polite); warning/error interrupt (assertive). +function ariaLiveFor(intent: Intent): 'polite' | 'assertive' { + return intent === 'warning' || intent === 'error' ? 'assertive' : 'polite'; +} + +function intentFromFlashEl(el: Element): Intent | null { + for (const intent of Object.keys(toastByIntent) as Intent[]) { + if (el.classList.contains(`flash-${intent}`)) return intent; + } + return null; +} + +/** + * Bridge server-rendered flash banners to client-side toasts. + * + * On DOM ready, each `.flash-message` banner emitted by the server template + * (`div.ui.message.flash-message.flash-{level}`) is converted into the matching + * toast, then the original banner is hidden. + * + * Everything is gated on the banner element existing: with JavaScript disabled, + * this code never runs and the server-rendered banner remains visible, so the + * no-JS fallback is preserved. Animation is left entirely to toastify + CSS, + * which already respect `prefers-reduced-motion`; we never force motion here. + */ +export function initFlashToast(): void { + const flashEls = document.querySelectorAll('.flash-message'); + if (!flashEls.length) return; // no banners -> nothing to do, no-JS fallback intact + + for (const flashEl of flashEls) { + const intent = intentFromFlashEl(flashEl); + if (!intent) continue; // unknown/unhandled banner: leave it as-is + + const message = flashEl.textContent?.trim() ?? ''; + if (!message) { + hideElem(flashEl); + continue; + } + + const toast = toastByIntent[intent](message); + if (toast) { + // success/info polite, warning/error assertive + toast.toastElement?.setAttribute('aria-live', ariaLiveFor(intent)); + toast.toastElement?.setAttribute('role', intent === 'error' ? 'alert' : 'status'); + } + + // Remove the redundant banner now that the toast carries the message. + hideElem(flashEl); + } +} diff --git a/web_src/js/modules/toast.test.ts b/web_src/js/modules/toast.test.ts index b29e9a32bd..54f6e05fe0 100644 --- a/web_src/js/modules/toast.test.ts +++ b/web_src/js/modules/toast.test.ts @@ -1,10 +1,19 @@ -import {showInfoToast, showErrorToast, showWarningToast} from './toast.ts'; +import {showInfoToast, showSuccessToast, showErrorToast, showWarningToast} from './toast.ts'; test('showInfoToast', async () => { - showInfoToast('success πŸ˜€', {duration: -1}); + showInfoToast('info πŸ˜€', {duration: -1}); expect(document.querySelector('.toastify')).toBeTruthy(); }); +test('showSuccessToast', async () => { + const toast = showSuccessToast('success πŸ˜€', {duration: -1}); + expect(toast).toBeTruthy(); + const el = document.querySelector('.toastify'); + expect(el).toBeTruthy(); + // success renders with the green theme token, distinct from info (blue) + expect(toast!.toastElement.style.background).toBe('var(--color-green)'); +}); + test('showWarningToast', async () => { showWarningToast('warning 😐', {duration: -1}); expect(document.querySelector('.toastify')).toBeTruthy(); diff --git a/web_src/js/modules/toast.ts b/web_src/js/modules/toast.ts index c551381bab..dca50e18b7 100644 --- a/web_src/js/modules/toast.ts +++ b/web_src/js/modules/toast.ts @@ -19,6 +19,11 @@ type ToastLevels = { const levels: ToastLevels = { info: { + icon: 'octicon-info', + background: 'var(--color-blue)', + duration: 2500, + }, + success: { icon: 'octicon-check', background: 'var(--color-green)', duration: 2500, @@ -88,6 +93,10 @@ export function showInfoToast(message: string, opts?: ToastOpts): Toast | null { return showToast(message, 'info', opts); } +export function showSuccessToast(message: string, opts?: ToastOpts): Toast | null { + return showToast(message, 'success', opts); +} + export function showWarningToast(message: string, opts?: ToastOpts): Toast | null { return showToast(message, 'warning', opts); } diff --git a/web_src/js/types.ts b/web_src/js/types.ts index 1453495f79..d96ab5ef96 100644 --- a/web_src/js/types.ts +++ b/web_src/js/types.ts @@ -1,6 +1,6 @@ export type IntervalId = ReturnType; -export type Intent = 'error' | 'warning' | 'info'; +export type Intent = 'error' | 'warning' | 'info' | 'success'; export type Mention = { key: string, -- 2.52.0 From cae7846c357cf8b0bc052361a28ec0a867442c4f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Jul 2026 14:22:18 -0500 Subject: [PATCH 3/4] feat(ui): unified theme+a11y FAB with mobile collapse-into-tab (#835) New mokogit-fab.{ts,css}: a sidebar-corner FAB hosting the light/dark+AUTO theme toggle and the a11y toolbar. On mobile (<=767.98px) it collapses into a labeled universal-access tab whose panel emerges from the tab (getBoundingClientRect), closes on tap-outside/Escape; on login it relocates into the auth card as a flush bottom bar. Refactors theme-toggle.ts/a11y-menu.ts to expose mountable controls (shared state, localStorage preserved) and removes the superseded navbar toggles (hidden CSRF theme form kept so signed-in theme POST still works). Fixes a11y being unreachable on phones (navbar-right was display:none <768px). Authored-by: Moko Consulting Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P --- templates/base/head_navbar.tmpl | 29 +---- web_src/css/features/mokogit-fab.css | 167 +++++++++++++++++++++++++++ web_src/js/features/a11y-menu.ts | 50 +++++--- web_src/js/features/mokogit-fab.ts | 142 +++++++++++++++++++++++ web_src/js/features/theme-toggle.ts | 110 +++++++++++++++--- 5 files changed, 446 insertions(+), 52 deletions(-) create mode 100644 web_src/css/features/mokogit-fab.css create mode 100644 web_src/js/features/mokogit-fab.ts diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 69bfb3a66e..59b10c9771 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -40,30 +40,11 @@