@@ -40,30 +40,11 @@
|
||||
|
||||
<!-- the full dropdown menus -->
|
||||
<div class="navbar-right">
|
||||
{{/* MokoGIT: accessibility menu toggle (client-side, all users) */}}
|
||||
<button type="button" class="item mokogit-a11y-toggle" id="mokogit-a11y-toggle"
|
||||
aria-haspopup="dialog" aria-expanded="false" aria-controls="mokogit-a11y-panel"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "mokogit.a11y_title"}}"
|
||||
aria-label="{{ctx.Locale.Tr "mokogit.a11y_title"}}"
|
||||
data-label-title="{{ctx.Locale.Tr "mokogit.a11y_title"}}"
|
||||
data-label-textsize="{{ctx.Locale.Tr "mokogit.a11y_text_size"}}"
|
||||
data-label-decrease="{{ctx.Locale.Tr "mokogit.a11y_decrease"}}"
|
||||
data-label-increase="{{ctx.Locale.Tr "mokogit.a11y_increase"}}"
|
||||
data-label-reset="{{ctx.Locale.Tr "mokogit.a11y_reset"}}"
|
||||
data-label-invert="{{ctx.Locale.Tr "mokogit.a11y_invert"}}"
|
||||
data-label-contrast="{{ctx.Locale.Tr "mokogit.a11y_contrast"}}"
|
||||
data-label-links="{{ctx.Locale.Tr "mokogit.a11y_links"}}"
|
||||
data-label-readable="{{ctx.Locale.Tr "mokogit.a11y_readable"}}"
|
||||
data-label-pause="{{ctx.Locale.Tr "mokogit.a11y_pause"}}"
|
||||
data-label-resetall="{{ctx.Locale.Tr "mokogit.a11y_reset_all"}}">
|
||||
<span class="flex-text-block"><i class="fa-solid fa-universal-access"></i></span>
|
||||
</button>
|
||||
{{/* MokoGIT: light/dark theme toggle */}}
|
||||
<button type="button" class="item mokogit-theme-toggle" id="mokogit-theme-toggle"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "mokogit.theme_toggle"}}"
|
||||
aria-label="{{ctx.Locale.Tr "mokogit.theme_toggle"}}">
|
||||
<span class="flex-text-block"><i class="fa-solid fa-sun"></i><i class="fa-solid fa-moon"></i></span>
|
||||
</button>
|
||||
{{/* MokoGIT: theme + accessibility controls now live in the unified FAB
|
||||
(#mokogit-fab, built client-side by web_src/js/features/mokogit-fab.ts),
|
||||
which stays reachable on mobile and relocates into the login card.
|
||||
The hidden CSRF form below is still required so the FAB's theme toggle
|
||||
can persist a signed-in user's choice via the appearance endpoint. */}}
|
||||
{{if .IsSigned}}
|
||||
<form id="mokogit-theme-form" method="post" action="{{AppSubUrl}}/user/settings/appearance/theme" class="tw-hidden">
|
||||
{{.CsrfTokenHtml}}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/* MokoGIT unified theme + accessibility FAB (WP-C / #835).
|
||||
A corner pill on desktop, a collapsed universal-access tab on mobile, and a
|
||||
flush bottom bar inside the auth card on login pages.
|
||||
|
||||
The FAB stays a CONSISTENT BRAND BLUE in BOTH light and dark themes — its
|
||||
colors are hard-coded (not var()-driven) so it never recolors per theme. */
|
||||
|
||||
.mokogit-fab {
|
||||
--mokogit-fab-bg: #112855;
|
||||
--mokogit-fab-fg: #ffffff;
|
||||
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 900;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 6px;
|
||||
|
||||
background: var(--mokogit-fab-bg);
|
||||
color: var(--mokogit-fab-fg);
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 0.35rem 1rem rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.mokogit-fab-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.mokogit-fab .mokogit-fab-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
color: var(--mokogit-fab-fg);
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.mokogit-fab .mokogit-fab-btn:hover,
|
||||
.mokogit-fab .mokogit-fab-btn:focus-visible {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mokogit-fab .mokogit-fab-btn:focus-visible {
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* theme toggle: show moon in light mode (click -> go dark), sun in dark mode */
|
||||
.mokogit-fab .mokogit-theme-toggle .fa-moon { display: inline-block; }
|
||||
.mokogit-fab .mokogit-theme-toggle .fa-sun { display: none; }
|
||||
.mokogit-fab .mokogit-theme-toggle.is-dark .fa-moon { display: none; }
|
||||
.mokogit-fab .mokogit-theme-toggle.is-dark .fa-sun { display: inline-block; }
|
||||
.mokogit-fab .mokogit-theme-toggle.is-muted { opacity: 0.55; }
|
||||
|
||||
/* AUTO toggle */
|
||||
.mokogit-fab .mokogit-theme-auto {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.mokogit-fab .mokogit-theme-auto.is-on {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
/* thin divider between the theme group and the a11y toggle */
|
||||
.mokogit-fab .mokogit-a11y-toggle {
|
||||
position: relative;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.mokogit-fab .mokogit-a11y-toggle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -3px;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
width: 1px;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* ==== mobile: collapse into a small labeled tab ==== */
|
||||
@media (max-width: 767.98px) {
|
||||
.mokogit-fab {
|
||||
right: 0;
|
||||
bottom: 84px; /* clear of the mobile bottom bar / thumb zone */
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-left-radius: 999px;
|
||||
border-bottom-left-radius: 999px;
|
||||
}
|
||||
|
||||
/* hide the theme controls in the collapsed tab; the a11y panel exposes them
|
||||
is not needed — theme toggle stays reachable via the tab's expanded state.
|
||||
Keep theme controls visible but stacked-compact by default: show only the
|
||||
universal-access circle as the tab face. */
|
||||
.mokogit-fab .mokogit-fab-theme-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mokogit-fab .mokogit-a11y-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-left: 0;
|
||||
font-size: 20px;
|
||||
border-radius: 999px 0 0 999px;
|
||||
}
|
||||
.mokogit-fab .mokogit-a11y-toggle::before { display: none; }
|
||||
}
|
||||
|
||||
/* ==== login: flush bottom bar inside the auth card ==== */
|
||||
body.login .mokogit-fab.in-card {
|
||||
position: static;
|
||||
z-index: auto;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: auto;
|
||||
|
||||
/* flush to the card edges via negative margins; shared rounded corners */
|
||||
margin: 12px -1rem -1rem;
|
||||
padding: 8px 10px;
|
||||
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border-top: 1px solid var(--color-secondary, rgba(0, 0, 0, 0.12));
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* in-card the controls need visible (theme-colored) chrome, not white-on-blue */
|
||||
body.login .mokogit-fab.in-card .mokogit-fab-btn {
|
||||
color: var(--color-text, inherit);
|
||||
}
|
||||
body.login .mokogit-fab.in-card .mokogit-fab-btn:hover,
|
||||
body.login .mokogit-fab.in-card .mokogit-fab-btn:focus-visible {
|
||||
background: var(--color-hover, rgba(0, 0, 0, 0.06));
|
||||
}
|
||||
body.login .mokogit-fab.in-card .mokogit-fab-btn:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--color-primary, #112855);
|
||||
}
|
||||
body.login .mokogit-fab.in-card .mokogit-fab-theme-group { display: inline-flex; }
|
||||
body.login .mokogit-fab.in-card .mokogit-a11y-toggle::before {
|
||||
background: var(--color-secondary, rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
/* respect reduced-motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mokogit-fab .mokogit-fab-btn { transition: none; }
|
||||
}
|
||||
@@ -5,6 +5,12 @@
|
||||
License + attribution: web_src/css/vendor/fontawesome/LICENSE.txt and THIRD-PARTY-NOTICES.md. */
|
||||
@import "./vendor/fontawesome/css/all.min.css";
|
||||
|
||||
/* MokoGIT: vendored Bootstrap 5.3.3 (MIT) — full drop-in. Loaded BEFORE the fomantic
|
||||
replacements so Fomantic component styles override Bootstrap Reboot on desktop; Bootstrap
|
||||
grid + responsive utilities remain available to power the mobile layer (see mokogit-mobile.css).
|
||||
License: web_src/css/vendor/bootstrap/LICENSE. */
|
||||
@import "./vendor/bootstrap/bootstrap.min.css";
|
||||
|
||||
/* fomantic replacements */
|
||||
@import "./modules/button.css";
|
||||
@import "./modules/container.css";
|
||||
@@ -54,6 +60,7 @@
|
||||
@import "./features/console.css";
|
||||
@import "./features/captcha.css";
|
||||
@import "./features/a11y-menu.css";
|
||||
@import "./features/mokogit-fab.css";
|
||||
|
||||
@import "./markup/content.css";
|
||||
@import "./markup/codeblock.css";
|
||||
@@ -95,5 +102,7 @@
|
||||
|
||||
/* MokoGIT brand reskin — last so it overrides component modules (Tailwind utilities still win). */
|
||||
@import "./mokogit-brand.css";
|
||||
/* MokoGIT mobile responsiveness (Bootstrap-powered; Gitea-selector fixes Bootstrap classes can't reach). */
|
||||
@import "./mokogit-mobile.css";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
/*!
|
||||
* mokogit-mobile.css — MokoGIT mobile-responsiveness pass (WP-E / issue #719)
|
||||
* ---------------------------------------------------------------------------
|
||||
* PURPOSE
|
||||
* CSS-only mobile/touch refinements layered on top of Fomantic-UI and the
|
||||
* MokoGIT brand reskin. Targets small phones through tablets, complementing
|
||||
* (never duplicating) the responsive rules already shipped in the component
|
||||
* modules (repo.css, menu.css, flexcontainer.css, release-tag.css, …).
|
||||
*
|
||||
* LOAD ORDER
|
||||
* Imported from web_src/css/index.css AFTER mokogit-brand.css so these rules
|
||||
* win the cascade against the brand + component layers. Tailwind `tw-`
|
||||
* utilities (`@tailwind utilities;`) still load last and out-rank these, so a
|
||||
* few high-specificity / !important rules are used deliberately where we must
|
||||
* override a Fomantic inline-ish default.
|
||||
*
|
||||
* BREAKPOINTS (match the existing MokoGIT/Fomantic convention)
|
||||
* - max-width: 767.98px → phones (primary target; Fomantic grid has stacked)
|
||||
* - max-width: 991.98px → tablet tier, used only where it demonstrably helps
|
||||
* - min-width: 768px → NOT used here; this file is mobile-first additive
|
||||
*
|
||||
* THEMING
|
||||
* Only theme custom properties (var(--color-*)) are used — no hardcoded
|
||||
* colors — so every rule works in mokogit-light, mokogit-dark and -auto.
|
||||
*
|
||||
* TOUCH TARGET BASELINE
|
||||
* --mg-touch-target: 44px (WCAG 2.5.5 / Apple HIG minimum tap size)
|
||||
*
|
||||
* SCOPE / OWNERSHIP
|
||||
* Owns ONLY this file. Does NOT touch the top navbar toggles or the FAB
|
||||
* (owned by another agent). Repo sub-nav, breadcrumb, diffs, lists, tables,
|
||||
* settings sidebar and generic touch targets are in scope.
|
||||
* ---------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
:root {
|
||||
--mg-touch-target: 44px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* SECTION 1 — PR & COMMIT DIFFS
|
||||
* --------------------------------------------------------------------------
|
||||
* Goal: kill full-page horizontal scrolling. The diff table itself may still
|
||||
* scroll, but the scroll must be *contained* inside the diff file box, and on
|
||||
* phones a split diff is far too wide — collapse it toward a unified/stacked
|
||||
* presentation and let long code lines wrap instead of pushing the viewport.
|
||||
*
|
||||
* Verified markup (templates/repo/diff/box.tmpl, section_split.tmpl):
|
||||
* .diff-file-box > .diff-file-header (sticky toolbar)
|
||||
* > .diff-file-body.ui.attached.table.segment
|
||||
* > .file-body.file-code.code-diff.(code-diff-split|code-diff-unified)
|
||||
* split rows use .lines-*-old / .lines-*-new; +/- rows are .add-code / .del-code
|
||||
* ========================================================================== */
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
/* Contain diff overflow: the box never widens the page; the scrollable
|
||||
region is the code table, not the document. */
|
||||
.repository .diff-file-box,
|
||||
.repository .diff-file-box .file-body.file-code {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch; /* momentum scroll within the diff */
|
||||
}
|
||||
|
||||
/* Let the fixed table layout give up its rigidity so lines can wrap rather
|
||||
than force a wide scroll. Fomantic sets table-layout:fixed in repo.css. */
|
||||
.repository .diff-file-box .code-diff table {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Wrap long code lines inside diff cells (both unified and split). Without
|
||||
this a single long line drags the whole table wider than the phone. */
|
||||
.repository .diff-file-box .code-diff .lines-code code,
|
||||
.repository .diff-file-box .code-diff .lines-code .code-inner {
|
||||
white-space: pre-wrap !important;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Tighten the gutters so wrapped code has more room. */
|
||||
.repository .diff-file-box .code-diff .lines-num {
|
||||
padding: 0 3px !important;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Sticky diff toolbar: allow it to wrap so the filename + action buttons
|
||||
don't overflow. The file name stays readable and truncates gracefully. */
|
||||
.repository .diff-file-box .diff-file-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
row-gap: 4px;
|
||||
}
|
||||
.repository .diff-file-box .diff-file-header .diff-file-name {
|
||||
min-width: 0;
|
||||
}
|
||||
.repository .diff-file-box .diff-file-header .file-link {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/*
|
||||
* SPLIT DIFF → stacked "unified-like" presentation.
|
||||
* A two-pane split diff is unusable at phone widths. Collapse the split
|
||||
* table so the old (left) and new (right) columns stack: each cell becomes
|
||||
* full width, so deletions render above additions in a single readable
|
||||
* column while still scoped inside the contained box.
|
||||
*/
|
||||
.repository .diff-file-box .code-diff-split table,
|
||||
.repository .diff-file-box .code-diff-split tbody,
|
||||
.repository .diff-file-box .code-diff-split tr {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.repository .diff-file-box .code-diff-split tr > td,
|
||||
.repository .diff-file-box .code-diff-split .lines-num-old,
|
||||
.repository .diff-file-box .code-diff-split .lines-num-new,
|
||||
.repository .diff-file-box .code-diff-split .lines-code-old,
|
||||
.repository .diff-file-box .code-diff-split .lines-code-new {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
/* Old/new code halves each take (nearly) the full row width when stacked. */
|
||||
.repository .diff-file-box .code-diff-split .lines-code-old,
|
||||
.repository .diff-file-box .code-diff-split .lines-code-new {
|
||||
width: calc(100% - 40px);
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* SECTION 2 — NAVIGATION / HEADER + REPO SUB-MENUS
|
||||
* --------------------------------------------------------------------------
|
||||
* Scope note: the top navbar toggles + FAB are OWNED BY ANOTHER AGENT and are
|
||||
* intentionally untouched here. This section only tames the repo sub-nav tab
|
||||
* bar (Code/Issues/PRs/Actions/Wiki/Settings), the breadcrumb, and secondary
|
||||
* menus so they scroll/stack cleanly instead of clipping.
|
||||
*
|
||||
* Verified markup (templates/repo/header.tmpl):
|
||||
* overflow-menu.ui.secondary.pointing.menu (repo tab bar, JS web-component)
|
||||
* > .overflow-menu-items > a.item(.active)
|
||||
* > .overflow-menu-button (JS-managed overflow popup)
|
||||
* .secondary-nav / .repo-header / .breadcrumb / .breadcrumb-divider
|
||||
* ========================================================================== */
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
/* Repo tab bar: the overflow-menu web component already pushes extras into a
|
||||
popup, but on narrow screens allow the visible strip to scroll sideways
|
||||
instead of clipping, and keep the page from scrolling. */
|
||||
.repository overflow-menu.ui.secondary.pointing.menu {
|
||||
max-width: 100%;
|
||||
}
|
||||
.repository overflow-menu .overflow-menu-items {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none; /* Firefox: hide the horizontal scrollbar chrome */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.repository overflow-menu .overflow-menu-items::-webkit-scrollbar {
|
||||
display: none; /* WebKit: hide scrollbar; keep the swipe affordance */
|
||||
}
|
||||
.repository overflow-menu .overflow-menu-items .item {
|
||||
flex: 0 0 auto; /* don't squeeze tabs; let them scroll */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Breadcrumb (owner / repo / path…): allow it to wrap and let long path
|
||||
segments break so the repo header never overflows the viewport. */
|
||||
.repository .breadcrumb,
|
||||
.repository .repo-header .breadcrumb {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
.repository .breadcrumb .repo-icon,
|
||||
.repository .breadcrumb a,
|
||||
.repository .breadcrumb .breadcrumb-divider {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Secondary sub-menus (e.g. filter bars) that use a horizontal Fomantic menu
|
||||
get horizontal scroll containment rather than wrapping into a tall stack. */
|
||||
.repository .secondary-nav .ui.secondary.menu,
|
||||
.repository .ui.secondary.menu.tw-overflow-x-auto {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* SECTION 3 — TABLES & LISTS
|
||||
* --------------------------------------------------------------------------
|
||||
* Issue lists, milestone tables and release pages. Most already stack via
|
||||
* flex-wrap, so here we (a) hide low-value metadata on the smallest phones to
|
||||
* cut clutter, (b) card-ify any remaining wide non-`unstackable` .ui.table by
|
||||
* stacking cells, and (c) let the admin/dashboard grids scroll-contain wide
|
||||
* tables instead of widening the page.
|
||||
*
|
||||
* Verified markup:
|
||||
* #issue-list.flex-divided-list.items-with-main > .item
|
||||
* > .item-leading | .item-main(.item-header/.item-body) | .item-trailing
|
||||
* .item-body children: a.index, a.milestone, a.project, .checklist, .due-date
|
||||
* .milestone-list > .milestone-card (milestones)
|
||||
* #release-list > .release-entry (releases — already stacks in release-tag.css)
|
||||
* ========================================================================== */
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
/* Issue/PR list rows: keep the title prominent; trailing (assignees/avatars)
|
||||
already wraps via flex-list.css — nudge it to sit under the main column. */
|
||||
#issue-list.items-with-main > .item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#issue-list.items-with-main > .item .item-trailing {
|
||||
flex-basis: 100%;
|
||||
justify-content: flex-start;
|
||||
padding-left: 26px; /* align under title, past the leading checkbox/icon */
|
||||
}
|
||||
|
||||
/* Generic wide data tables that are NOT explicitly .unstackable: stack each
|
||||
row into a card so cells read top-to-bottom instead of scrolling wide.
|
||||
(Fomantic marks tables that must stay tabular with .unstackable.) */
|
||||
.ui.table:not(.unstackable):not(.single.line) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* When a table genuinely must stay tabular (code, diffs, unstackable), keep
|
||||
it scroll-contained inside its own wrapper rather than widening the page. */
|
||||
.ui.table.unstackable,
|
||||
.admin .ui.table,
|
||||
#repo-files-table {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smallest phones (≤ 480px): shed non-essential issue-row metadata to reduce
|
||||
vertical noise. Milestone/project chips and the checklist progress are the
|
||||
least critical; the index (#123), title and labels always remain. */
|
||||
@media (max-width: 480px) {
|
||||
#issue-list.items-with-main > .item .item-body .milestone,
|
||||
#issue-list.items-with-main > .item .item-body .project,
|
||||
#issue-list.items-with-main > .item .item-body .checklist {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet tier: give very wide admin tables a horizontal scroll container so
|
||||
they don't force the whole layout wider between 768–991px. */
|
||||
@media (max-width: 991.98px) {
|
||||
.admin table.ui.table,
|
||||
.user.settings table.ui.table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* SECTION 4 — SETTINGS PAGES (repo/user/org/admin left sidebar)
|
||||
* --------------------------------------------------------------------------
|
||||
* flexcontainer.css already stacks .flex-container to a column below 768px and
|
||||
* moves .flex-container-nav above the content (order:-1). But the vertical menu
|
||||
* then becomes a tall ~15-item column. Convert it into a horizontal, swipeable
|
||||
* strip so it behaves like a compact top nav / drawer instead of a cramped
|
||||
* stack — while keeping the active item visible.
|
||||
*
|
||||
* Verified markup (templates/{repo,user,org}/settings/navbar.tmpl):
|
||||
* .flex-container-nav > .ui.fluid.vertical.menu
|
||||
* > .header.item | a.item(.active) | details.toggleable-item > summary + .menu
|
||||
* ========================================================================== */
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
/* The nav wrapper is already full width + order:-1 (flexcontainer.css). Make
|
||||
it a horizontally scrolling rail so the menu doesn't eat vertical space. */
|
||||
.flex-container-nav {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
.flex-container-nav .ui.fluid.vertical.menu,
|
||||
.flex-container-nav .ui.vertical.menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
width: 100% !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.flex-container-nav .ui.vertical.menu::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Each item becomes a horizontal pill; no full-width stacking. */
|
||||
.flex-container-nav .ui.vertical.menu > .item,
|
||||
.flex-container-nav .ui.vertical.menu > .item.toggleable-item {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
border: none !important; /* drop the vertical-menu divider borders */
|
||||
min-height: var(--mg-touch-target);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* The "Settings" header label item wastes horizontal room on mobile. */
|
||||
.flex-container-nav .ui.vertical.menu > .header.item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Keep the active tab obvious with an underline (matches the pointing-menu
|
||||
idiom used by the repo sub-nav) rather than a full background block. */
|
||||
.flex-container-nav .ui.vertical.menu > .active.item {
|
||||
background: var(--color-active);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* Collapsible <details> groups (e.g. Actions → runners/secrets): when open,
|
||||
let the nested sub-menu drop below the rail as a full-width sub-list so it
|
||||
stays usable even inside a horizontal parent. */
|
||||
.flex-container-nav .ui.vertical.menu > details.toggleable-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.flex-container-nav .ui.vertical.menu > details.toggleable-item[open] {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
.flex-container-nav .ui.vertical.menu > details.toggleable-item[open] > .menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* SECTION 5 — TOUCH TARGETS (≥ 44×44px)
|
||||
* --------------------------------------------------------------------------
|
||||
* Ensure interactive controls meet the 44px minimum tap size on touch. Scoped
|
||||
* with a coarse-pointer query where appropriate so desktop density is
|
||||
* unaffected, plus a width-based fallback for phones without pointer support
|
||||
* reporting.
|
||||
*
|
||||
* Verified selectors: .ui.button (button.css), .ui.pagination.menu .item and
|
||||
* .ui.tabular.menu .item / .ui.menu .item (menu.css), .fold-file / .btn
|
||||
* (diff toolbar buttons), overflow-menu items (base.css).
|
||||
* ========================================================================== */
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
/* Buttons: guarantee a comfortable tap height without distorting layout. */
|
||||
.ui.button,
|
||||
.ui.buttons > .button {
|
||||
min-height: var(--mg-touch-target);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Icon-only / tiny buttons (diff toolbar fold, copy, kebab) — pad the hit
|
||||
area out to 44px even though the glyph stays small. */
|
||||
.ui.tiny.button,
|
||||
.ui.mini.button,
|
||||
.btn.fold-file,
|
||||
.diff-file-header .btn,
|
||||
.repository .diff-file-header-actions .ui.compact.icon.buttons .button {
|
||||
min-width: var(--mg-touch-target);
|
||||
min-height: var(--mg-touch-target);
|
||||
}
|
||||
|
||||
/* Pagination: on mobile menu.css hides all but active + prev/next; make the
|
||||
survivors full 44px tap targets. */
|
||||
.ui.pagination.menu .item {
|
||||
min-width: var(--mg-touch-target);
|
||||
min-height: var(--mg-touch-target);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Tab selectors and generic menu items (repo sub-nav, settings rail, tabular
|
||||
tabs) get a 44px minimum so they're easy to hit. */
|
||||
.ui.tabular.menu .item,
|
||||
.ui.secondary.pointing.menu .item,
|
||||
overflow-menu .overflow-menu-items .item,
|
||||
.ui.menu:not(.pagination) .item {
|
||||
min-height: var(--mg-touch-target);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* The overflow-menu popup button (repo sub-nav "more") also needs a real
|
||||
tap size; base.css sizes it 32px wide for desktop. */
|
||||
overflow-menu .overflow-menu-button {
|
||||
min-width: var(--mg-touch-target);
|
||||
min-height: var(--mg-touch-target);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fine-tune purely for touch devices regardless of width: checkboxes and small
|
||||
links in lists benefit from a padded hit area on any touch screen. */
|
||||
@media (pointer: coarse) {
|
||||
.issue-checkbox,
|
||||
.flex-divided-list .item input[type="checkbox"] {
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
}
|
||||
/* Anchor-style menu/list actions get a minimum tap height on coarse pointers. */
|
||||
.ui.list .item > a,
|
||||
.repository .ui.tabs .item {
|
||||
min-height: var(--mg-touch-target);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2024 The Bootstrap Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
File diff suppressed because one or more lines are too long
@@ -137,37 +137,54 @@ function buildPanel(btn: HTMLElement, state: A11yState): HTMLElement {
|
||||
return panel;
|
||||
}
|
||||
|
||||
export function initA11yMenu(): void {
|
||||
const btn = document.querySelector<HTMLButtonElement>('#mokogit-a11y-toggle');
|
||||
if (!btn) return;
|
||||
|
||||
// Wire an accessibility toggle button to the a11y panel. The button may live in
|
||||
// the navbar (legacy) or inside the MokoGIT FAB/tab. An optional positioner lets
|
||||
// the caller (e.g. the mobile tab) override where the panel emerges from; by
|
||||
// default the panel drops from the button's bottom-right corner.
|
||||
//
|
||||
// Returns { open, close, isOpen } so the FAB can coordinate (e.g. close on
|
||||
// tap-outside of the whole FAB, reposition on the mobile tab).
|
||||
export function mountA11yToggle(
|
||||
btn: HTMLElement,
|
||||
positioner?: (panel: HTMLElement) => void,
|
||||
): {open: () => void; close: () => void; isOpen: () => boolean} {
|
||||
const state = load();
|
||||
apply(state);
|
||||
let panel = buildPanel(btn, state);
|
||||
const panel = buildPanel(btn, state);
|
||||
document.body.append(panel);
|
||||
|
||||
const getPanel = () => document.querySelector('#mokogit-a11y-panel') as HTMLElement;
|
||||
|
||||
const positionPanel = () => {
|
||||
const p = getPanel();
|
||||
if (positioner) {
|
||||
positioner(p);
|
||||
return;
|
||||
}
|
||||
const r = btn.getBoundingClientRect();
|
||||
panel.style.top = `${Math.round(r.bottom + 6)}px`;
|
||||
panel.style.right = `${Math.round(window.innerWidth - r.right)}px`;
|
||||
p.style.top = `${Math.round(r.bottom + 6)}px`;
|
||||
p.style.right = `${Math.round(window.innerWidth - r.right)}px`;
|
||||
p.style.bottom = 'auto';
|
||||
p.style.left = 'auto';
|
||||
};
|
||||
|
||||
const isOpen = () => !getPanel().hidden;
|
||||
|
||||
const open = () => {
|
||||
panel = document.querySelector('#mokogit-a11y-panel') as HTMLElement;
|
||||
positionPanel();
|
||||
panel.hidden = false;
|
||||
getPanel().hidden = false;
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
document.addEventListener('click', onDocClick, true);
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
};
|
||||
const close = () => {
|
||||
(document.querySelector('#mokogit-a11y-panel') as HTMLElement).hidden = true;
|
||||
getPanel().hidden = true;
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
document.removeEventListener('click', onDocClick, true);
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
};
|
||||
const onDocClick = (e: MouseEvent) => {
|
||||
const p = document.querySelector('#mokogit-a11y-panel') as HTMLElement;
|
||||
const p = getPanel();
|
||||
if (p.contains(e.target as Node) || btn.contains(e.target as Node)) return;
|
||||
close();
|
||||
};
|
||||
@@ -177,7 +194,14 @@ export function initA11yMenu(): void {
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const p = document.querySelector('#mokogit-a11y-panel') as HTMLElement;
|
||||
p.hidden ? open() : close();
|
||||
isOpen() ? close() : open();
|
||||
});
|
||||
|
||||
return {open, close, isOpen};
|
||||
}
|
||||
|
||||
export function initA11yMenu(): void {
|
||||
const btn = document.querySelector<HTMLButtonElement>('#mokogit-a11y-toggle');
|
||||
if (!btn) return;
|
||||
mountA11yToggle(btn);
|
||||
}
|
||||
|
||||
@@ -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<Intent, (message: string, opts?: any) => 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<HTMLElement>('.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// MokoGIT unified theme + accessibility FAB (WP-C / #835).
|
||||
//
|
||||
// Desktop (>=768px): a fixed pill in the sidebar/screen corner hosting the
|
||||
// light/dark theme toggle, an AUTO toggle, and the accessibility toggle. The
|
||||
// pill stays a CONSISTENT BLUE in both light and dark themes (see CSS).
|
||||
//
|
||||
// Mobile (<=767.98px): the cluster collapses into a small labeled tab (a
|
||||
// universal-access circle) fixed at the screen edge. Tapping it pops the
|
||||
// accessibility panel, positioned to emerge from the tab so tab + panel read as
|
||||
// one continuous bar. Tap-outside / Escape closes.
|
||||
//
|
||||
// Login pages (body.login): a MutationObserver relocates the FAB into the auth
|
||||
// card as a flush bottom bar (.in-card), since there is no navbar/sidebar there.
|
||||
//
|
||||
// Reuses mountThemeToggle() + mountA11yToggle() so all persistence
|
||||
// (localStorage "moko-a11y", the theme CSRF form / "mokogit-theme") lives in
|
||||
// those modules — the FAB only owns layout + responsive behavior.
|
||||
|
||||
import {mountThemeToggle} from './theme-toggle.ts';
|
||||
import {mountA11yToggle} from './a11y-menu.ts';
|
||||
|
||||
const MOBILE_MAX = 767.98;
|
||||
|
||||
// Read localized labels off the (removed) navbar toggle if present, else the FAB
|
||||
// root's own data-* attributes, else English fallbacks. Kept minimal — the a11y
|
||||
// panel reads its own labels from the toggle button's dataset.
|
||||
function isMobile(): boolean {
|
||||
return window.matchMedia?.(`(max-width: ${MOBILE_MAX}px)`).matches ?? window.innerWidth <= MOBILE_MAX;
|
||||
}
|
||||
|
||||
export function initMokogitFab(): void {
|
||||
// Guard against double-init.
|
||||
if (document.querySelector('#mokogit-fab')) return;
|
||||
|
||||
const fab = document.createElement('div');
|
||||
fab.id = 'mokogit-fab';
|
||||
fab.className = 'mokogit-fab';
|
||||
|
||||
// --- theme controls (light/dark + auto) ---
|
||||
const themeGroup = mountThemeToggle();
|
||||
fab.append(themeGroup);
|
||||
|
||||
// --- accessibility toggle button (the FAB owns the button; a11y-menu owns the panel) ---
|
||||
const a11yBtn = document.createElement('button');
|
||||
a11yBtn.type = 'button';
|
||||
a11yBtn.className = 'mokogit-fab-btn mokogit-a11y-toggle';
|
||||
a11yBtn.id = 'mokogit-a11y-toggle';
|
||||
a11yBtn.setAttribute('aria-haspopup', 'dialog');
|
||||
a11yBtn.setAttribute('aria-expanded', 'false');
|
||||
a11yBtn.setAttribute('aria-controls', 'mokogit-a11y-panel');
|
||||
|
||||
// Carry the localized a11y labels: prefer any that a template stamped on the
|
||||
// FAB root, fall back to English. buildPanel() reads them off this button.
|
||||
const labelDefaults: Record<string, string> = {
|
||||
labelTitle: 'Accessibility',
|
||||
labelTextsize: 'Text size',
|
||||
labelDecrease: 'Decrease text size',
|
||||
labelIncrease: 'Increase text size',
|
||||
labelReset: 'Reset',
|
||||
labelInvert: 'Invert colors',
|
||||
labelContrast: 'High contrast',
|
||||
labelLinks: 'Highlight links',
|
||||
labelReadable: 'Readable font',
|
||||
labelPause: 'Pause animations',
|
||||
labelResetall: 'Reset all',
|
||||
};
|
||||
for (const [key, fallback] of Object.entries(labelDefaults)) {
|
||||
a11yBtn.dataset[key] = fab.dataset[key] ?? fallback;
|
||||
}
|
||||
a11yBtn.setAttribute('aria-label', a11yBtn.dataset.labelTitle || 'Accessibility');
|
||||
|
||||
const a11yIconWrap = document.createElement('span');
|
||||
a11yIconWrap.className = 'flex-text-block';
|
||||
const a11yIcon = document.createElement('i');
|
||||
a11yIcon.className = 'fa-solid fa-universal-access';
|
||||
a11yIconWrap.append(a11yIcon);
|
||||
a11yBtn.append(a11yIconWrap);
|
||||
fab.append(a11yBtn);
|
||||
|
||||
document.body.append(fab);
|
||||
|
||||
// On mobile the panel should emerge from the tab: pin it to the FAB's fixed
|
||||
// corner (bottom/right) so tab + panel read as one continuous bar.
|
||||
const mobilePositioner = (panel: HTMLElement) => {
|
||||
if (!isMobile()) {
|
||||
// desktop default: drop from the a11y button
|
||||
const r = a11yBtn.getBoundingClientRect();
|
||||
panel.style.top = `${Math.round(r.bottom + 6)}px`;
|
||||
panel.style.right = `${Math.round(window.innerWidth - r.right)}px`;
|
||||
panel.style.bottom = 'auto';
|
||||
panel.style.left = 'auto';
|
||||
return;
|
||||
}
|
||||
const r = fab.getBoundingClientRect();
|
||||
// Emerge upward from the tab: bottom edge just above the tab, aligned right.
|
||||
panel.style.bottom = `${Math.round(window.innerHeight - r.top + 6)}px`;
|
||||
panel.style.right = `${Math.round(window.innerWidth - r.right)}px`;
|
||||
panel.style.top = 'auto';
|
||||
panel.style.left = 'auto';
|
||||
};
|
||||
|
||||
const a11y = mountA11yToggle(a11yBtn, mobilePositioner);
|
||||
|
||||
// Tap-outside closes the a11y panel when it is open — check the whole FAB and
|
||||
// the panel, so tapping elsewhere on the collapsed tab bar still counts.
|
||||
document.addEventListener(
|
||||
'click',
|
||||
(e) => {
|
||||
if (!a11y.isOpen()) return;
|
||||
const panel = document.querySelector('#mokogit-a11y-panel');
|
||||
const t = e.target as Node;
|
||||
if (fab.contains(t) || (panel && panel.contains(t))) return;
|
||||
a11y.close();
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// --- login relocation: move the FAB into the auth card as a flush bottom bar ---
|
||||
if (document.body.classList.contains('login')) {
|
||||
const tryRelocate = (): boolean => {
|
||||
const card =
|
||||
document.querySelector<HTMLElement>('.mokogit-auth-card') ||
|
||||
document.querySelector<HTMLElement>('.user.signin .ui.container .segment') ||
|
||||
document.querySelector<HTMLElement>('body.login .card') ||
|
||||
document.querySelector<HTMLElement>('.user.signin .ui.container') ||
|
||||
document.querySelector<HTMLElement>('.page-content.user .ui.container');
|
||||
if (!card) return false;
|
||||
card.append(fab);
|
||||
fab.classList.add('in-card');
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!tryRelocate()) {
|
||||
const mo = new MutationObserver(() => {
|
||||
if (tryRelocate()) mo.disconnect();
|
||||
});
|
||||
mo.observe(document.body, {childList: true, subtree: true});
|
||||
// Stop watching after a while so we don't observe forever.
|
||||
window.setTimeout(() => mo.disconnect(), 8000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,57 @@
|
||||
// MokoGIT navbar light/dark toggle.
|
||||
// MokoGIT theme toggle.
|
||||
// Signed-in users: persists to the account via the existing
|
||||
// /user/settings/appearance/theme endpoint (a hidden CSRF-protected form is
|
||||
// rendered in the navbar), so the choice syncs across devices; the page reloads
|
||||
// with the new theme. Anonymous users: best-effort data-theme flip + localStorage
|
||||
// (only the server-served theme CSS is loaded for anon, so effect is limited).
|
||||
//
|
||||
// Two entry points:
|
||||
// - initThemeToggle(): legacy navbar button (#mokogit-theme-toggle), kept for
|
||||
// backwards-compat if that button is ever rendered.
|
||||
// - mountThemeToggle(): builds and returns the light/dark toggle + an AUTO
|
||||
// toggle for the unified MokoGIT FAB. Shares all persistence logic below.
|
||||
|
||||
const LIGHT = 'mokogit-light';
|
||||
const DARK = 'mokogit-dark';
|
||||
const AUTO = 'mokogit-auto';
|
||||
const LS_KEY = 'mokogit-theme';
|
||||
|
||||
function rawTheme(): string {
|
||||
return document.documentElement.getAttribute('data-theme') || '';
|
||||
}
|
||||
|
||||
function currentIsAuto(): boolean {
|
||||
return rawTheme().includes('auto');
|
||||
}
|
||||
|
||||
function currentIsDark(): boolean {
|
||||
const t = document.documentElement.getAttribute('data-theme') || '';
|
||||
const t = rawTheme();
|
||||
if (t.includes('dark')) return true;
|
||||
if (t.includes('light')) return false;
|
||||
// auto / unknown -> follow the OS preference
|
||||
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
||||
}
|
||||
|
||||
// Persist a theme choice. Signed users POST the hidden CSRF form (server persists
|
||||
// + redirects, re-rendering with the new theme). Anonymous users get a best-effort
|
||||
// data-theme flip + localStorage. Returns true if a form submit was triggered
|
||||
// (caller should not attempt further DOM sync in that case — the page reloads).
|
||||
function persistTheme(target: string): boolean {
|
||||
const form = document.querySelector<HTMLFormElement>('#mokogit-theme-form');
|
||||
if (form) {
|
||||
const input = form.querySelector<HTMLInputElement>('input[name="theme"]');
|
||||
if (input) input.value = target;
|
||||
form.submit(); // server persists + redirects back, re-rendering with the new theme
|
||||
return true;
|
||||
}
|
||||
// anonymous best-effort
|
||||
document.documentElement.setAttribute('data-theme', target);
|
||||
try {
|
||||
localStorage.setItem(LS_KEY, target);
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function initThemeToggle(): void {
|
||||
const btn = document.querySelector<HTMLButtonElement>('#mokogit-theme-toggle');
|
||||
if (!btn) return;
|
||||
@@ -25,19 +60,64 @@ export function initThemeToggle(): void {
|
||||
sync();
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const target = currentIsDark() ? LIGHT : DARK;
|
||||
const form = document.querySelector<HTMLFormElement>('#mokogit-theme-form');
|
||||
if (form) {
|
||||
const input = form.querySelector<HTMLInputElement>('input[name="theme"]');
|
||||
if (input) input.value = target;
|
||||
form.submit(); // server persists + redirects back, re-rendering with the new theme
|
||||
return;
|
||||
}
|
||||
// anonymous best-effort
|
||||
document.documentElement.setAttribute('data-theme', target);
|
||||
try {
|
||||
localStorage.setItem(LS_KEY, target);
|
||||
} catch {}
|
||||
if (persistTheme(currentIsDark() ? LIGHT : DARK)) return;
|
||||
sync();
|
||||
});
|
||||
}
|
||||
|
||||
// Build the theme controls (light/dark toggle + AUTO toggle) for the FAB.
|
||||
// Returns a fragment-ready container element that the FAB mounts into its shell.
|
||||
export function mountThemeToggle(): HTMLElement {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'mokogit-fab-group mokogit-fab-theme-group';
|
||||
|
||||
// light/dark toggle (sun/moon)
|
||||
const themeBtn = document.createElement('button');
|
||||
themeBtn.type = 'button';
|
||||
themeBtn.className = 'mokogit-fab-btn mokogit-theme-toggle';
|
||||
themeBtn.id = 'mokogit-theme-toggle';
|
||||
themeBtn.setAttribute('aria-label', 'Toggle light / dark theme');
|
||||
const themeIcons = document.createElement('span');
|
||||
themeIcons.className = 'flex-text-block';
|
||||
const sunIcon = document.createElement('i');
|
||||
sunIcon.className = 'fa-solid fa-sun';
|
||||
const moonIcon = document.createElement('i');
|
||||
moonIcon.className = 'fa-solid fa-moon';
|
||||
themeIcons.append(sunIcon, moonIcon);
|
||||
themeBtn.append(themeIcons);
|
||||
|
||||
// AUTO toggle (follow OS preference)
|
||||
const autoBtn = document.createElement('button');
|
||||
autoBtn.type = 'button';
|
||||
autoBtn.className = 'mokogit-fab-btn mokogit-theme-auto';
|
||||
autoBtn.id = 'mokogit-theme-auto';
|
||||
autoBtn.setAttribute('aria-label', 'Match system theme (auto)');
|
||||
autoBtn.setAttribute('aria-pressed', 'false');
|
||||
autoBtn.textContent = 'A';
|
||||
|
||||
const sync = () => {
|
||||
themeBtn.classList.toggle('is-dark', currentIsDark());
|
||||
const auto = currentIsAuto();
|
||||
autoBtn.classList.toggle('is-on', auto);
|
||||
autoBtn.setAttribute('aria-pressed', String(auto));
|
||||
// when auto is active the explicit light/dark toggle is de-emphasized
|
||||
themeBtn.classList.toggle('is-muted', auto);
|
||||
};
|
||||
sync();
|
||||
|
||||
themeBtn.addEventListener('click', () => {
|
||||
if (persistTheme(currentIsDark() ? LIGHT : DARK)) return;
|
||||
sync();
|
||||
});
|
||||
|
||||
autoBtn.addEventListener('click', () => {
|
||||
// toggle auto: if already auto, drop back to an explicit choice matching the
|
||||
// resolved appearance; otherwise switch to auto.
|
||||
const target = currentIsAuto() ? (currentIsDark() ? DARK : LIGHT) : AUTO;
|
||||
if (persistTheme(target)) return;
|
||||
sync();
|
||||
});
|
||||
|
||||
wrap.append(themeBtn, autoBtn);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
+6
-4
@@ -1,5 +1,7 @@
|
||||
import '../fomantic/build/fomantic.js';
|
||||
import '../css/index.css';
|
||||
// MokoGIT: vendored Bootstrap 5.3.3 JS bundle (incl. Popper; registers the data-bs-* API).
|
||||
import './vendor/bootstrap/bootstrap.bundle.min.js';
|
||||
|
||||
import {initDashboardRepoList} from './features/dashboard.ts';
|
||||
import {initGlobalCopyToClipboardListener} from './features/clipboard.ts';
|
||||
@@ -39,8 +41,8 @@ import {initInstall} from './features/install.ts';
|
||||
import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts';
|
||||
import {initRepoBranchButton} from './features/repo-branch.ts';
|
||||
import {initCommonOrganization} from './features/common-organization.ts';
|
||||
import {initThemeToggle} from './features/theme-toggle.ts';
|
||||
import {initA11yMenu} from './features/a11y-menu.ts';
|
||||
import {initMokogitFab} from './features/mokogit-fab.ts';
|
||||
import {initFlashToast} from './features/flash-toast.ts';
|
||||
import {initRepoWikiForm} from './features/repo-wiki.ts';
|
||||
import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts';
|
||||
import {initCopyContent} from './features/copycontent.ts';
|
||||
@@ -166,8 +168,8 @@ const initPerformanceTracer = callInitFunctions([
|
||||
|
||||
initDevtest,
|
||||
|
||||
initThemeToggle,
|
||||
initA11yMenu,
|
||||
initMokogitFab,
|
||||
initFlashToast,
|
||||
]);
|
||||
|
||||
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
|
||||
|
||||
@@ -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<HTMLElement>('.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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
export type IntervalId = ReturnType<typeof setInterval>;
|
||||
|
||||
export type Intent = 'error' | 'warning' | 'info';
|
||||
export type Intent = 'error' | 'warning' | 'info' | 'success';
|
||||
|
||||
export type Mention = {
|
||||
key: string,
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
Bootstrap v5.3.3 (MIT). Full license text: web_src/css/vendor/bootstrap/LICENSE
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user