Compare commits

..

1 Commits

54 changed files with 236 additions and 1472 deletions
-8
View File
@@ -7,13 +7,5 @@
/options/fileicon/** linguist-generated
/vendor/** -text -eol linguist-vendored
/web_src/js/vendor/** -text -eol linguist-vendored
/web_src/css/vendor/** -text -eol linguist-vendored
# Binary web fonts must never be eol-normalized (the default `* text=auto eol=lf`
# otherwise corrupts them on cross-platform checkout -> unparseable font -> tofu icons).
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.otf binary
Dockerfile.* linguist-language=Dockerfile
Makefile.* linguist-language=Makefile
@@ -0,0 +1,83 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGIT.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogit/workflows/workflow-sync-trigger.yml
# VERSION: 01.01.01
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
name: "Universal: Workflow Sync Trigger"
on:
workflow_dispatch:
pull_request:
types: [closed]
branches:
- main
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync:
name: Sync workflows to live repos
runs-on: release
if: >-
startsWith(github.event.repository.name, 'Template-') &&
(github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]')))
steps:
- name: Determine platform from repo name
id: platform
run: |
REPO="${{ github.event.repository.name }}"
case "$REPO" in
Template-Joomla) PLATFORM="joomla" ;;
Template-Dolibarr) PLATFORM="dolibarr" ;;
Template-Go) PLATFORM="go" ;;
Template-NPM) PLATFORM="npm" ;;
Template-Generic) PLATFORM="" ;;
*) PLATFORM="" ;;
esac
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform: ${PLATFORM:-all}"
- name: Clone mokocli
env:
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
run: |
MOKOGIT_URL="${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install PHP
run: |
if ! command -v php &> /dev/null; then
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
fi
- name: Install dependencies
run: |
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- name: Run workflow sync
env:
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
run: |
ARGS="--token ${MOKOGIT_TOKEN}"
ARGS="${ARGS} --org ${{ vars.MOKOGIT_ORG || github.repository_owner }}"
ARGS="${ARGS} --phase repos"
ARGS="${ARGS} --delete-orphans"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ -n "$PLATFORM" ]; then
ARGS="${ARGS} --platform-filter ${PLATFORM}"
fi
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
@@ -1,72 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Keeps the bundled MokoOnyx theme (public/assets/vendor/mokoonyx) up to date from the
# canonical source MokoConsulting/MokoOnyx. Lives in .mokogit/workflows/custom/ so the
# Template-Go workflow sync never overwrites it.
name: Sync MokoOnyx theme
on:
workflow_dispatch: {}
schedule:
- cron: '17 6 * * *' # daily 06:17 UTC
push:
branches: [dev, main]
paths:
- '.mokogit/workflows/custom/sync-mokoonyx-theme.yml'
concurrency:
group: sync-mokoonyx-${{ github.ref }}
cancel-in-progress: true
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout MokoGIT
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGIT_TOKEN }}
- name: Fetch MokoOnyx source
run: |
set -euo pipefail
git clone --depth 1 \
"https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git" \
"$RUNNER_TEMP/mokoonyx"
- name: Copy bundled theme files
run: |
set -euo pipefail
SRC="$RUNNER_TEMP/mokoonyx/source"
DST_PUB="public/assets/vendor/mokoonyx"
DST_REF="web_src/css/vendor/mokoonyx"
mkdir -p "$DST_PUB" "$DST_REF"
# Bundled (served) files
cp "$SRC/media/css/template.css" "$DST_PUB/template.css"
cp "$SRC/media/css/theme/light.standard.css" "$DST_PUB/light.standard.css"
cp "$SRC/media/css/theme/dark.standard.css" "$DST_PUB/dark.standard.css"
# Reference sources kept in web_src (token-parity fallback / audit)
cp "$SRC/templates/light.custom.css" "$DST_REF/light.custom.css"
cp "$SRC/templates/dark.custom.css" "$DST_REF/dark.custom.css"
# Regenerate tokens-only extracts (:root[data-bs-theme=...] -> :root), Bootstrap classes stripped
for m in light dark; do
awk '
/:root\[data-bs-theme=/ {inblk=1; print "/* Auto-extracted from vendor/mokoonyx/'"$m"'.custom.css :root[data-bs-theme] — re-scoped to :root. DO NOT hand-edit. */"; print ":root {"; next}
inblk && /^\}/ {inblk=0; print "}"; next}
inblk {print}
' "$DST_REF/$m.custom.css" > "$DST_REF/$m.tokens.css" || true
done
- name: Commit if changed
run: |
set -euo pipefail
git config user.name "git-actions[bot]"
git config user.email "git-actions[bot]@mokoconsulting.tech"
if git diff --quiet -- public/assets/vendor/mokoonyx web_src/css/vendor/mokoonyx; then
echo "MokoOnyx theme already up to date."
exit 0
fi
git add public/assets/vendor/mokoonyx web_src/css/vendor/mokoonyx
git commit -m "chore(theme): sync MokoOnyx template + themes from MokoConsulting/MokoOnyx [skip ci]"
git push
-11
View File
@@ -32,17 +32,6 @@ var wellKnownMimeTypesLower = sync.OnceValue(func() map[string]string {
// well, there are some types missing from the builtin list
".txt": "text/plain; charset=utf-8",
// MokoGIT #818: web fonts. Without a well-known type, .woff2 (etc.) falls through to
// the sniff path in http.ServeContent, whose embedded-asset ReadSeeker fails to rewind
// after the 512-byte content-type sniff — dropping the first 512 bytes of every served
// font and rendering all Font Awesome / icon glyphs as tofu. Setting the Content-Type by
// extension here means ServeContent never sniffs, so the font is served byte-exact.
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".otf": "font/otf",
".eot": "application/vnd.ms-fontobject",
}
})
-21
View File
@@ -1,21 +0,0 @@
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.
-1
View File
@@ -1 +0,0 @@
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -57,7 +57,7 @@ func LicenseTierCreate(ctx *context.Context) {
if tierKey == "" || tierName == "" {
ctx.Flash.Error("Tier key and name are required")
ctx.Redirect("/-/admin/license-tiers")
ctx.Redirect("/admin/license-tiers")
return
}
@@ -75,7 +75,7 @@ func LicenseTierCreate(ctx *context.Context) {
} else {
ctx.Flash.Success("Tier '" + tierName + "' created")
}
ctx.Redirect("/-/admin/license-tiers")
ctx.Redirect("/admin/license-tiers")
}
// LicenseTierUpdate handles POST to update a tier.
@@ -101,7 +101,7 @@ func LicenseTierUpdate(ctx *context.Context) {
} else {
ctx.Flash.Success("Tier '" + tier.TierName + "' updated")
}
ctx.Redirect("/-/admin/license-tiers")
ctx.Redirect("/admin/license-tiers")
}
// LicenseTierDelete handles POST to delete a tier.
@@ -118,11 +118,11 @@ func LicenseTierDelete(ctx *context.Context) {
count, _ := db.GetEngine(ctx).Where("tier = ?", tier.TierKey).Count(new(licensing_model.License))
if count > 0 {
ctx.Flash.Error("Cannot delete tier with active licenses. Reassign licenses first.")
ctx.Redirect("/-/admin/license-tiers")
ctx.Redirect("/admin/license-tiers")
return
}
db.GetEngine(ctx).ID(id).Delete(new(licensing_model.ProductTier))
ctx.Flash.Success("Tier '" + tier.TierName + "' deleted")
ctx.Redirect("/-/admin/license-tiers")
ctx.Redirect("/admin/license-tiers")
}
+1 -1
View File
@@ -6,7 +6,7 @@
<a class="ui primary tiny button" href="{{AppSubUrl}}/-/admin/auths/new">{{ctx.Locale.Tr "admin.auths.new"}}</a>
</div>
</h4>
<div class="ui attached table segment table-responsive">
<div class="ui attached table segment">
<table class="ui very basic table unstackable">
<thead>
<tr>
+1 -1
View File
@@ -29,7 +29,7 @@
</div>
</form>
</div>
<div class="ui attached table segment table-responsive">
<div class="ui attached table segment">
<table class="ui very basic striped table unstackable">
<thead>
<tr>
-2
View File
@@ -5,7 +5,6 @@
</h4>
<div class="ui attached table segment">
<form method="post" action="{{AppSubUrl}}/-/admin">
<div class="table-responsive">
<table class="ui very basic table unstackable tw-mb-0">
<thead>
<tr>
@@ -32,7 +31,6 @@
{{end}}
</tbody>
</table>
</div>
<input type="hidden" name="from" value="monitor">
</form>
</div>
+1 -1
View File
@@ -23,7 +23,7 @@
</div>
</div>
</div>
<div class="ui attached table segment table-responsive">
<div class="ui attached table segment">
<table class="ui very basic table unstackable">
<thead>
<tr>
+1 -1
View File
@@ -23,7 +23,7 @@
<tbody>
{{range .Tiers}}
<tr>
<td>{{.TierKey}}</td>
<td><code>{{.TierKey}}</code></td>
<td>{{.TierName}}</td>
<td>
{{range .Repos}}
-6
View File
@@ -1,9 +1,4 @@
<div class="flex-container-nav">
{{/* MokoGIT mobile: Bootstrap collapse trigger shown only < md (768px); the vertical menu below is a collapse drawer on mobile and always-visible (d-md-block) on desktop. */}}
<button class="ui fluid button d-md-none mg-settings-nav-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#mg-settings-nav" aria-expanded="false" aria-controls="mg-settings-nav">
{{svg "octicon-gear"}} {{ctx.Locale.Tr "admin.settings"}} {{svg "octicon-chevron-down"}}
</button>
<div class="collapse d-md-block mg-settings-nav-collapse" id="mg-settings-nav">
<div class="ui fluid vertical menu" style="text-align: left !important;">
<div class="header item">{{ctx.Locale.Tr "admin.settings"}}</div>
@@ -130,5 +125,4 @@
</div>
</details>
</div>
</div>
</div>
-2
View File
@@ -3,7 +3,6 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.notices.system_notice_list"}} ({{ctx.Locale.Tr "admin.total" .Total}})
</h4>
<div class="table-responsive">
<table class="ui attached segment select selectable table unstackable g-table-auto-ellipsis">
<thead>
<tr>
@@ -59,7 +58,6 @@
</tfoot>
{{end}}
</table>
</div>
{{template "base/paginate" .}}
</div>
+1 -1
View File
@@ -28,7 +28,7 @@
</div>
</div>
</div>
<div class="ui attached table segment table-responsive">
<div class="ui attached table segment">
<table class="ui very basic table unstackable">
<thead>
<tr>
+1 -1
View File
@@ -25,7 +25,7 @@
</div>
</form>
</div>
<div class="ui attached table segment table-responsive">
<div class="ui attached table segment">
<table class="ui very basic table unstackable">
<thead>
<tr>
+1 -1
View File
@@ -3,7 +3,7 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.monitor.queues"}}
</h4>
<div class="ui attached table segment table-responsive">
<div class="ui attached table segment">
<table class="ui very basic table unstackable">
<thead>
<tr>
+1 -1
View File
@@ -9,7 +9,7 @@
<div class="ui attached segment">
{{template "shared/repo/search" .}}
</div>
<div class="ui attached table segment table-responsive">
<div class="ui attached table segment">
<table class="ui very basic table selectable unstackable">
<thead>
<tr>
+1 -1
View File
@@ -3,7 +3,7 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.dashboard.statistic"}}
</h4>
<div class="ui attached table segment table-responsive">
<div class="ui attached table segment">
<table class="ui very basic table unstackable">
{{range $statsKey := .StatsKeys}}
<tr>
+1 -1
View File
@@ -55,7 +55,7 @@
</div>
</form>
</div>
<div class="ui attached table segment table-responsive">
<div class="ui attached table segment">
<table class="ui very basic selectable table unstackable">
<thead>
<tr>
+4 -5
View File
@@ -1,7 +1,5 @@
<footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
{{/* empty spacer keeps the single link group right-aligned via the footer's space-between */}}
<div class="left-links" aria-hidden="true"></div>
<div class="right-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.links"}}">
<div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}">
{{if ShowFooterPoweredBy}}
<a target="_blank" href="https://code.mokoconsulting.tech/MokoConsulting/MokoGIT">{{ctx.Locale.Tr "powered_by" AppName}}</a>
{{end}}
@@ -15,14 +13,15 @@
{{end}}
</span>
{{end}}
{{/* Template / page load-time info is admin-only. */}}
{{if and .IsAdmin .TemplateLoadTimes ShowFooterTemplateLoadTime}}
{{if and .TemplateLoadTimes ShowFooterTemplateLoadTime}}
<span>
{{ctx.Locale.Tr "page"}}: <strong>{{LoadTimes .PageStartTime}}</strong>
{{ctx.Locale.Tr "template"}}{{if .TemplateName}} {{.TemplateName}}{{end}}: <strong>{{call .TemplateLoadTimes}}</strong>
</span>
{{end}}
{{if $.ViteModeIsDev}}<span class="ui basic label primary">ViteDevMode</span>{{end}}
</div>
<div class="right-links" role="group" aria-label="{{ctx.Locale.Tr "aria.footer.links"}}">
<div class="ui dropdown custom" id="footer-theme-selector">
<span class="default-text">{{ctx.RenderUtils.RenderThemeItem ctx.CurrentWebTheme 16}}</span>
<div class="menu theme-menu"></div>
+1 -36
View File
@@ -3,29 +3,6 @@
<head>
{{ctx.HeadMetaContentSecurityPolicy}}
<meta name="viewport" content="width=device-width, initial-scale=1">
{{/* MokoGIT: seed data-bs-theme + the MokoOnyx FAB's localStorage state from the Gitea theme
(pre-paint, no flash), and bridge the MokoOnyx FAB's theme toggle back to Gitea's theme
system (persist via the hidden form + reload so Fomantic + MokoOnyx stay in sync). */}}
<script nonce="{{ctx.CspScriptNonce}}">(function(){try{
var html=document.documentElement;
var t="{{ctx.CurrentWebTheme.InternalName}}";
var mode=t.indexOf("dark")>=0?"dark":t.indexOf("light")>=0?"light":"auto";
var resolved=mode==="auto"?(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):mode;
html.setAttribute("data-bs-theme",resolved);
html.setAttribute("data-aria-theme",resolved);
try{localStorage.setItem("theme",mode);}catch(e){}
var first=true;
new MutationObserver(function(){
if(first){first=false;return;}
var m; try{m=localStorage.getItem("theme");}catch(e){}
if(m!=="light"&&m!=="dark"&&m!=="auto"){m=html.getAttribute("data-bs-theme")==="dark"?"dark":"light";}
var gt="mokogit-"+m;
if(gt===t)return;
var f=document.getElementById("mokogit-theme-form");
if(f){var i=f.querySelector('[name=theme]');if(i){i.value=gt;f.submit();return;}}
html.setAttribute("data-theme",gt);
}).observe(html,{attributes:true,attributeFilter:["data-bs-theme"]});
}catch(e){document.documentElement.setAttribute("data-bs-theme","light");}})();</script>
<title>{{if .Title}}{{.Title}} - {{end}}{{.PageTitleCommon}}</title>
<link rel="manifest" href="{{AssetUrlPrefix}}/site-manifest.json">
<meta name="theme-color" content="#112855">{{/* MokoGIT navy installed PWA / mobile browser chrome */}}
@@ -48,19 +25,7 @@
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
{{template "base/head_opengraph" .}}
{{/* MokoGIT: MokoOnyx palette/theme sheets + FA + Bootstrap load BEFORE the Fomantic bundle so
Fomantic (Gitea's contrast-correct dark theme, tuned to the MokoOnyx palette) wins on components
— eliminating light-on-light collisions — while MokoOnyx still supplies the tokens/palette + the
FAB. data-bs-theme (set above) picks light/dark. Synced from MokoConsulting/MokoOnyx by
.mokogit/workflows/custom/sync-mokoonyx-theme.yml. */}}
<link rel="stylesheet" href="{{AssetUrlPrefix}}/vendor/fontawesome/css/all.min.css">
<link rel="stylesheet" href="{{AssetUrlPrefix}}/vendor/mokoonyx/template.css">
<link rel="stylesheet" href="{{AssetUrlPrefix}}/vendor/mokoonyx/light.standard.css">
<link rel="stylesheet" href="{{AssetUrlPrefix}}/vendor/mokoonyx/dark.standard.css">
<script src="{{AssetUrlPrefix}}/vendor/bootstrap/bootstrap.bundle.min.js" defer></script>
{{template "base/head_style" .}}
{{/* MokoOnyx FAB + a11y toolbar (exact widget); enabled via the body data-* attrs; theme toggle bridged above. */}}
<script src="{{AssetUrlPrefix}}/vendor/mokoonyx/template.min.js" defer></script>
{{template "base/head_script" .}}
{{template "custom/header" .}}
<script nonce="{{ctx.CspScriptNonce}}">
@@ -71,7 +36,7 @@
}
</script>
</head>
<body data-bs-no-jquery data-theme-fab-enabled="1" data-theme-fab-pos="bl" data-a11y-toolbar="1" data-a11y-resize="1" data-a11y-invert="1" data-a11y-contrast="1" data-a11y-links="1" data-a11y-font="1" data-a11y-animations="1" data-a11y-pos="bl">
<body>
{{template "custom/body_outer_pre" .}}
<div class="full height">
+25 -6
View File
@@ -2,7 +2,7 @@
<div class="navbar-left">
<!-- the logo -->
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/favicon.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true" onerror="this.src='{{AssetUrlPrefix}}/img/favicon.png'">
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
</a>
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
@@ -40,11 +40,30 @@
<!-- the full dropdown menus -->
<div class="navbar-right">
{{/* 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. */}}
{{/* 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>
{{if .IsSigned}}
<form id="mokogit-theme-form" method="post" action="{{AppSubUrl}}/user/settings/appearance/theme" class="tw-hidden">
{{.CsrfTokenHtml}}
-6
View File
@@ -1,9 +1,4 @@
<div class="flex-container-nav">
{{/* MokoGIT mobile: Bootstrap collapse trigger shown only < md (768px); the vertical menu below is a collapse drawer on mobile and always-visible (d-md-block) on desktop. */}}
<button class="ui fluid button d-md-none mg-settings-nav-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#mg-settings-nav" aria-expanded="false" aria-controls="mg-settings-nav">
{{svg "octicon-gear"}} {{ctx.Locale.Tr "org.settings"}} {{svg "octicon-chevron-down"}}
</button>
<div class="collapse d-md-block mg-settings-nav-collapse" id="mg-settings-nav">
<div class="ui fluid vertical menu">
<div class="header item">{{svg "octicon-gear"}} {{ctx.Locale.Tr "org.settings"}}</div>
<a class="{{if .PageIsSettingsOptions}}active {{end}}item" href="{{.OrgLink}}/settings">
@@ -65,5 +60,4 @@
</details>
{{end}}
</div>
</div>
</div>
+2 -2
View File
@@ -82,7 +82,7 @@
<div class="ui container">
<overflow-menu class="ui secondary pointing menu">
{{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}}
<div class="overflow-menu-items overflow-auto flex-nowrap">
<div class="overflow-menu-items">
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
<a class="{{if .PageIsViewCode}}active {{end}}item" href="{{.RepoLink}}{{if and (ne .BranchName .Repository.DefaultBranch) (not $.PageIsWiki)}}/src/{{.RefTypeNameSubURL}}{{end}}">
{{svg "octicon-code"}} {{ctx.Locale.Tr "repo.code"}}
@@ -195,7 +195,7 @@
{{end}}
</div>
{{else}}
<div class="overflow-menu-items overflow-auto flex-nowrap">
<div class="overflow-menu-items">
{{if(and .Repository.IsBeingCreated (.Permission.CanRead ctx.Consts.RepoUnitTypeCode))}}
<a class="{{if not .PageIsRepoSettings}}active {{end}}item" href="{{.RepoLink}}">
{{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.migration_status"}}
-6
View File
@@ -1,9 +1,4 @@
<div class="flex-container-nav">
{{/* MokoGIT mobile: Bootstrap collapse trigger shown only < md (768px); the vertical menu below is a collapse drawer on mobile and always-visible (d-md-block) on desktop. */}}
<button class="ui fluid button d-md-none mg-settings-nav-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#mg-settings-nav" aria-expanded="false" aria-controls="mg-settings-nav">
{{svg "octicon-gear"}} {{ctx.Locale.Tr "repo.settings"}} {{svg "octicon-chevron-down"}}
</button>
<div class="collapse d-md-block mg-settings-nav-collapse" id="mg-settings-nav">
<div class="ui fluid vertical menu">
<div class="header item">{{svg "octicon-gear"}} {{ctx.Locale.Tr "repo.settings"}}</div>
<a class="{{if .PageIsSettingsOptions}}active {{end}}item" href="{{.RepoLink}}/settings">
@@ -77,5 +72,4 @@
</div>
</details>
</div>
</div>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
<div class="ui container fluid">
<div class="tw-text-center tw-mb-4">
<img src="{{AssetUrlPrefix}}/img/login-logo.png" alt="{{AppName}}" style="max-width: 220px; max-height: 80px; object-fit: contain;" onerror="if(!this.dataset.fb){this.dataset.fb=1;this.src='{{AssetUrlPrefix}}/img/favicon.svg';}else{this.style.display='none';}">
<img src="{{AssetUrlPrefix}}/img/login-logo.png" style="max-width: 220px; max-height: 80px; object-fit: contain;" onerror="this.style.display='none'">
</div>
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}}
{{template "base/alert" .}}
-6
View File
@@ -1,9 +1,4 @@
<div class="flex-container-nav">
{{/* MokoGIT mobile: Bootstrap collapse trigger shown only < md (768px); the vertical menu below is a collapse drawer on mobile and always-visible (d-md-block) on desktop. */}}
<button class="ui fluid button d-md-none mg-settings-nav-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#mg-settings-nav" aria-expanded="false" aria-controls="mg-settings-nav">
{{svg "octicon-gear"}} {{ctx.Locale.Tr "user.settings"}} {{svg "octicon-chevron-down"}}
</button>
<div class="collapse d-md-block mg-settings-nav-collapse" id="mg-settings-nav">
<div class="ui fluid vertical menu">
<div class="header item">{{svg "octicon-gear"}} {{ctx.Locale.Tr "user.settings"}}</div>
<a class="{{if .PageIsSettingsProfile}}active {{end}}item" href="{{AppSubUrl}}/user/settings">
@@ -74,5 +69,4 @@
{{svg "octicon-repo"}} {{ctx.Locale.Tr "settings.repos"}}
</a>
</div>
</div>
</div>
-167
View File
@@ -1,167 +0,0 @@
/* 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; }
}
+3 -5
View File
@@ -1,9 +1,9 @@
@import "./modules/normalize.css";
@import "./modules/animations.css";
/* Font Awesome + Bootstrap are served statically from public/assets/vendor and <link>ed in
templates/base/head.tmpl (NOT bundled here) Vite's file-emission corrupts binary web fonts,
so static byte-exact serving is required. See templates/base/head.tmpl. */
/* Vendored Font Awesome 7 Free (unmodified). Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT.
License + attribution: web_src/css/vendor/fontawesome/LICENSE.txt and THIRD-PARTY-NOTICES.md. */
@import "./vendor/fontawesome/css/all.min.css";
/* fomantic replacements */
@import "./modules/button.css";
@@ -95,7 +95,5 @@
/* 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;
-200
View File
@@ -112,203 +112,3 @@ 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);
}
/* MokoOnyx (Bootstrap template.css) × Fomantic collision fixes
template.css loads after the Fomantic bundle to drive the look, but Bootstrap's
generic classes clobber some Fomantic components. Restore the readable ones here. */
/* Bootstrap `.active` turned Fomantic's active menu item white-on-white (e.g. the
dashboard Repository/Organization switcher). Restore a readable selected state. */
.ui.menu .item.active,
.ui.two.item.menu .item.active,
.ui.tabular.menu .item.active,
.ui.secondary.pointing.menu .item.active {
background: var(--color-box-header) !important;
color: var(--color-text) !important;
}
/* Theme/a11y FAB = the login-card / navbar dark navy (#112855), not the accent blue.
MokoOnyx's FAB reads --theme-fab-bg (fallback --color-primary); pin it here. !important
so it wins over the later-loaded MokoOnyx sheets. */
:root {
--theme-fab-bg: #112855 !important;
--theme-fab-color: #ffffff !important;
--theme-fab-border: rgba(255, 255, 255, 0.28) !important;
--a11y-btn-bg: #112855 !important;
}
/* Contrast safety: MokoOnyx/Bootstrap whited out some Fomantic controls (light-on-light)
Force readable fg/bg pairs. If these keep multiplying, the systemic fix is loading MokoOnyx
BEFORE the Fomantic bundle (see head.tmpl) so Fomantic's contrast-correct theme wins. */
/* Top-right "Sign In" (and other navbar controls) were whited out on the navy navbar. */
#navbar .item,
#navbar a.item,
#navbar .ui.button,
.navbar-right .item {
background: transparent !important;
color: var(--color-nav-text, #ffffff) !important;
border-color: rgba(255, 255, 255, 0.35) !important;
}
#navbar .item:hover,
#navbar .ui.button:hover {
background: rgba(255, 255, 255, 0.10) !important;
}
/* "Sign in with a passkey" link — visible on the dark auth card. */
.signin-passkey {
color: #8ab4f8 !important;
font-weight: 600;
text-decoration: underline;
}
.signin-passkey:hover { color: #b6d0fb !important; }
/* a11y toggle inside the theme FAB must match the FAB's navy background (not the default
MokoOnyx accent blue), so the switcher reads as one solid navy pill. */
#mokoThemeFab .a11y-toggle,
#mokoThemeFab .a11y-toggle-inline,
.a11y-toggle-inline {
background: var(--theme-fab-bg, #112855) !important;
color: var(--theme-fab-color, #ffffff) !important;
border-color: var(--theme-fab-bg, #112855) !important;
}
-443
View File
@@ -1,443 +0,0 @@
/*!
* 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, ).
*
* BOOTSTRAP 5.3.3 (feat/mobile-retheme)
* Bootstrap 5.3.3 is vendored + loaded GLOBALLY and now powers the responsive
* PRIMITIVES on every page:
* Wide data tables scroll inside a `.table-responsive` wrapper (added in
* the templates) so the hand-rolled `.admin .ui.table { overflow-x }`
* rules that used to do this were REMOVED from this file.
* Settings sidebars (repo/user/org/admin navbar.tmpl) are a Bootstrap
* `.collapse` drawer on mobile, always-shown via `.d-md-block` on desktop,
* toggled by a `.d-md-none` "Menu" button (data-bs-toggle="collapse").
* This file only styles that trigger + drawer chrome and dark-mode polish.
* Repo sub-nav (header.tmpl) carries Bootstrap `.overflow-auto .flex-nowrap`
* for horizontal scroll; the momentum/scrollbar-hiding polish stays here.
* Display toggles use Bootstrap `.d-none/.d-md-block` utilities directly.
* This file now keeps ONLY the Gitea-selector-specific fixes Bootstrap cannot
* reach: diff single-column collapse, issue-list column hiding, and 44px
* touch targets, plus small theme-var overrides so the Bootstrap components
* read correctly in mokogit-dark.
*
* 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) and is
NOT already wrapped by Bootstrap's `.table-responsive` (admin panels now
are see the templates), keep it scroll-contained inside its own wrapper
rather than widening the page. Bootstrap's `.table-responsive` handles the
admin/settings tables; this covers the remaining Gitea-owned ones. */
.ui.table.unstackable:not(.table-responsive),
#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 (768991px): admin/user-settings wide tables are now wrapped in
Bootstrap `.table-responsive` in their templates, which scroll-contains them
at every width so the previous hand-rolled `.admin table.ui.table` /
`.user.settings table.ui.table { overflow-x }` block was REMOVED here as
redundant. Nothing to add; Bootstrap owns this case. */
/* ==========================================================================
* SECTION 4 SETTINGS PAGES (repo/user/org/admin left sidebar)
* --------------------------------------------------------------------------
* Bootstrap-powered: each settings navbar.tmpl now renders a `.d-md-none`
* trigger button + a Bootstrap `.collapse.d-md-block` drawer wrapping the
* existing `.ui.fluid.vertical.menu`. Bootstrap's own CSS/JS drive the show/hide:
* - < md (768px): `.collapse` is display:none until the button toggles it,
* then the FULL Fomantic vertical menu drops down as a tap-friendly drawer.
* - >= md: `.d-md-block` forces the drawer visible and the `.d-md-none` button
* is hidden, so desktop keeps the untouched Fomantic vertical sidebar.
* flexcontainer.css already stacks .flex-container to a column below 768px and
* puts .flex-container-nav above the content (order:-1), so the drawer opens in
* the right place. This file only adds trigger chrome + FAB-safe spacing +
* dark-mode polish no display logic (Bootstrap owns that).
*
* Verified markup (templates/{repo,user,org,admin}/{settings,}/navbar.tmpl):
* .flex-container-nav
* > button.d-md-none.mg-settings-nav-toggle[data-bs-toggle=collapse]
* > .collapse.d-md-block.mg-settings-nav-collapse#mg-settings-nav
* > .ui.fluid.vertical.menu > .header.item | a.item | details.toggleable-item
* ========================================================================== */
@media (max-width: 767.98px) {
/* Trigger button: full-width, comfortable tap height, spaced from the menu it
reveals. The trailing chevron rotates when the drawer is open. */
.flex-container-nav .mg-settings-nav-toggle {
min-height: var(--mg-touch-target);
justify-content: space-between;
margin-bottom: 8px;
}
.flex-container-nav .mg-settings-nav-toggle .svg.octicon-chevron-down {
transition: transform 0.15s ease;
}
.flex-container-nav .mg-settings-nav-toggle[aria-expanded="true"] .svg.octicon-chevron-down {
transform: rotate(180deg);
}
/* The drawer is a normal full-width stacked vertical menu (the Fomantic
default). Give it a little breathing room from the page content and make
each item a 44px tap target. The redundant header.item is dropped since the
trigger button already names the section. */
.flex-container-nav .mg-settings-nav-collapse .ui.vertical.menu {
width: 100% !important;
margin-top: 0;
}
.flex-container-nav .mg-settings-nav-collapse .ui.vertical.menu > .header.item {
display: none;
}
.flex-container-nav .mg-settings-nav-collapse .ui.vertical.menu > .item,
.flex-container-nav .mg-settings-nav-collapse .ui.vertical.menu .menu > .item {
min-height: var(--mg-touch-target);
display: flex;
align-items: center;
}
/* Keep the whole drawer clear of the fixed FAB (bottom-right) so the last
item is never hidden behind it. */
.flex-container-nav .mg-settings-nav-collapse {
margin-bottom: 72px;
}
}
/* ==========================================================================
* 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);
}
}
/* Footer: stack + center the two link groups on mobile so they don't misalign/overflow. */
@media (max-width: 767.98px) {
.page-footer {
flex-direction: column;
align-items: center;
gap: 0.4rem;
text-align: center;
}
.page-footer .left-links,
.page-footer .right-links {
justify-content: center;
flex-wrap: wrap;
gap: 0.25rem 0.75rem;
}
}
/* Settings navs (user/org/repo/admin) collapse ONLY on mobile. On desktop the menu is always
expanded and the toggle is hidden regardless of Bootstrap's .collapse default (display:none). */
@media (min-width: 768px) {
.mg-settings-nav-collapse {
display: block !important;
height: auto !important;
overflow: visible !important;
visibility: visible !important;
}
.mg-settings-nav-toggle { display: none !important; }
}
+22 -25
View File
@@ -213,12 +213,12 @@ gitea-theme-meta-info {
--color-box-header: #151b22;
--color-box-body: #10151b;
--color-box-body-highlight: #151b22;
--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-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-footer: var(--color-nav-bg);
--color-timeline: #383b40;
--color-input-text: var(--color-text-dark);
@@ -260,9 +260,8 @@ gitea-theme-meta-info {
--color-label-bg: #7a7f8a4b;
--color-label-hover-bg: #7a7f8aa0;
--color-label-active-bg: #7a7f8aff;
/* Links/accents on dark: mokoonyx readable light-blue (#60a5fa). */
--color-accent: #60a5fa;
--color-small-accent: var(--color-primary-light-4);
--color-accent: var(--color-primary-light-1);
--color-small-accent: var(--color-primary-light-5);
--color-highlight-fg: #87651e;
--color-highlight-bg: #443a27;
--color-overlay-backdrop: #080808c0;
@@ -311,22 +310,20 @@ gitea-theme-meta-info {
--color-syntax-traceback: #ff8686;
--color-syntax-matching-bracket-bg: #00918a48;
--color-syntax-nonmatching-bracket-bg: #cc484848;
/* ===== 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 */
/* ===== 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 */
accent-color: var(--color-accent);
color-scheme: dark;
}
+45 -53
View File
@@ -5,38 +5,34 @@ gitea-theme-meta-info {
:root {
--is-dark-theme: false;
/* 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);
/* 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);
--color-secondary: #d0d7de;
--color-secondary-dark-1: #c7ced5;
--color-secondary-dark-2: #b9c0c7;
@@ -217,12 +213,12 @@ gitea-theme-meta-info {
--color-box-header: #f1f3f5;
--color-box-body: #ffffff;
--color-box-body-highlight: #ecf5fd;
--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-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-footer: var(--color-nav-bg);
--color-timeline: #d0d7de;
--color-input-text: var(--color-text-dark);
@@ -264,10 +260,8 @@ gitea-theme-meta-info {
--color-label-bg: #949da64b;
--color-label-hover-bg: #949da6a0;
--color-label-active-bg: #949da6ff;
/* 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-accent: var(--color-primary-light-1);
--color-small-accent: var(--color-primary-light-6);
--color-highlight-fg: #eed200;
--color-highlight-bg: #f5efc5;
--color-overlay-backdrop: #080808c0;
@@ -316,20 +310,18 @@ gitea-theme-meta-info {
--color-syntax-traceback: #c00000;
--color-syntax-matching-bracket-bg: #00b5ad38;
--color-syntax-nonmatching-bracket-bg: #db282838;
/* ===== 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 */
/* ===== 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 */
--color-card: #ffffff;
--color-menu: #ffffff;
--color-button: #ffffff;
--color-timeline: #dfe3e7;
--color-input-border: #dfe3e7; /* mokoonyx gray-300 border */
--color-light-border: #dfe3e7; /* solid gray-300 border definition */
--color-light-border: #0000172e; /* stronger gray-300 border definition */
accent-color: var(--color-accent);
color-scheme: light;
}
+13 -37
View File
@@ -137,54 +137,37 @@ function buildPanel(btn: HTMLElement, state: A11yState): HTMLElement {
return panel;
}
// 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} {
export function initA11yMenu(): void {
const btn = document.querySelector<HTMLButtonElement>('#mokogit-a11y-toggle');
if (!btn) return;
const state = load();
apply(state);
const panel = buildPanel(btn, state);
let 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();
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';
panel.style.top = `${Math.round(r.bottom + 6)}px`;
panel.style.right = `${Math.round(window.innerWidth - r.right)}px`;
};
const isOpen = () => !getPanel().hidden;
const open = () => {
panel = document.querySelector('#mokogit-a11y-panel') as HTMLElement;
positionPanel();
getPanel().hidden = false;
panel.hidden = false;
btn.setAttribute('aria-expanded', 'true');
document.addEventListener('click', onDocClick, true);
document.addEventListener('keydown', onKey, true);
};
const close = () => {
getPanel().hidden = true;
(document.querySelector('#mokogit-a11y-panel') as HTMLElement).hidden = true;
btn.setAttribute('aria-expanded', 'false');
document.removeEventListener('click', onDocClick, true);
document.removeEventListener('keydown', onKey, true);
};
const onDocClick = (e: MouseEvent) => {
const p = getPanel();
const p = document.querySelector('#mokogit-a11y-panel') as HTMLElement;
if (p.contains(e.target as Node) || btn.contains(e.target as Node)) return;
close();
};
@@ -194,14 +177,7 @@ export function mountA11yToggle(
btn.addEventListener('click', (e) => {
e.preventDefault();
isOpen() ? close() : open();
const p = document.querySelector('#mokogit-a11y-panel') as HTMLElement;
p.hidden ? open() : close();
});
return {open, close, isOpen};
}
export function initA11yMenu(): void {
const btn = document.querySelector<HTMLButtonElement>('#mokogit-a11y-toggle');
if (!btn) return;
mountA11yToggle(btn);
}
-62
View File
@@ -1,62 +0,0 @@
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);
}
}
-142
View File
@@ -1,142 +0,0 @@
// 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);
}
}
}
+15 -95
View File
@@ -1,57 +1,22 @@
// MokoGIT theme toggle.
// MokoGIT navbar light/dark 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 = rawTheme();
const t = document.documentElement.getAttribute('data-theme') || '';
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;
@@ -60,64 +25,19 @@ export function initThemeToggle(): void {
sync();
btn.addEventListener('click', () => {
if (persistTheme(currentIsDark() ? LIGHT : DARK)) return;
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 {}
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;
}
+4 -4
View File
@@ -1,7 +1,5 @@
import '../fomantic/build/fomantic.js';
import '../css/index.css';
// Bootstrap JS is loaded statically via <script> in templates/base/head.tmpl (see there), not
// bundled here. `<body data-bs-no-jquery>` stops it clobbering Fomantic's jQuery plugins.
import {initDashboardRepoList} from './features/dashboard.ts';
import {initGlobalCopyToClipboardListener} from './features/clipboard.ts';
@@ -41,7 +39,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 {initFlashToast} from './features/flash-toast.ts';
import {initThemeToggle} from './features/theme-toggle.ts';
import {initA11yMenu} from './features/a11y-menu.ts';
import {initRepoWikiForm} from './features/repo-wiki.ts';
import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts';
import {initCopyContent} from './features/copycontent.ts';
@@ -167,7 +166,8 @@ const initPerformanceTracer = callInitFunctions([
initDevtest,
initFlashToast,
initThemeToggle,
initA11yMenu,
]);
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
+2 -11
View File
@@ -1,19 +1,10 @@
import {showInfoToast, showSuccessToast, showErrorToast, showWarningToast} from './toast.ts';
import {showInfoToast, showErrorToast, showWarningToast} from './toast.ts';
test('showInfoToast', async () => {
showInfoToast('info 😀', {duration: -1});
showInfoToast('success 😀', {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();
-9
View File
@@ -19,11 +19,6 @@ 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,
@@ -93,10 +88,6 @@ 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
View File
@@ -1,6 +1,6 @@
export type IntervalId = ReturnType<typeof setInterval>;
export type Intent = 'error' | 'warning' | 'info' | 'success';
export type Intent = 'error' | 'warning' | 'info';
export type Mention = {
key: string,