Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
34f9ae6119
|
|||
| c2a8894eee | |||
|
99692b1bbb
|
|||
| dcc8b1e5ca | |||
|
20b8b77436
|
|||
| 5afbf005ed | |||
| 65019dd5ad | |||
| e8d6451f17 | |||
|
a3f92d3890
|
|||
| f5bf2321de | |||
| e0e5f2f9a7 | |||
|
49e7485acc
|
|||
| 8828c33e16 | |||
| cae7846c35 | |||
| c77eebfe27 | |||
| 4f825e7cff | |||
|
b20fe1d37c
|
|||
| 670d26408d | |||
|
ef0c22aecc
|
|||
|
c6e0a45bb4
|
|||
|
154c3f77e4
|
|||
| c72d872557 | |||
|
02b0377b1d
|
|||
|
f27c73195f
|
|||
|
c2b64de621
|
|||
| d7d6662804 | |||
|
2878699082
|
|||
| b551493110 | |||
|
24e6e2b4a1
|
|||
| 7f540b4510 | |||
| 92d7d5563b | |||
| 00f3d5d6d8 | |||
|
f6b43492e9
|
|||
| 457a53f987 |
@@ -7,5 +7,13 @@
|
||||
/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
|
||||
|
||||
@@ -144,3 +144,4 @@ Makefile.local
|
||||
# ============================================================
|
||||
wiki/
|
||||
docs/
|
||||
.claude/
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# INGROUP: MokoCLI.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogit/workflows/auto-release.yml
|
||||
# VERSION: 05.01.02
|
||||
# VERSION: 05.02.00
|
||||
# BRIEF: Universal build & release � detects platform from metadata API
|
||||
#
|
||||
# +=======================================================================+
|
||||
@@ -39,6 +39,12 @@ on:
|
||||
- '.gitattributes'
|
||||
- '.gitmessage'
|
||||
- 'LICENSE'
|
||||
# Daily safety-net: catch merges whose pull_request event never created a run
|
||||
# (e.g. this workflow file being re-synced from the template concurrently with the
|
||||
# merge). Off-round minute to avoid a fleet-wide spike. The safety-net job below
|
||||
# only dispatches a release when main actually has unreleased changes.
|
||||
schedule:
|
||||
- cron: '37 8 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
@@ -505,3 +511,51 @@ jobs:
|
||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# ── Scheduled safety-net ─────────────────────────────────────────────────────────
|
||||
# A merge to main normally fires the `release` job via the pull_request `closed`
|
||||
# event. If that event never creates a run (observed when this workflow file is
|
||||
# being re-synced from the template in the same window as the merge), the release
|
||||
# is silently skipped. This daily job self-heals that: if main's CHANGELOG
|
||||
# [Unreleased] section still has content — which the release job empties on a
|
||||
# successful promote — it dispatches a normal release. Dependency-free (awk only)
|
||||
# so it runs on any runner; never touches the release job's own conditions.
|
||||
scheduled-safety-net:
|
||||
name: Scheduled release safety-net
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'schedule' && !startsWith(github.event.repository.name, 'Template-')
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Dispatch release if main has unreleased changes
|
||||
env:
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
run: |
|
||||
# [Unreleased] is non-empty precisely when changes were merged but not yet
|
||||
# released (the release job promotes it to a version and empties it).
|
||||
NOTES=""
|
||||
if [ -f CHANGELOG.md ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{if(f)exit} f{print}' CHANGELOG.md)
|
||||
fi
|
||||
if [ -z "$(printf '%s' "$NOTES" | tr -d '[:space:]')" ]; then
|
||||
echo "No unreleased changes on main — safety-net has nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
echo "Unreleased changes detected on main — a merge release was likely missed."
|
||||
echo "Dispatching a release via the safety-net."
|
||||
HTTP=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
||||
-H "Authorization: token ${MOKOGIT_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"ref":"main","inputs":{"action":"release"}}' \
|
||||
"${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}/actions/workflows/auto-release.yml/dispatches")
|
||||
if [ "$HTTP" = "204" ] || [ "$HTTP" = "201" ] || [ "$HTTP" = "200" ]; then
|
||||
echo "Release dispatched (HTTP $HTTP)."
|
||||
else
|
||||
echo "::warning::Safety-net release dispatch failed (HTTP $HTTP)"
|
||||
fi
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# BRIEF: Deploy to the Dev environment on push to the dev branch via the
|
||||
# restricted per-repo deploy pattern. This is a THIN trigger: it only
|
||||
# validates the tier+tag and hands "<tier> <tag>" to the forced-command
|
||||
# deploy-mokogit user over SSH (mokocli cli/deploy.php). The server-side
|
||||
# forced command sudo's to a root-owned deployer that builds, pushes,
|
||||
# recreates and health-checks — the runner never builds, holds a registry
|
||||
# token, or runs the health check. See `.vault system/deploy` and runbook
|
||||
# 16-restricted-deploy-pattern.
|
||||
# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3.
|
||||
# NOTE: deploy-*.yml are repo-managed (per-repo deploy user + secrets/vars)
|
||||
# and excluded from template sync — see mokocli workflow_sync PLATFORM_EXCLUDES['go'].
|
||||
# BRIEF: Deploy to the Dev environment on push to the dev branch. THIN TRIGGER —
|
||||
# the deploy LOGIC lives in mokocli (cli/deploy.php) and server-side in the
|
||||
# restricted per-repo deploy pattern (.vault system/deploy, runbook 16).
|
||||
# This workflow validates + invokes only; it carries NO registry token and
|
||||
# NO build script. A leaked DEPLOY_SSH_KEY can only redeploy this one repo.
|
||||
# OWNER: Template-Go (canonical; syncs to each repo's .mokogit/workflows).
|
||||
#
|
||||
# Required repo VARIABLES:
|
||||
# DEV_SSH_HOST, DEV_SSH_PORT, DEV_SSH_USERNAME - SSH deploy target for the dev tier
|
||||
# (DEV_SSH_USERNAME = deploy-mokogit, the forced-command deploy user)
|
||||
# Required SECRETS (already configured; reused, not re-set):
|
||||
# DEPLOY_SSH_KEY - deploy-mokogit private key (repo secret)
|
||||
# Required repo VARIABLES (tier-scoped): DEV_SSH_HOST, DEV_SSH_PORT, DEV_SSH_USERNAME.
|
||||
# Required SECRET: DEPLOY_SSH_KEY (the deploy-<repo> private key).
|
||||
# ONBOARDING: a repo joins the restricted deploy pattern when DEV_SSH_USERNAME is
|
||||
# set to deploy-<repo>. Un-onboarded go repos skip the job (guards below).
|
||||
|
||||
name: Deploy (Dev)
|
||||
|
||||
@@ -24,13 +18,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
# Manual trigger for isolated end-to-end tests.
|
||||
# Runs on the ref it is dispatched from.
|
||||
workflow_dispatch:
|
||||
|
||||
# No `concurrency:` block: it triggers a MokoGIT Actions run-creation bug that
|
||||
# silently drops deploys on rapid pushes to the branch. Do not re-add until the
|
||||
# upstream bug is confirmed fixed.
|
||||
# No `concurrency:` block: triggers a MokoGIT run-creation bug that drops deploys.
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
@@ -39,6 +29,7 @@ jobs:
|
||||
deploy-dev:
|
||||
name: "Deploy to Dev"
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ vars.DEV_SSH_USERNAME != '' }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v4
|
||||
@@ -60,29 +51,20 @@ jobs:
|
||||
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
|
||||
- name: Setup MokoCLI tools
|
||||
- name: Set up mokocli (deploy logic)
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
run: |
|
||||
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
||||
if [ -f /opt/mokocli/cli/deploy.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/mokocli
|
||||
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/mokocli
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||
fi
|
||||
command -v php >/dev/null || { sudo apt-get update -qq && sudo apt-get install -y -qq php-cli; }
|
||||
git clone --depth 1 "https://x-access-token:${MOKOGIT_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" "$RUNNER_TEMP/mokocli"
|
||||
echo "MOKO_CLI=$RUNNER_TEMP/mokocli/cli" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy (dev)
|
||||
- name: Deploy (mokocli triggers the restricted deploy pattern)
|
||||
run: |
|
||||
php ${MOKO_CLI}/deploy.php --tier dev --tag "${{ steps.config.outputs.tag }}" \
|
||||
--ssh-host ${{ vars.DEV_SSH_HOST }} --ssh-port ${{ vars.DEV_SSH_PORT }} \
|
||||
--ssh-user ${{ vars.DEV_SSH_USERNAME }} --ssh-key ~/.ssh/deploy_key
|
||||
if [ -z "${{ vars.DEV_SSH_USERNAME }}" ]; then
|
||||
echo "DEV_SSH_USERNAME unset — repo not onboarded to the restricted deploy pattern; skipping."
|
||||
exit 0
|
||||
fi
|
||||
php "$MOKO_CLI/deploy.php" --tier dev --tag "${{ steps.config.outputs.tag }}" \
|
||||
--ssh-host "${{ vars.DEV_SSH_HOST }}" --ssh-port "${{ vars.DEV_SSH_PORT }}" \
|
||||
--ssh-user "${{ vars.DEV_SSH_USERNAME }}" --ssh-key ~/.ssh/deploy_key
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# BRIEF: Deploy to the Prod environment on push to the main branch via the
|
||||
# restricted per-repo deploy pattern. This is a THIN trigger: it only
|
||||
# validates the tier+tag and hands "<tier> <tag>" to the forced-command
|
||||
# deploy-mokogit user over SSH (mokocli cli/deploy.php). The server-side
|
||||
# forced command sudo's to a root-owned deployer that builds, pushes,
|
||||
# recreates and health-checks — the runner never builds, holds a registry
|
||||
# token, or runs the health check. See `.vault system/deploy` and runbook
|
||||
# 16-restricted-deploy-pattern.
|
||||
# Prod deploys on merge to main (dev -> rc -> main pipeline).
|
||||
# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3.
|
||||
# NOTE: deploy-*.yml are repo-managed (per-repo deploy user + secrets/vars)
|
||||
# and excluded from template sync — see mokocli workflow_sync PLATFORM_EXCLUDES['go'].
|
||||
# BRIEF: Deploy to the Prod environment on push to the main branch. THIN TRIGGER —
|
||||
# the deploy LOGIC lives in mokocli (cli/deploy.php) and server-side in the
|
||||
# restricted per-repo deploy pattern (.vault system/deploy, runbook 16).
|
||||
# This workflow validates + invokes only; it carries NO registry token and
|
||||
# NO build script. A leaked DEPLOY_SSH_KEY can only redeploy this one repo.
|
||||
# OWNER: Template-Go (canonical; syncs to each repo's .mokogit/workflows).
|
||||
#
|
||||
# Required repo VARIABLES:
|
||||
# PROD_SSH_HOST, PROD_SSH_PORT, PROD_SSH_USERNAME - SSH deploy target for the prod tier
|
||||
# (PROD_SSH_USERNAME = deploy-mokogit, the forced-command deploy user)
|
||||
# Required SECRETS (already configured; reused, not re-set):
|
||||
# DEPLOY_SSH_KEY - deploy-mokogit private key (repo secret)
|
||||
# Required repo VARIABLES (tier-scoped): PROD_SSH_HOST, PROD_SSH_PORT, PROD_SSH_USERNAME.
|
||||
# Required SECRET: DEPLOY_SSH_KEY (the deploy-<repo> private key).
|
||||
# ONBOARDING: a repo joins the restricted deploy pattern when PROD_SSH_USERNAME is
|
||||
# set to deploy-<repo>. Un-onboarded go repos skip the job (guards below).
|
||||
|
||||
name: Deploy (Prod)
|
||||
|
||||
@@ -25,13 +18,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# Manual trigger for a prod re-deploy.
|
||||
# Runs on the ref it is dispatched from (use main).
|
||||
workflow_dispatch:
|
||||
|
||||
# No `concurrency:` block: it triggers a MokoGIT Actions run-creation bug that
|
||||
# silently drops deploys on rapid pushes to the branch. Do not re-add until the
|
||||
# upstream bug is confirmed fixed.
|
||||
# No `concurrency:` block: triggers a MokoGIT run-creation bug that drops deploys.
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
@@ -40,6 +29,7 @@ jobs:
|
||||
deploy-prod:
|
||||
name: "Deploy to Prod"
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ vars.PROD_SSH_USERNAME != '' }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v4
|
||||
@@ -61,29 +51,20 @@ jobs:
|
||||
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
|
||||
- name: Setup MokoCLI tools
|
||||
- name: Set up mokocli (deploy logic)
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
run: |
|
||||
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
||||
if [ -f /opt/mokocli/cli/deploy.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/mokocli
|
||||
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/mokocli
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||
fi
|
||||
command -v php >/dev/null || { sudo apt-get update -qq && sudo apt-get install -y -qq php-cli; }
|
||||
git clone --depth 1 "https://x-access-token:${MOKOGIT_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" "$RUNNER_TEMP/mokocli"
|
||||
echo "MOKO_CLI=$RUNNER_TEMP/mokocli/cli" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy (prod)
|
||||
- name: Deploy (mokocli triggers the restricted deploy pattern)
|
||||
run: |
|
||||
php ${MOKO_CLI}/deploy.php --tier prod --tag "${{ steps.config.outputs.tag }}" \
|
||||
--ssh-host ${{ vars.PROD_SSH_HOST }} --ssh-port ${{ vars.PROD_SSH_PORT }} \
|
||||
--ssh-user ${{ vars.PROD_SSH_USERNAME }} --ssh-key ~/.ssh/deploy_key
|
||||
if [ -z "${{ vars.PROD_SSH_USERNAME }}" ]; then
|
||||
echo "PROD_SSH_USERNAME unset — repo not onboarded to the restricted deploy pattern; skipping."
|
||||
exit 0
|
||||
fi
|
||||
php "$MOKO_CLI/deploy.php" --tier prod --tag "${{ steps.config.outputs.tag }}" \
|
||||
--ssh-host "${{ vars.PROD_SSH_HOST }}" --ssh-port "${{ vars.PROD_SSH_PORT }}" \
|
||||
--ssh-user "${{ vars.PROD_SSH_USERNAME }}" --ssh-key ~/.ssh/deploy_key
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# BRIEF: Deploy to the RC environment on push to the rc branch via the
|
||||
# restricted per-repo deploy pattern. This is a THIN trigger: it only
|
||||
# validates the tier+tag and hands "<tier> <tag>" to the forced-command
|
||||
# deploy-mokogit user over SSH (mokocli cli/deploy.php). The server-side
|
||||
# forced command sudo's to a root-owned deployer that builds, pushes,
|
||||
# recreates and health-checks — the runner never builds, holds a registry
|
||||
# token, or runs the health check. See `.vault system/deploy` and runbook
|
||||
# 16-restricted-deploy-pattern.
|
||||
# The rc branch is created by promote-rc when a PR to main opens.
|
||||
# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3.
|
||||
# NOTE: deploy-*.yml are repo-managed (per-repo deploy user + secrets/vars)
|
||||
# and excluded from template sync — see mokocli workflow_sync PLATFORM_EXCLUDES['go'].
|
||||
# BRIEF: Deploy to the RC environment on push to the rc branch. THIN TRIGGER —
|
||||
# the deploy LOGIC lives in mokocli (cli/deploy.php) and server-side in the
|
||||
# restricted per-repo deploy pattern (.vault system/deploy, runbook 16).
|
||||
# This workflow validates + invokes only; it carries NO registry token and
|
||||
# NO build script. A leaked DEPLOY_SSH_KEY can only redeploy this one repo.
|
||||
# OWNER: Template-Go (canonical; syncs to each repo's .mokogit/workflows).
|
||||
#
|
||||
# Required repo VARIABLES:
|
||||
# RC_SSH_HOST, RC_SSH_PORT, RC_SSH_USERNAME - SSH deploy target for the rc tier
|
||||
# (RC_SSH_USERNAME = deploy-mokogit, the forced-command deploy user)
|
||||
# Required SECRETS (already configured; reused, not re-set):
|
||||
# DEPLOY_SSH_KEY - deploy-mokogit private key (repo secret)
|
||||
# Required repo VARIABLES (tier-scoped): RC_SSH_HOST, RC_SSH_PORT, RC_SSH_USERNAME.
|
||||
# Required SECRET: DEPLOY_SSH_KEY (the deploy-<repo> private key).
|
||||
# ONBOARDING: a repo joins the restricted deploy pattern when RC_SSH_USERNAME is
|
||||
# set to deploy-<repo>. Un-onboarded go repos skip the job (guards below), so this
|
||||
# template is safe to cascade to every go repo.
|
||||
|
||||
name: Deploy (RC)
|
||||
|
||||
@@ -25,15 +19,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- rc
|
||||
# Manual trigger for isolated end-to-end tests without a full RC promotion.
|
||||
# Runs on the ref it is dispatched from; that ref must carry current source
|
||||
# (>= the RC database migration version) or the rebuilt image will refuse the
|
||||
# newer DB. Dispatch from `rc` once `rc` is current.
|
||||
workflow_dispatch:
|
||||
|
||||
# No `concurrency:` block: it triggers a MokoGIT Actions run-creation bug that
|
||||
# silently drops deploys on rapid pushes to the branch. Do not re-add until the
|
||||
# upstream bug is confirmed fixed.
|
||||
# No `concurrency:` block: triggers a MokoGIT run-creation bug that drops deploys.
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
@@ -42,6 +30,8 @@ jobs:
|
||||
deploy-rc:
|
||||
name: "Deploy to RC"
|
||||
runs-on: ubuntu-latest
|
||||
# Guard 1 (job-level): skip entirely unless onboarded.
|
||||
if: ${{ vars.RC_SSH_USERNAME != '' }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v4
|
||||
@@ -63,29 +53,21 @@ jobs:
|
||||
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
|
||||
- name: Setup MokoCLI tools
|
||||
- name: Set up mokocli (deploy logic)
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
run: |
|
||||
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
||||
if [ -f /opt/mokocli/cli/deploy.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/mokocli
|
||||
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/mokocli
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||
fi
|
||||
command -v php >/dev/null || { sudo apt-get update -qq && sudo apt-get install -y -qq php-cli; }
|
||||
git clone --depth 1 "https://x-access-token:${MOKOGIT_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" "$RUNNER_TEMP/mokocli"
|
||||
echo "MOKO_CLI=$RUNNER_TEMP/mokocli/cli" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy (rc)
|
||||
- name: Deploy (mokocli triggers the restricted deploy pattern)
|
||||
run: |
|
||||
php ${MOKO_CLI}/deploy.php --tier rc --tag "${{ steps.config.outputs.tag }}" \
|
||||
--ssh-host ${{ vars.RC_SSH_HOST }} --ssh-port ${{ vars.RC_SSH_PORT }} \
|
||||
--ssh-user ${{ vars.RC_SSH_USERNAME }} --ssh-key ~/.ssh/deploy_key
|
||||
# Guard 2 (step-level, reliable): no-op if not onboarded.
|
||||
if [ -z "${{ vars.RC_SSH_USERNAME }}" ]; then
|
||||
echo "RC_SSH_USERNAME unset — repo not onboarded to the restricted deploy pattern; skipping."
|
||||
exit 0
|
||||
fi
|
||||
php "$MOKO_CLI/deploy.php" --tier rc --tag "${{ steps.config.outputs.tag }}" \
|
||||
--ssh-host "${{ vars.RC_SSH_HOST }}" --ssh-port "${{ vars.RC_SSH_PORT }}" \
|
||||
--ssh-user "${{ vars.RC_SSH_USERNAME }}" --ssh-key ~/.ssh/deploy_key
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Update server (Joomla): serve the installable package, not the source archive** — `GenerateJoomlaXML` selected the first `.zip` attachment on a release, so releases carrying extra zips (e.g. `<repo>-<channel>-source.zip` ahead of `pkg_*.zip`) advertised the raw source archive; Joomla then downloaded a non-installable zip and failed with "Install path does not exist" (prerelease/dev channels). New `selectJoomlaArtifact()` prefers an extension-prefixed package (`pkg_`/`com_`/`mod_`/`plg_`/`tpl_`/`lib_`) and never a `-source` archive (hotfixed to prod)
|
||||
- **Actions: `.mokogit/workflows` custom-path detection restored** — `WorkflowDirs` now scans `.mokogit/workflows`; the legacy `.mokogitea/workflows` is fully retired. Workflow indexing, push-triggered CI/deploys, and `workflow_dispatch` work again (#798)
|
||||
- **Issue custom-status dropdown shows its options again** — the status `<select>` carried the `ui compact dropdown` class, so fomantic turned it into an overlay menu that painted behind the page content (options were present in the DOM but invisible); it is now a plain native `<select>` with its inline styles moved to a stylesheet (`web_src/css/repo/issue-status.css`)
|
||||
- Restored the repo's `.mokogit/workflows/` (deploy + org CI, ~20 workflows) from Template-Go after the workflow sync had dropped them
|
||||
|
||||
@@ -13,18 +13,22 @@ services:
|
||||
environment:
|
||||
- USER_UID=${GIT_UID}
|
||||
- USER_GID=${GIT_GID}
|
||||
- GITEA__server__DOMAIN=${GIT_DOMAIN}
|
||||
- GITEA__server__ROOT_URL=${GIT_ROOT_URL}
|
||||
- GITEA__server__SSH_DOMAIN=${GIT_SSH_DOMAIN}
|
||||
- GITEA__server__SSH_PORT=${GIT_SSH_PUBLIC_PORT}
|
||||
- GITEA__database__DB_TYPE=mysql
|
||||
- GITEA__database__HOST=${GIT_DB_HOST}
|
||||
- GITEA__database__NAME=${GIT_DB_NAME}
|
||||
- GITEA__database__USER=${GIT_DB_USER}
|
||||
- GITEA__database__PASSWD=${GIT_DB_PASSWD}
|
||||
- MOKOGIT__server__DOMAIN=${GIT_DOMAIN}
|
||||
- MOKOGIT__server__ROOT_URL=${GIT_ROOT_URL}
|
||||
- MOKOGIT__server__SSH_DOMAIN=${GIT_SSH_DOMAIN}
|
||||
- MOKOGIT__server__SSH_PORT=${GIT_SSH_PUBLIC_PORT}
|
||||
- MOKOGIT__database__DB_TYPE=mysql
|
||||
- MOKOGIT__database__HOST=${GIT_DB_HOST}
|
||||
- MOKOGIT__database__NAME=${GIT_DB_NAME}
|
||||
- MOKOGIT__database__USER=${GIT_DB_USER}
|
||||
- MOKOGIT__database__PASSWD=${GIT_DB_PASSWD}
|
||||
volumes:
|
||||
# RECONCILE: live app.ini at ${GIT_DATA_DIR}/conf/app.ini (/opt/mokogit/dev/conf).
|
||||
- "${GIT_DATA_DIR}:/data"
|
||||
# Clean disconnect layout (#839): MOKOGIT_CUSTOM=/data/mokogit is baked into the image.
|
||||
# Parent (/data/mokogit) listed before child (/data/mokogit/conf) so Docker attaches it first.
|
||||
- "${GIT_DATA_DIR}:/var/lib/gitea"
|
||||
- "${GIT_DATA_DIR}:/data/mokogit"
|
||||
- "${GIT_DATA_DIR}/conf:/data/mokogit/conf"
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
|
||||
@@ -17,19 +17,23 @@ services:
|
||||
environment:
|
||||
- USER_UID=${GIT_UID}
|
||||
- USER_GID=${GIT_GID}
|
||||
- GITEA__server__DOMAIN=${GIT_DOMAIN}
|
||||
- GITEA__server__ROOT_URL=${GIT_ROOT_URL}
|
||||
- GITEA__server__SSH_DOMAIN=${GIT_SSH_DOMAIN}
|
||||
- GITEA__server__SSH_PORT=${GIT_SSH_PUBLIC_PORT}
|
||||
- MOKOGIT__server__DOMAIN=${GIT_DOMAIN}
|
||||
- MOKOGIT__server__ROOT_URL=${GIT_ROOT_URL}
|
||||
- MOKOGIT__server__SSH_DOMAIN=${GIT_SSH_DOMAIN}
|
||||
- MOKOGIT__server__SSH_PORT=${GIT_SSH_PUBLIC_PORT}
|
||||
# DB is HOST MySQL (RECONCILE host reachability: host-gateway / socket / LAN IP).
|
||||
- GITEA__database__DB_TYPE=mysql
|
||||
- GITEA__database__HOST=${GIT_DB_HOST}
|
||||
- GITEA__database__NAME=${GIT_DB_NAME}
|
||||
- GITEA__database__USER=${GIT_DB_USER}
|
||||
- GITEA__database__PASSWD=${GIT_DB_PASSWD}
|
||||
- MOKOGIT__database__DB_TYPE=mysql
|
||||
- MOKOGIT__database__HOST=${GIT_DB_HOST}
|
||||
- MOKOGIT__database__NAME=${GIT_DB_NAME}
|
||||
- MOKOGIT__database__USER=${GIT_DB_USER}
|
||||
- MOKOGIT__database__PASSWD=${GIT_DB_PASSWD}
|
||||
volumes:
|
||||
# RECONCILE: live app.ini is at ${GIT_DATA_DIR}/conf/app.ini (/opt/mokogit/prod/conf).
|
||||
- "${GIT_DATA_DIR}:/data"
|
||||
# Clean disconnect layout (#839): MOKOGIT_CUSTOM=/data/mokogit is baked into the image.
|
||||
# Parent (/data/mokogit) listed before child (/data/mokogit/conf) so Docker attaches it first.
|
||||
- "${GIT_DATA_DIR}:/var/lib/gitea"
|
||||
- "${GIT_DATA_DIR}:/data/mokogit"
|
||||
- "${GIT_DATA_DIR}/conf:/data/mokogit/conf"
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
|
||||
@@ -13,18 +13,22 @@ services:
|
||||
environment:
|
||||
- USER_UID=${GIT_UID}
|
||||
- USER_GID=${GIT_GID}
|
||||
- GITEA__server__DOMAIN=${GIT_DOMAIN}
|
||||
- GITEA__server__ROOT_URL=${GIT_ROOT_URL}
|
||||
- GITEA__server__SSH_DOMAIN=${GIT_SSH_DOMAIN}
|
||||
- GITEA__server__SSH_PORT=${GIT_SSH_PUBLIC_PORT}
|
||||
- GITEA__database__DB_TYPE=mysql
|
||||
- GITEA__database__HOST=${GIT_DB_HOST}
|
||||
- GITEA__database__NAME=${GIT_DB_NAME}
|
||||
- GITEA__database__USER=${GIT_DB_USER}
|
||||
- GITEA__database__PASSWD=${GIT_DB_PASSWD}
|
||||
- MOKOGIT__server__DOMAIN=${GIT_DOMAIN}
|
||||
- MOKOGIT__server__ROOT_URL=${GIT_ROOT_URL}
|
||||
- MOKOGIT__server__SSH_DOMAIN=${GIT_SSH_DOMAIN}
|
||||
- MOKOGIT__server__SSH_PORT=${GIT_SSH_PUBLIC_PORT}
|
||||
- MOKOGIT__database__DB_TYPE=mysql
|
||||
- MOKOGIT__database__HOST=${GIT_DB_HOST}
|
||||
- MOKOGIT__database__NAME=${GIT_DB_NAME}
|
||||
- MOKOGIT__database__USER=${GIT_DB_USER}
|
||||
- MOKOGIT__database__PASSWD=${GIT_DB_PASSWD}
|
||||
volumes:
|
||||
# RECONCILE: live app.ini at ${GIT_DATA_DIR}/conf/app.ini (/opt/mokogit/rc/conf).
|
||||
- "${GIT_DATA_DIR}:/data"
|
||||
# Clean disconnect layout (#839): MOKOGIT_CUSTOM=/data/mokogit is baked into the image.
|
||||
# Parent (/data/mokogit) listed before child (/data/mokogit/conf) so Docker attaches it first.
|
||||
- "${GIT_DATA_DIR}:/var/lib/gitea"
|
||||
- "${GIT_DATA_DIR}:/data/mokogit"
|
||||
- "${GIT_DATA_DIR}/conf:/data/mokogit/conf"
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
|
||||
+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.
|
||||
+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
File diff suppressed because one or more lines are too long
Vendored
@@ -200,6 +200,16 @@ func GenerateReleaseArtifacts(ctx context.Context, rel *repo_model.Release) erro
|
||||
} else if meta != nil {
|
||||
entryPoint = meta.EntryPoint
|
||||
}
|
||||
|
||||
// Joomla repos are packaged by the CI pipeline (release_package.php),
|
||||
// which uploads the correct installable zip/tar.gz. Server-generated
|
||||
// source archives are redundant and confuse users about which file to
|
||||
// download, so skip them entirely.
|
||||
if meta != nil && strings.EqualFold(meta.Platform, "joomla") {
|
||||
log.Info("GenerateReleaseArtifacts: skipping source archives for Joomla repo %d (CI pipeline packages)", rel.RepoID)
|
||||
return nil
|
||||
}
|
||||
|
||||
wantSource := !isRootEntryPoint(entryPoint)
|
||||
|
||||
// Serialize the delete+attach sequence per-release so concurrent
|
||||
|
||||
@@ -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">
|
||||
<div class="ui attached table segment table-responsive">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<div class="ui attached table segment table-responsive">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
</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>
|
||||
@@ -31,6 +32,7 @@
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<input type="hidden" name="from" value="monitor">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<div class="ui attached table segment table-responsive">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<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>
|
||||
|
||||
@@ -125,4 +130,5 @@
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<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>
|
||||
@@ -58,6 +59,7 @@
|
||||
</tfoot>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<div class="ui attached table segment table-responsive">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<div class="ui attached table segment table-responsive">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.monitor.queues"}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<div class="ui attached table segment table-responsive">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="ui attached segment">
|
||||
{{template "shared/repo/search" .}}
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<div class="ui attached table segment table-responsive">
|
||||
<table class="ui very basic table selectable unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.dashboard.statistic"}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<div class="ui attached table segment table-responsive">
|
||||
<table class="ui very basic table unstackable">
|
||||
{{range $statsKey := .StatsKeys}}
|
||||
<tr>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<div class="ui attached table segment table-responsive">
|
||||
<table class="ui very basic selectable table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
<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: vendored third-party libs served statically from public/assets/vendor (byte-exact,
|
||||
bypassing the Vite asset pipeline which corrupts binary web fonts on file emission). Bootstrap
|
||||
CSS loads before the Gitea/Fomantic bundle so Fomantic overrides Reboot on desktop. */}}
|
||||
<link rel="stylesheet" href="{{AssetUrlPrefix}}/vendor/bootstrap/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{AssetUrlPrefix}}/vendor/fontawesome/css/all.min.css">
|
||||
<script src="{{AssetUrlPrefix}}/vendor/bootstrap/bootstrap.bundle.min.js" defer></script>
|
||||
{{template "base/head_style" .}}
|
||||
{{template "base/head_script" .}}
|
||||
{{template "custom/header" .}}
|
||||
@@ -36,7 +42,7 @@
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<body data-bs-no-jquery>
|
||||
{{template "custom/body_outer_pre" .}}
|
||||
|
||||
<div class="full height">
|
||||
|
||||
@@ -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/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
|
||||
<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'">
|
||||
</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,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}}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<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">
|
||||
@@ -60,4 +65,5 @@
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
<div class="overflow-menu-items overflow-auto flex-nowrap">
|
||||
{{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">
|
||||
<div class="overflow-menu-items overflow-auto flex-nowrap">
|
||||
{{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"}}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<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">
|
||||
@@ -72,4 +77,5 @@
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="ui container fluid">
|
||||
<div class="tw-text-center tw-mb-4">
|
||||
<img src="{{AssetUrlPrefix}}/img/login-logo.png" style="max-width: 220px; max-height: 80px; object-fit: contain;" onerror="this.style.display='none'">
|
||||
<img src="{{AssetUrlPrefix}}/img/favicon.svg" 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.png';}else{this.style.display='none';}">
|
||||
</div>
|
||||
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}}
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<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">
|
||||
@@ -69,4 +74,5 @@
|
||||
{{svg "octicon-repo"}} {{ctx.Locale.Tr "settings.repos"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGIT/models/auth"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// apiMetadataResponse mirrors the subset of the repo manifest/metadata API
|
||||
// payload exercised by this test. The handler struct (routers/api/v1/repo)
|
||||
// is unexported, so the fields are re-declared here by their JSON tags.
|
||||
type apiMetadataResponse struct {
|
||||
Name string `json:"name"`
|
||||
NodeMinimum string `json:"node_minimum"`
|
||||
NpmPackage string `json:"npm_package"`
|
||||
PublishTarget string `json:"publish_target"`
|
||||
}
|
||||
|
||||
// TestAPIRepoMetadataNpmFieldsRoundTrip guards against regression of issue #847
|
||||
// (from #827): the npm/mcp fields node_minimum, npm_package and publish_target
|
||||
// must be persisted via PUT and returned via GET on the metadata endpoint,
|
||||
// rather than being silently dropped by the apiMetadata struct.
|
||||
func TestAPIRepoMetadataNpmFieldsRoundTrip(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
// user1 is the site admin in the fixtures; the PUT route requires admin.
|
||||
session := loginUser(t, "user1")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
const owner, repo = "user2", "repo1"
|
||||
|
||||
// PUT the three npm/mcp fields.
|
||||
req := NewRequestWithJSON(t, "PUT", "/api/v1/repos/"+owner+"/"+repo+"/metadata", map[string]string{
|
||||
"node_minimum": "18",
|
||||
"npm_package": "@moko/foo",
|
||||
"publish_target": "npm",
|
||||
}).AddTokenAuth(token)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// The PUT response should echo the persisted fields.
|
||||
var put apiMetadataResponse
|
||||
DecodeJSON(t, resp, &put)
|
||||
assert.Equal(t, "18", put.NodeMinimum)
|
||||
assert.Equal(t, "@moko/foo", put.NpmPackage)
|
||||
assert.Equal(t, "npm", put.PublishTarget)
|
||||
|
||||
// A subsequent GET should read the same values back from the DB.
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/"+owner+"/"+repo+"/metadata").AddTokenAuth(token)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var got apiMetadataResponse
|
||||
DecodeJSON(t, resp, &got)
|
||||
assert.Equal(t, "18", got.NodeMinimum)
|
||||
assert.Equal(t, "@moko/foo", got.NpmPackage)
|
||||
assert.Equal(t, "npm", got.PublishTarget)
|
||||
})
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
@import "./modules/normalize.css";
|
||||
@import "./modules/animations.css";
|
||||
|
||||
/* 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";
|
||||
/* 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. */
|
||||
|
||||
/* fomantic replacements */
|
||||
@import "./modules/button.css";
|
||||
@@ -54,6 +54,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 +96,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, …).
|
||||
*
|
||||
* 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 (768–991px): 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
// 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';
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user