Compare commits

..

34 Commits

Author SHA1 Message Date
jmiller 34f9ae6119 Merge pull request 'fix(build): serve Bootstrap + FA statically from public/assets/vendor — fixes tofu icons (#818)' (#861) from feat/mobile-retheme into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Deploy (Dev) / Deploy to Dev (push) Has started running
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m8s
2026-07-21 21:08:35 +00:00
jmiller c2a8894eee fix(build): serve Bootstrap + Font Awesome statically from public/assets/vendor (#818 #719)
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Generic: Project CI / Tests (pull_request) Successful in 38s
Generic: Project CI / Lint & Validate (pull_request) Successful in 40s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 10s
Universal: PR Check / Secret Scan (pull_request) Successful in 57s
Real root cause of the tofu icons: Vite's asset FILE-emission corrupts the binary woff2
during `make frontend` (served font magic bytes `d46b6c0e`, 512 bytes short of the valid
`wOF2`; the inlined data-URI regular font worked, the file-emitted solid font did not).
git checkout and nginx were ruled out (server working-tree font + committed blob are valid
wOF2; Content-Length header even reports the correct 113152 while the body is corrupt).

Fix + reorg (per request: vendored tools live in a vendor folder): move Font Awesome and
Bootstrap out of the Vite pipeline into public/assets/vendor/{fontawesome,bootstrap}, which
Gitea embeds and serves byte-exact (like public/assets/img, verified by the working favicon).
- head.tmpl <link>s the vendored CSS (Bootstrap before the Fomantic bundle so Reboot is
  overridden on desktop) and <script defer>s Bootstrap's JS bundle.
- Remove the FA + Bootstrap @import from web_src/css/index.css and the Bootstrap JS import
  from web_src/js/index.ts.
- <body data-bs-no-jquery> still prevents Bootstrap clobbering Fomantic's jQuery plugins.

mokoonyx stays under web_src/css/vendor (pure CSS tokens that must be bundled, no binary).

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 16:06:32 -05:00
jmiller 99692b1bbb Merge pull request 'fix(build): mark web fonts binary in .gitattributes — fixes tofu FA icons (#818)' (#860) from feat/mobile-retheme into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 52s
Deploy (Dev) / Deploy to Dev (push) Successful in 3m38s
2026-07-21 20:48:26 +00:00
jmiller dcc8b1e5ca fix(build): mark web fonts binary so CI checkout doesn't corrupt them (#818)
Generic: Project CI / Tests (pull_request) Successful in 27s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 32s
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 8s
Universal: PR Check / Secret Scan (pull_request) Successful in 48s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Root cause of the blank/tofu Font Awesome icons: .gitattributes had `* text=auto
eol=lf` and protected `/vendor/**` and `/web_src/js/vendor/**` with `-text -eol`, but
NOT `/web_src/css/vendor/**` — where the vendored FA webfonts live. So the woff2 files
were eol-normalized on the Linux CI checkout (served magic bytes `d46b6c0e`, 512 bytes
short, vs valid `wOF2`), making the font unparseable — the browser reported the
fa-solid-900 @font-face as status:"error" and every fa-solid glyph rendered as tofu.

Add `/web_src/css/vendor/** -text -eol` and explicit `*.woff/*.woff2/*.ttf/*.eot/*.otf
binary` so no web font is ever eol-normalized regardless of location. The committed blob
was already valid wOF2; this stops the corruption on checkout.

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 15:47:00 -05:00
jmiller 20b8b77436 Merge pull request 'fix(ui): body data-bs-no-jquery (real Fomantic fix) + login/navbar favicon (#719 #819)' (#859) from feat/mobile-retheme into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m13s
Deploy (Dev) / Deploy to Dev (push) Successful in 4m0s
2026-07-21 20:09:21 +00:00
jmiller 5afbf005ed fix(ui): stop Bootstrap clobbering Fomantic via <body data-bs-no-jquery> (#719)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Generic: Project CI / Tests (pull_request) Successful in 39s
Generic: Project CI / Lint & Validate (pull_request) Successful in 44s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m2s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The earlier noConflict shim (f5bf232) didn't work: Bootstrap 5 registers its jQuery
plugins inside an onDOMContentLoaded callback, so the shim (running at module-eval, before
DOMContentLoaded) restored Fomantic's $.fn.* BEFORE Bootstrap overwrote them — then
Bootstrap clobbered again. Verified still broken on dev (fullTextSearch TypeError persisted
in the redeployed bundle).

Correct fix: add `data-bs-no-jquery` to <body>. Bootstrap's getjQuery() then returns null
and it registers NO jQuery plugins, so Fomantic's dropdown/modal/tab/... are never touched.
Bootstrap's data-bs-* data-API is bound independently and keeps working (mobile collapse
drawers). Removes the now-unnecessary noconflict.ts.

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 15:08:37 -05:00
jmiller 65019dd5ad fix(ui): navbar brand uses selected favicon instead of gitea logo (#819)
The navbar brand (top-left, shown on the dashboard you land on after login) rendered
the default gitea logo.svg. Point it at the admin-selected favicon (/img/favicon.svg,
png fallback) so post-login pages carry the configured site icon.

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 15:03:03 -05:00
jmiller e8d6451f17 fix(ui): use selected favicon on the login page instead of missing gitea logo (#819)
The login page hard-coded {{AssetUrlPrefix}}/img/login-logo.png, which 404s (broken
image above the sign-in card). Point it at the configured favicon (/img/favicon.svg)
with a png fallback then hide, so the login page shows the actual selected site
icon/branding. Adds an alt of {{AppName}}.

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 14:54:43 -05:00
jmiller a3f92d3890 Merge pull request 'fix(ui): Bootstrap/Fomantic jQuery no-conflict + Bootstrap-component mobile rewrite (#719)' (#858) from feat/mobile-retheme into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m49s
Deploy (Dev) / Deploy to Dev (push) Successful in 5m7s
2026-07-21 19:52:11 +00:00
jmiller f5bf2321de fix(ui): restore Fomantic jQuery plugins after Bootstrap clobbers them (#719)
Generic: Project CI / Lint & Validate (pull_request) Successful in 35s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 14s
Generic: Project CI / Tests (pull_request) Successful in 32s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 53s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Bootstrap 5 still registers jQuery plugins (defineJQueryPlugin) when jQuery is present,
overwriting $.fn.{dropdown,modal,tab,collapse,tooltip,...} — the same names Fomantic-UI
owns. That made $.fn.dropdown.settings undefined, so Gitea's Fomantic init threw
"Cannot set properties of undefined (setting 'fullTextSearch')" and aborted the whole
client init pipeline (dropdowns/modals/tabs broke; the FAB never rendered).

Add vendor/bootstrap/noconflict.ts (imported right after the bundle) that calls each
colliding plugin's .noConflict() to hand the name back to Fomantic. Bootstrap's data-bs-*
API stays functional (it's bound to document events, not the jQuery interface), so the
mobile collapse drawers still work.

Verified via dev deploy: the drop-in built green but the login page surfaced the uncaught
fullTextSearch error + missing FAB; this restores Fomantic.

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 14:50:56 -05:00
jmiller e0e5f2f9a7 feat(ui): Bootstrap-powered mobile rewrite of nav/tables/settings (#719)
Convert the mobile-problem areas to real Bootstrap 5 components (drop-in already loaded):
- Settings navbars (repo/user/org/admin): wrap the Fomantic vertical menu in a Bootstrap
  .collapse .d-md-block drawer with a .d-md-none "Menu" trigger (data-bs-toggle=collapse).
  Chose collapse over offcanvas to avoid z-index races with the fixed FAB.
- Admin wide tables (11): .table-responsive so they scroll-contain instead of the page.
- Repo sub-nav (header.tmpl): Bootstrap .overflow-auto .flex-nowrap for the tab bar.
- mokogit-mobile.css: drop rules now handled by Bootstrap (table overflow, the hand-rolled
  settings rail); keep only Gitea-selector fixes Bootstrap can't reach (diff single-column
  collapse, issue-list column hiding, 44px touch targets, FAB clearance).

Additive template edits; all existing Fomantic classes preserved (desktop unchanged at md+).

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 14:34:33 -05:00
jmiller 49e7485acc Merge pull request 'feat(ui): mobile responsiveness + mokoonyx retheme + Bootstrap 5 drop-in (#719 #822 #835 #818 #803)' (#857) from feat/mobile-retheme into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m13s
Deploy (Dev) / Deploy to Dev (push) Successful in 5m15s
2026-07-21 19:29:06 +00:00
jmiller 8828c33e16 feat(ui): vendor Bootstrap 5 drop-in + mobile layer + wire inits (#719)
Generic: Project CI / Lint & Validate (pull_request) Successful in 33s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Tests (pull_request) Successful in 23s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 18s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 51s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Vendor Bootstrap 5.3.3 (MIT) CSS + JS bundle into web_src/{css,js}/vendor/bootstrap
(exFAT-safe; no node_modules symlink dependency). Load bootstrap.min.css BEFORE the
fomantic replacements so Fomantic overrides Reboot on desktop while Bootstrap grid +
responsive utilities power the mobile layer; import the JS bundle (incl. Popper) in
index.ts. Add mokogit-mobile.css (diffs, nav, tables, settings drawer, 44px touch
targets) targeting Gitea selectors. Wire index.css/index.ts: import mokogit-fab.css +
mokogit-mobile.css, and replace initThemeToggle/initA11yMenu with initMokogitFab +
initFlashToast.

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 14:22:20 -05:00
jmiller cae7846c35 feat(ui): unified theme+a11y FAB with mobile collapse-into-tab (#835)
New mokogit-fab.{ts,css}: a sidebar-corner FAB hosting the light/dark+AUTO theme
toggle and the a11y toolbar. On mobile (<=767.98px) it collapses into a labeled
universal-access tab whose panel emerges from the tab (getBoundingClientRect),
closes on tap-outside/Escape; on login it relocates into the auth card as a flush
bottom bar. Refactors theme-toggle.ts/a11y-menu.ts to expose mountable controls
(shared state, localStorage preserved) and removes the superseded navbar toggles
(hidden CSRF theme form kept so signed-in theme POST still works). Fixes a11y being
unreachable on phones (navbar-right was display:none <768px).

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 14:22:18 -05:00
jmiller c77eebfe27 feat(ui): toast success level + flash→toast bridge (#803)
Add 'success' to Intent + levels.success (green var(--color-green), check icon) and
export showSuccessToast; rewire info to a distinct neutral blue. New flash-toast.ts
mirrors server .flash-message banners into aria-live toasts (polite success/info,
assertive warning/error) and removes the banner, gated on the element existing so the
no-JS banner fallback is preserved. Unit test covers showSuccessToast.

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 14:22:15 -05:00
jmiller 4f825e7cff feat(ui): retheme tokens + app chrome to mokoonyx (#822)
Retune theme-mokogit-{light,dark}.css token VALUES to the MokoAi mokoonyx palette
(dark bg #0e1318 / card #151b22 / border #2b323b / text #e6ebf1; light accent
#aec5f5 with black text on accent-filled buttons). Extend mokogit-brand.css with a
MokoOnyx app-chrome layer (navbar, settings sidebars, cards, tables, buttons,
dropdowns, form focus ring) — all via theme vars so light/dark follow automatically.

Authored-by: Moko Consulting
Claude-Session: https://claude.ai/code/session_01D5Zxu4xRRGoh9etzgShP4P
2026-07-21 14:22:12 -05:00
jmiller b20fe1d37c Merge pull request 'chore(sync): cascade main -> dev' (#856) from main into dev
Deploy (Dev) / Deploy to Dev (push) Successful in 6m48s
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
2026-07-21 18:20:53 +00:00
jmiller 670d26408d chore: sync auto-release.yml from Template-Go [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-07-21 18:20:27 +00:00
jmiller ef0c22aecc Merge pull request 'chore(release): merge dev to main' (#855) from dev into main
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 50s
Universal: Push Notifications / push-notify (push) Successful in 2s
Generic: Standards Compliance / Secret Scanning (push) Failing after 9s
Generic: Standards Compliance / License Header Validation (push) Successful in 7s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 9s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 9s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 1m40s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 7s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 7s
Deploy (Prod) / Deploy to Prod (push) Successful in 3m22s
Generic: Standards Compliance / README Completeness Check (push) Failing after 5s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 7s
Generic: Standards Compliance / Line Length Check (push) Successful in 13s
Generic: Standards Compliance / File Naming Standards (push) Successful in 6s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 32s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 5s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 38s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 38s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 7s
Generic: Standards Compliance / File Size Limits (push) Successful in 6s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 5s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 41s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 8s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 6s
Generic: Standards Compliance / Accessibility Check (push) Successful in 6s
Generic: Standards Compliance / Binary File Detection (push) Successful in 1m16s
Generic: Standards Compliance / Performance Metrics (push) Successful in 5s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 44s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 9s
Generic: Standards Compliance / Repository Health Check (push) Successful in 46s
Generic: Standards Compliance / Enterprise Readiness Check (push) Successful in 52s
Generic: Project CI / Lint & Validate (pull_request) Successful in 37s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Generic: Project CI / Tests (pull_request) Successful in 40s
Universal: PR Check / Validate PR (pull_request) Successful in 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 47s
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
2026-07-21 16:51:24 +00:00
jmiller c6e0a45bb4 Merge pull request 'fix(release): skip source archive generation for Joomla repos (#853)' (#854) from fix/853-skip-joomla-source-archives into dev
Deploy (Dev) / Deploy to Dev (push) Successful in 2m50s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Project CI / Tests (pull_request) Successful in 33s
Generic: Project CI / Lint & Validate (pull_request) Successful in 35s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 8s
Universal: PR Check / Require Docs Update (pull_request) Failing after 41s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Standards Compliance / Secret Scanning (pull_request) Failing after 8s
Generic: Standards Compliance / License Header Validation (pull_request) Successful in 7s
Generic: Standards Compliance / Repository Structure Validation (pull_request) Successful in 8s
Generic: Standards Compliance / Coding Standards Check (pull_request) Successful in 9s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Generic: Standards Compliance / Workflow Configuration Check (pull_request) Failing after 8s
Universal: PR Check / Secret Scan (pull_request) Successful in 40s
Generic: Standards Compliance / Documentation Quality Check (pull_request) Successful in 7s
Generic: Standards Compliance / README Completeness Check (pull_request) Failing after 7s
Generic: Standards Compliance / Script Integrity Validation (pull_request) Successful in 10s
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 43s
Generic: Standards Compliance / Line Length Check (pull_request) Successful in 17s
Generic: Standards Compliance / File Naming Standards (pull_request) Successful in 6s
Generic: Standards Compliance / Git Repository Hygiene (pull_request) Successful in 39s
Generic: Standards Compliance / Version Consistency Check (pull_request) Successful in 56s
Generic: Standards Compliance / Insecure Code Pattern Detection (pull_request) Successful in 6s
Generic: Standards Compliance / Dead Code Detection (pull_request) Successful in 9s
Generic: Standards Compliance / File Size Limits (pull_request) Successful in 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 37s
Generic: Standards Compliance / Code Complexity Analysis (pull_request) Successful in 44s
Generic: Standards Compliance / Code Duplication Detection (pull_request) Successful in 45s
Generic: Standards Compliance / TODO/FIXME Tracking (pull_request) Successful in 6s
Generic: Standards Compliance / Dependency Vulnerability Scanning (pull_request) Successful in 42s
Generic: Standards Compliance / Unused Dependencies Check (pull_request) Successful in 38s
Generic: Standards Compliance / API Documentation Coverage (pull_request) Successful in 6s
Generic: Standards Compliance / Binary File Detection (pull_request) Successful in 1m15s
Generic: Standards Compliance / Broken Link Detection (pull_request) Successful in 8s
Generic: Standards Compliance / Accessibility Check (pull_request) Successful in 6s
Generic: Standards Compliance / Performance Metrics (pull_request) Successful in 7s
Generic: Standards Compliance / Terraform Configuration Validation (pull_request) Successful in 8s
Generic: Standards Compliance / Enterprise Readiness Check (pull_request) Successful in 44s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Standards Compliance / Repository Health Check (pull_request) Successful in 42s
Deploy (RC) / Deploy to RC (push) Failing after 5m53s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Generic: Standards Compliance / Compliance Summary (pull_request) Has been cancelled
fix(release): skip source archive generation for Joomla repos (#853)

Closes #853
2026-07-20 14:47:44 +00:00
jmiller 154c3f77e4 Merge pull request 'test(metadata): apiMetadata npm-fields round-trip test [#847]' (#848) from fix/apimetadata-npm-fields into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 46s
Deploy (Dev) / Deploy to Dev (push) Successful in 2m40s
2026-07-20 14:37:41 +00:00
Moko Consulting c72d872557 fix(release): skip source archive generation for Joomla repos (#853)
Generic: Project CI / Lint & Validate (pull_request) Successful in 37s
Generic: Project CI / Tests (pull_request) Successful in 36s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 48s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 12s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m1s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Joomla repos are packaged by the CI pipeline (release_package.php),
which uploads the correct installable zip/tar.gz. The server-generated
source archives are redundant and confuse users about which file to
download.

Closes #853

Claude-Session: https://claude.ai/code/session_01CwLGvFJPjoPTp9BEnSjtJf
2026-07-20 09:34:01 -05:00
jmiller 02b0377b1d Merge pull request 'fix(deploy): migrate deploy/git compose templates to MOKOGIT__ + /data/mokogit' (#846) from feature/deploy-templates-mokogit-paths into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 41s
Deploy (Dev) / Deploy to Dev (push) Successful in 3m13s
2026-07-20 14:33:57 +00:00
jmiller f27c73195f Merge pull request 'release: MokoGIT hard-fork disconnect to prod (rebrand + Option-B GPL + /data/mokogit)' (#852) from dev into main
Cascade Main -> Dev / Cascade main -> dev (push) Has been cancelled
Deploy (Prod) / Deploy to Prod (push) Has been cancelled
Universal: Push Notifications / push-notify (push) Has been cancelled
Generic: Standards Compliance / Secret Scanning (push) Has been cancelled
Generic: Standards Compliance / License Header Validation (push) Has been cancelled
Generic: Standards Compliance / Repository Structure Validation (push) Has been cancelled
Generic: Standards Compliance / Coding Standards Check (push) Has been cancelled
Generic: Standards Compliance / Version Consistency Check (push) Has been cancelled
Generic: Standards Compliance / Workflow Configuration Check (push) Has been cancelled
Generic: Standards Compliance / Documentation Quality Check (push) Has been cancelled
Generic: Standards Compliance / README Completeness Check (push) Has been cancelled
Generic: Standards Compliance / Git Repository Hygiene (push) Has been cancelled
Generic: Standards Compliance / Script Integrity Validation (push) Has been cancelled
Generic: Standards Compliance / Line Length Check (push) Has been cancelled
Generic: Standards Compliance / File Naming Standards (push) Has been cancelled
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Has been cancelled
Generic: Standards Compliance / Code Complexity Analysis (push) Has been cancelled
Generic: Standards Compliance / Code Duplication Detection (push) Has been cancelled
Generic: Standards Compliance / Dead Code Detection (push) Has been cancelled
Generic: Standards Compliance / File Size Limits (push) Has been cancelled
Generic: Standards Compliance / Binary File Detection (push) Has been cancelled
Generic: Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Generic: Standards Compliance / Unused Dependencies Check (push) Has been cancelled
Generic: Standards Compliance / Broken Link Detection (push) Has been cancelled
Generic: Standards Compliance / API Documentation Coverage (push) Has been cancelled
Generic: Standards Compliance / Accessibility Check (push) Has been cancelled
Generic: Standards Compliance / Performance Metrics (push) Has been cancelled
Generic: Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
Generic: Standards Compliance / Repository Health Check (push) Has been cancelled
Generic: Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
2026-07-20 14:13:16 +00:00
jmiller c2b64de621 Merge pull request 'docs(changelog): Joomla update-server artifact-selection fix' (#851) from chore/changelog-updateserver into dev
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 47s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 47s
Deploy (Dev) / Deploy to Dev (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Successful in 44s
Generic: Project CI / Lint & Validate (pull_request) Successful in 47s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Standards Compliance / Secret Scanning (pull_request) Has been cancelled
Generic: Standards Compliance / License Header Validation (pull_request) Has been cancelled
Generic: Standards Compliance / Repository Structure Validation (pull_request) Has been cancelled
Generic: Standards Compliance / Coding Standards Check (pull_request) Has been cancelled
Generic: Standards Compliance / Version Consistency Check (pull_request) Has been cancelled
Generic: Standards Compliance / Workflow Configuration Check (pull_request) Has been cancelled
Generic: Standards Compliance / Documentation Quality Check (pull_request) Has been cancelled
Generic: Standards Compliance / README Completeness Check (pull_request) Has been cancelled
Generic: Standards Compliance / Git Repository Hygiene (pull_request) Has been cancelled
Generic: Standards Compliance / Script Integrity Validation (pull_request) Has been cancelled
Generic: Standards Compliance / Line Length Check (pull_request) Has been cancelled
Generic: Standards Compliance / File Naming Standards (pull_request) Has been cancelled
Generic: Standards Compliance / Insecure Code Pattern Detection (pull_request) Has been cancelled
Generic: Standards Compliance / Code Complexity Analysis (pull_request) Has been cancelled
Generic: Standards Compliance / Code Duplication Detection (pull_request) Has been cancelled
Generic: Standards Compliance / Dead Code Detection (pull_request) Has been cancelled
Generic: Standards Compliance / File Size Limits (pull_request) Has been cancelled
Generic: Standards Compliance / Binary File Detection (pull_request) Has been cancelled
Generic: Standards Compliance / TODO/FIXME Tracking (pull_request) Has been cancelled
Generic: Standards Compliance / Dependency Vulnerability Scanning (pull_request) Has been cancelled
Generic: Standards Compliance / Unused Dependencies Check (pull_request) Has been cancelled
Generic: Standards Compliance / Broken Link Detection (pull_request) Has been cancelled
Generic: Standards Compliance / API Documentation Coverage (pull_request) Has been cancelled
Generic: Standards Compliance / Accessibility Check (pull_request) Has been cancelled
Generic: Standards Compliance / Performance Metrics (pull_request) Has been cancelled
Generic: Standards Compliance / Enterprise Readiness Check (pull_request) Has been cancelled
Generic: Standards Compliance / Repository Health Check (pull_request) Has been cancelled
Generic: Standards Compliance / Terraform Configuration Validation (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been cancelled
Deploy (RC) / Deploy to RC (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Generic: Standards Compliance / Compliance Summary (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Successful in 23s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m4s
Universal: PR Check / Require Docs Update (pull_request) Successful in 1m7s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
2026-07-20 14:09:12 +00:00
Moko Consulting d7d6662804 docs(changelog): record Joomla update-server artifact-selection fix
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 28s
Generic: Project CI / Lint & Validate (pull_request) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Universal: PR Check / Require Docs Update (pull_request) Has been cancelled
Universal: PR Check / Wiki Update Reminder (pull_request) Has been cancelled
Universal: PR Check / Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-07-20 09:08:21 -05:00
jmiller 2878699082 Merge pull request 'chore(sync): cascade main -> dev' (#845) from main into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 50s
Deploy (Dev) / Deploy to Dev (push) Has been cancelled
2026-07-20 13:57:29 +00:00
jmiller b551493110 chore: sync deploy-{dev,rc,prod}.yml from Template-Go [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Thin-trigger deploy: logic in mokocli (cli/deploy.php) + restricted per-repo deploy
pattern. No registry token / build script in CI. Onboarding-guarded.
Authored-by: Moko Consulting
2026-07-20 08:57:14 -05:00
jmiller 24e6e2b4a1 Merge pull request 'chore: ignore .claude + untrack .gemini (AI client dot-folders)' (#850) from chore/ignore-ai-folders into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m25s
Deploy (Dev) / Build & Deploy to Dev (push) Failing after 57s
2026-07-20 13:47:02 +00:00
Moko Consulting 7f540b4510 chore: ignore .claude + untrack .gemini (AI client dot-folders)
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 49s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 33s
Universal: PR Check / Validate PR (pull_request) Successful in 10s
Generic: Project CI / Tests (pull_request) Successful in 29s
Universal: PR Check / Secret Scan (pull_request) Successful in 40s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
AI-client working dirs must not be committed. Add .claude/ to .gitignore
(.gemini/ and .mokoai/ already present) and untrack .gemini/GEMINI.md.

Authored-by: Moko Consulting
2026-07-20 08:46:22 -05:00
Moko Consulting 92d7d5563b fix(metadata): serialize npm/mcp fields in apiMetadata (npm_package/node_minimum/publish_target) [#847]
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 44s
Generic: Project CI / Tests (pull_request) Successful in 29s
Generic: Project CI / Lint & Validate (pull_request) Successful in 34s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 9s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 40s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The GET/PUT /api/v1/repos/{owner}/{repo}/metadata handler already declares
node_minimum, npm_package and publish_target on the apiMetadata struct and
wires both the GET-serialize and PUT-persist paths (commit 5f85b27, #363).
No round-trip test guarded that behavior, so a regression could silently
reintroduce #847 (from #827) where these DB columns are un-settable and
un-readable over the REST API.

Add an integration test that PUTs the three fields and asserts they are
echoed by the PUT response and read back by a subsequent GET.

Authored-by: Moko Consulting
2026-07-20 08:41:39 -05:00
Moko Consulting 00f3d5d6d8 fix(deploy): migrate compose templates to MOKOGIT__ prefix + /data/mokogit mounts [#839 follow-up]
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Generic: Project CI / Lint & Validate (pull_request) Successful in 28s
Generic: Project CI / Tests (pull_request) Successful in 29s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 8s
Universal: PR Check / Secret Scan (pull_request) Successful in 39s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
The image disconnect (#839) renamed the env prefix GITEA__ -> MOKOGIT__ and
moved the custom path to /data/mokogit (MOKOGIT_CUSTOM baked into the image,
no GITEA__ fallback in the binary). The deploy compose templates still used the
old GITEA__ prefix and a single :/data mount, so they produced a forge with no
config. Align dev/rc/prod templates with the validated clean running state:

- Rename all GITEA__section__key -> MOKOGIT__section__key (server + database).
- Replace the single ${GIT_DATA_DIR}:/data bind with the clean 3-mount layout:
  ${GIT_DATA_DIR}:/var/lib/gitea (WorkPath),
  ${GIT_DATA_DIR}:/data/mokogit (CustomPath),
  ${GIT_DATA_DIR}/conf:/data/mokogit/conf (app.ini, child after parent).

Ports, SSH, domain, container_name, image, healthcheck, and all ${GIT_*}
variable names are unchanged. .env.example files reference no GITEA_ vars or
old paths, so they need no changes.

Authored-by: Moko Consulting <hello@mokoconsulting.tech>
2026-07-20 07:37:44 -05:00
jmiller f6b43492e9 Merge pull request 'fix(updateserver): pick installable package zip, not source archive (prod hotfix)' (#844) from hotfix/joomla-artifact-selection into main
Universal: Push Notifications / push-notify (push) Successful in 2s
Generic: Standards Compliance / Secret Scanning (push) Failing after 8s
Generic: Standards Compliance / License Header Validation (push) Successful in 12s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 16s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 14s
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 1m5s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 8s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 19s
Generic: Standards Compliance / README Completeness Check (push) Failing after 16s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 2m2s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 17s
Generic: Standards Compliance / Line Length Check (push) Successful in 23s
Generic: Standards Compliance / File Naming Standards (push) Successful in 11s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 10s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 2m54s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 1m9s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 8s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 54s
Generic: Standards Compliance / File Size Limits (push) Successful in 6s
Deploy (Prod) / Build & Deploy to Prod (push) Successful in 5m46s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 6s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 41s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 46s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 6s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 9s
Generic: Standards Compliance / Accessibility Check (push) Successful in 6s
Generic: Standards Compliance / Performance Metrics (push) Successful in 6s
Generic: Standards Compliance / Binary File Detection (push) Successful in 1m20s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 10s
Generic: Standards Compliance / Repository Health Check (push) Successful in 42s
Generic: Standards Compliance / Enterprise Readiness Check (push) Successful in 42s
Generic: Project CI / Lint & Validate (pull_request) Successful in 29s
Generic: Project CI / Tests (pull_request) Successful in 28s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 35s
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
2026-07-20 11:53:14 +00:00
Moko Consulting 457a53f987 fix(updateserver): pick installable package zip, not the source archive
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m5s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 1m8s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Project CI / Lint & Validate (pull_request) Successful in 34s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 1s
Generic: Project CI / Tests (pull_request) Successful in 22s
Universal: PR Check / Validate PR (pull_request) Successful in 7s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Standards Compliance / Secret Scanning (pull_request) Failing after 8s
Universal: PR Check / Require Docs Update (pull_request) Failing after 35s
Generic: Standards Compliance / License Header Validation (pull_request) Successful in 7s
Generic: Standards Compliance / Repository Structure Validation (pull_request) Successful in 13s
Generic: Standards Compliance / Coding Standards Check (pull_request) Successful in 18s
Generic: Standards Compliance / Workflow Configuration Check (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 54s
Generic: Standards Compliance / Documentation Quality Check (pull_request) Successful in 6s
Generic: Standards Compliance / README Completeness Check (pull_request) Failing after 7s
Generic: Standards Compliance / Script Integrity Validation (pull_request) Successful in 8s
Generic: Standards Compliance / Line Length Check (pull_request) Successful in 14s
Generic: Standards Compliance / Version Consistency Check (pull_request) Successful in 49s
Generic: Standards Compliance / File Naming Standards (pull_request) Successful in 9s
Generic: Standards Compliance / Git Repository Hygiene (pull_request) Successful in 45s
Generic: Standards Compliance / Insecure Code Pattern Detection (pull_request) Successful in 15s
Generic: Standards Compliance / Code Duplication Detection (pull_request) Successful in 47s
Generic: Standards Compliance / Code Complexity Analysis (pull_request) Successful in 1m15s
Generic: Standards Compliance / Dead Code Detection (pull_request) Successful in 25s
Generic: Standards Compliance / File Size Limits (pull_request) Successful in 20s
Generic: Standards Compliance / TODO/FIXME Tracking (pull_request) Successful in 21s
Generic: Standards Compliance / Dependency Vulnerability Scanning (pull_request) Successful in 1m6s
Generic: Standards Compliance / Unused Dependencies Check (pull_request) Successful in 49s
Generic: Standards Compliance / Broken Link Detection (pull_request) Successful in 8s
Generic: Standards Compliance / API Documentation Coverage (pull_request) Successful in 6s
Generic: Standards Compliance / Accessibility Check (pull_request) Successful in 6s
Generic: Standards Compliance / Performance Metrics (pull_request) Successful in 6s
Generic: Standards Compliance / Binary File Detection (pull_request) Successful in 2m3s
Generic: Standards Compliance / Terraform Configuration Validation (pull_request) Successful in 13s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Standards Compliance / Enterprise Readiness Check (pull_request) Successful in 53s
Generic: Standards Compliance / Repository Health Check (pull_request) Successful in 51s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Generic: Standards Compliance / Compliance Summary (pull_request) Has been cancelled
The Joomla update feed took the FIRST .zip attachment on a release as the
download URL. Releases that carry extra zips (e.g. <repo>-<channel>-source.zip
alongside pkg_*.zip) then advertised the raw SOURCE archive, so Joomla
downloaded a non-installable zip and failed with "Install path does not exist"
(affected prerelease/dev channels; stable had a single pkg_ zip so was fine).

Add selectJoomlaArtifact(): prefer an extension-prefixed zip
(pkg_/com_/mod_/plg_/tpl_/lib_) that is not a "-source" archive, then any
non-source zip, then any zip.

Authored-by: Moko Consulting
2026-07-20 06:51:55 -05:00
56 changed files with 1513 additions and 307 deletions
View File
+8
View File
@@ -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
+1
View File
@@ -144,3 +144,4 @@ Makefile.local
# ============================================================
wiki/
docs/
.claude/
+55 -1
View File
@@ -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
+25 -43
View File
@@ -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
+25 -44
View File
@@ -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
+28 -46
View File
@@ -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
+1
View File
@@ -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
+14 -10
View File
@@ -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:
+14 -10
View File
@@ -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:
+14 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
+10
View File
@@ -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
+1 -1
View File
@@ -6,7 +6,7 @@
<a class="ui primary tiny button" href="{{AppSubUrl}}/-/admin/auths/new">{{ctx.Locale.Tr "admin.auths.new"}}</a>
</div>
</h4>
<div class="ui attached table segment">
<div class="ui attached table segment table-responsive">
<table class="ui very basic table unstackable">
<thead>
<tr>
+1 -1
View File
@@ -29,7 +29,7 @@
</div>
</form>
</div>
<div class="ui attached table segment">
<div class="ui attached table segment table-responsive">
<table class="ui very basic striped table unstackable">
<thead>
<tr>
+2
View File
@@ -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>
+1 -1
View File
@@ -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>
+6
View File
@@ -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>
+2
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -3,7 +3,7 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.monitor.queues"}}
</h4>
<div class="ui attached table segment">
<div class="ui attached table segment table-responsive">
<table class="ui very basic table unstackable">
<thead>
<tr>
+1 -1
View File
@@ -9,7 +9,7 @@
<div class="ui attached segment">
{{template "shared/repo/search" .}}
</div>
<div class="ui attached table segment">
<div class="ui attached table segment table-responsive">
<table class="ui very basic table selectable unstackable">
<thead>
<tr>
+1 -1
View File
@@ -3,7 +3,7 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.dashboard.statistic"}}
</h4>
<div class="ui attached table segment">
<div class="ui attached table segment table-responsive">
<table class="ui very basic table unstackable">
{{range $statsKey := .StatsKeys}}
<tr>
+1 -1
View File
@@ -55,7 +55,7 @@
</div>
</form>
</div>
<div class="ui attached table segment">
<div class="ui attached table segment table-responsive">
<table class="ui very basic selectable table unstackable">
<thead>
<tr>
+7 -1
View File
@@ -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">
+6 -25
View File
@@ -2,7 +2,7 @@
<div class="navbar-left">
<!-- the logo -->
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/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}}
+6
View File
@@ -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>
+2 -2
View File
@@ -82,7 +82,7 @@
<div class="ui container">
<overflow-menu class="ui secondary pointing menu">
{{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}}
<div class="overflow-menu-items">
<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"}}
+6
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
<div class="ui container fluid">
<div class="tw-text-center tw-mb-4">
<img src="{{AssetUrlPrefix}}/img/login-logo.png" 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" .}}
+6
View File
@@ -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)
})
}
+167
View File
@@ -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; }
}
+6 -3
View File
@@ -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;
+140
View File
@@ -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);
}
+415
View File
@@ -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 (768991px): admin/user-settings wide tables are now wrapped in
Bootstrap `.table-responsive` in their templates, which scroll-contains them
at every width — so the previous hand-rolled `.admin table.ui.table` /
`.user.settings table.ui.table { overflow-x }` block was REMOVED here as
redundant. Nothing to add; Bootstrap owns this case. */
/* ==========================================================================
* SECTION 4 — SETTINGS PAGES (repo/user/org/admin left sidebar)
* --------------------------------------------------------------------------
* Bootstrap-powered: each settings navbar.tmpl now renders a `.d-md-none`
* trigger button + a Bootstrap `.collapse.d-md-block` drawer wrapping the
* existing `.ui.fluid.vertical.menu`. Bootstrap's own CSS/JS drive the show/hide:
* - < md (768px): `.collapse` is display:none until the button toggles it,
* then the FULL Fomantic vertical menu drops down as a tap-friendly drawer.
* - >= md: `.d-md-block` forces the drawer visible and the `.d-md-none` button
* is hidden, so desktop keeps the untouched Fomantic vertical sidebar.
* flexcontainer.css already stacks .flex-container to a column below 768px and
* puts .flex-container-nav above the content (order:-1), so the drawer opens in
* the right place. This file only adds trigger chrome + FAB-safe spacing +
* dark-mode polish — no display logic (Bootstrap owns that).
*
* Verified markup (templates/{repo,user,org,admin}/{settings,}/navbar.tmpl):
* .flex-container-nav
* > button.d-md-none.mg-settings-nav-toggle[data-bs-toggle=collapse]
* > .collapse.d-md-block.mg-settings-nav-collapse#mg-settings-nav
* > .ui.fluid.vertical.menu > .header.item | a.item | details.toggleable-item
* ========================================================================== */
@media (max-width: 767.98px) {
/* Trigger button: full-width, comfortable tap height, spaced from the menu it
reveals. The trailing chevron rotates when the drawer is open. */
.flex-container-nav .mg-settings-nav-toggle {
min-height: var(--mg-touch-target);
justify-content: space-between;
margin-bottom: 8px;
}
.flex-container-nav .mg-settings-nav-toggle .svg.octicon-chevron-down {
transition: transform 0.15s ease;
}
.flex-container-nav .mg-settings-nav-toggle[aria-expanded="true"] .svg.octicon-chevron-down {
transform: rotate(180deg);
}
/* The drawer is a normal full-width stacked vertical menu (the Fomantic
default). Give it a little breathing room from the page content and make
each item a 44px tap target. The redundant header.item is dropped since the
trigger button already names the section. */
.flex-container-nav .mg-settings-nav-collapse .ui.vertical.menu {
width: 100% !important;
margin-top: 0;
}
.flex-container-nav .mg-settings-nav-collapse .ui.vertical.menu > .header.item {
display: none;
}
.flex-container-nav .mg-settings-nav-collapse .ui.vertical.menu > .item,
.flex-container-nav .mg-settings-nav-collapse .ui.vertical.menu .menu > .item {
min-height: var(--mg-touch-target);
display: flex;
align-items: center;
}
/* Keep the whole drawer clear of the fixed FAB (bottom-right) so the last
item is never hidden behind it. */
.flex-container-nav .mg-settings-nav-collapse {
margin-bottom: 72px;
}
}
/* ==========================================================================
* SECTION 5 — TOUCH TARGETS (≥ 44×44px)
* --------------------------------------------------------------------------
* Ensure interactive controls meet the 44px minimum tap size on touch. Scoped
* with a coarse-pointer query where appropriate so desktop density is
* unaffected, plus a width-based fallback for phones without pointer support
* reporting.
*
* Verified selectors: .ui.button (button.css), .ui.pagination.menu .item and
* .ui.tabular.menu .item / .ui.menu .item (menu.css), .fold-file / .btn
* (diff toolbar buttons), overflow-menu items (base.css).
* ========================================================================== */
@media (max-width: 767.98px) {
/* Buttons: guarantee a comfortable tap height without distorting layout. */
.ui.button,
.ui.buttons > .button {
min-height: var(--mg-touch-target);
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Icon-only / tiny buttons (diff toolbar fold, copy, kebab) — pad the hit
area out to 44px even though the glyph stays small. */
.ui.tiny.button,
.ui.mini.button,
.btn.fold-file,
.diff-file-header .btn,
.repository .diff-file-header-actions .ui.compact.icon.buttons .button {
min-width: var(--mg-touch-target);
min-height: var(--mg-touch-target);
}
/* Pagination: on mobile menu.css hides all but active + prev/next; make the
survivors full 44px tap targets. */
.ui.pagination.menu .item {
min-width: var(--mg-touch-target);
min-height: var(--mg-touch-target);
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Tab selectors and generic menu items (repo sub-nav, settings rail, tabular
tabs) get a 44px minimum so they're easy to hit. */
.ui.tabular.menu .item,
.ui.secondary.pointing.menu .item,
overflow-menu .overflow-menu-items .item,
.ui.menu:not(.pagination) .item {
min-height: var(--mg-touch-target);
display: inline-flex;
align-items: center;
}
/* The overflow-menu popup button (repo sub-nav "more") also needs a real
tap size; base.css sizes it 32px wide for desktop. */
overflow-menu .overflow-menu-button {
min-width: var(--mg-touch-target);
min-height: var(--mg-touch-target);
}
}
/* Fine-tune purely for touch devices regardless of width: checkboxes and small
links in lists benefit from a padded hit area on any touch screen. */
@media (pointer: coarse) {
.issue-checkbox,
.flex-divided-list .item input[type="checkbox"] {
min-width: 20px;
min-height: 20px;
}
/* Anchor-style menu/list actions get a minimum tap height on coarse pointers. */
.ui.list .item > a,
.repository .ui.tabs .item {
min-height: var(--mg-touch-target);
}
}
+25 -22
View File
@@ -213,12 +213,12 @@ gitea-theme-meta-info {
--color-box-header: #151b22;
--color-box-body: #10151b;
--color-box-body-highlight: #151b22;
--color-text-dark: #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;
}
+53 -45
View File
@@ -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;
}
+37 -13
View File
@@ -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);
}
+62
View File
@@ -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);
}
}
+142
View File
@@ -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);
}
}
}
+95 -15
View File
@@ -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
View File
@@ -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.
+11 -2
View File
@@ -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();
+9
View File
@@ -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
View File
@@ -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,