diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 460ecda..6915d73 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -103,46 +103,7 @@ var basePage = template.Must(template.New("base").Parse(` {{.Title}} — Gitea Mobile - + diff --git a/internal/templates/.gitkeep b/internal/templates/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/internal/templates/create_issue.html b/internal/templates/create_issue.html new file mode 100644 index 0000000..ac1c6f7 --- /dev/null +++ b/internal/templates/create_issue.html @@ -0,0 +1,42 @@ +{{define "content"}} +

Create Issue

+ +
+
+ + + + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +{{end}} diff --git a/internal/templates/dashboard.html b/internal/templates/dashboard.html new file mode 100644 index 0000000..102c008 --- /dev/null +++ b/internal/templates/dashboard.html @@ -0,0 +1,26 @@ +{{define "content"}} +

Dashboard

+ +{{if .Error}} +

{{.Error}}

+{{else if not .Items}} +

No items need attention. Nice work!

+{{else}} +
+ {{range .Items}} +
+
+ {{if eq .Type "pull"}}PR{{else}}issue{{end}} + {{.Title}} +
+
+ {{.RepoOwner}}/{{.RepoName}} #{{.Number}} + {{range .Labels}} + {{.}} + {{end}} +
+
+ {{end}} +
+{{end}} +{{end}} diff --git a/internal/templates/issue_detail.html b/internal/templates/issue_detail.html new file mode 100644 index 0000000..5ec2b62 --- /dev/null +++ b/internal/templates/issue_detail.html @@ -0,0 +1,43 @@ +{{define "content"}} +

{{.Issue.Title}}

+ +
+
+ {{.Issue.State}} + {{.Issue.RepoOwner}}/{{.Issue.RepoName}} #{{.Issue.Number}} + {{range .Issue.Labels}} + {{.Name}} + {{end}} +
+ {{if .Issue.Body}} +
{{.Issue.Body}}
+ {{end}} +
+ +{{if .Comments}} +

Comments

+{{range .Comments}} +
+
+ {{.User}} + {{.CreatedAt}} +
+
{{.Body}}
+
+{{end}} +{{end}} + +
+

Actions

+
+
+ + +
+
+
+{{end}} diff --git a/internal/templates/issues.html b/internal/templates/issues.html new file mode 100644 index 0000000..49451ac --- /dev/null +++ b/internal/templates/issues.html @@ -0,0 +1,44 @@ +{{define "content"}} +

Issues

+ +
+ + +
+ +{{if .Error}} +

{{.Error}}

+{{else if not .Issues}} +

No issues found.

+{{else}} +
+ {{range .Issues}} +
+
{{.Title}}
+
+ {{.RepoOwner}}/{{.RepoName}} #{{.Number}} + {{range .Labels}} + {{.Name}} + {{end}} + {{if .Assignee}} + {{.Assignee.Login}} + {{end}} +
+
+ {{end}} + {{if .HasMore}} +
+
+
+ {{end}} +
+{{end}} +{{end}} diff --git a/internal/templates/layout.html b/internal/templates/layout.html new file mode 100644 index 0000000..3acdfb4 --- /dev/null +++ b/internal/templates/layout.html @@ -0,0 +1,45 @@ +{{define "layout"}} + + + + + + + + + + {{.Title}} — Gitea Mobile + + + + +
+ {{template "content" .}} +
+ + + +{{end}} diff --git a/internal/templates/pull_detail.html b/internal/templates/pull_detail.html new file mode 100644 index 0000000..3f4a5b9 --- /dev/null +++ b/internal/templates/pull_detail.html @@ -0,0 +1,47 @@ +{{define "content"}} +

{{.Pull.Title}}

+ +
+
+ PR + {{.Pull.State}} + {{.Pull.RepoOwner}}/{{.Pull.RepoName}} #{{.Pull.Number}} + {{range .Pull.Labels}} + {{.Name}} + {{end}} +
+
+ +{{.Pull.Additions}} + -{{.Pull.Deletions}} + {{if .Pull.Mergeable}}Mergeable{{end}} +
+ {{if .Pull.Body}} +
{{.Pull.Body}}
+ {{end}} +
+ +
+

Submit Review

+
+
+ + +
+
+ + + +
+ +
+
+{{end}} diff --git a/internal/templates/pulls.html b/internal/templates/pulls.html new file mode 100644 index 0000000..762b230 --- /dev/null +++ b/internal/templates/pulls.html @@ -0,0 +1,38 @@ +{{define "content"}} +

Pull Requests

+ +
+ +
+ +{{if .Error}} +

{{.Error}}

+{{else if not .Pulls}} +

No open pull requests found.

+{{else}} +
+ {{range .Pulls}} +
+
+ PR + {{.Title}} +
+
+ {{.RepoOwner}}/{{.RepoName}} #{{.Number}} + {{range .Labels}} + {{.Name}} + {{end}} + +{{.Additions}} + -{{.Deletions}} + {{if .Mergeable}}mergeable{{end}} +
+
+ {{end}} +
+{{end}} +{{end}} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..e867918 --- /dev/null +++ b/static/style.css @@ -0,0 +1,477 @@ +/* Gitea Mobile — Mobile-first CSS (~5KB target) */ + +/* Reset */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +/* CSS Variables */ +:root { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #21262d; + --border: #30363d; + --text-primary: #e6edf3; + --text-secondary: #8b949e; + --text-link: #58a6ff; + --accent-green: #3fb950; + --accent-red: #f85149; + --accent-yellow: #d29922; + --accent-blue: #58a6ff; + --accent-purple: #bc8cff; + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --radius: 8px; + --radius-sm: 6px; + --radius-pill: 10px; + --nav-height: 56px; + --font-sm: 0.75rem; + --font-base: 0.875rem; + --font-lg: 1rem; + --font-xl: 1.25rem; +} + +/* Base */ +html { + font-size: 16px; + -webkit-text-size-adjust: 100%; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; + padding-bottom: calc(var(--nav-height) + env(safe-area-inset-bottom)); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Content area */ +.content { + padding: var(--spacing-lg); + padding-top: max(var(--spacing-lg), env(safe-area-inset-top)); + max-width: 640px; + margin: 0 auto; +} + +/* Typography */ +h1 { + font-size: var(--font-xl); + font-weight: 700; + margin-bottom: var(--spacing-lg); +} + +h2 { + font-size: var(--font-lg); + font-weight: 600; + margin-bottom: var(--spacing-md); +} + +a { + color: var(--text-link); + text-decoration: none; +} + +a:active { + opacity: 0.7; +} + +/* Cards */ +.card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--spacing-md); + margin-bottom: var(--spacing-sm); + transition: background 0.15s ease; +} + +.card:active { + background: var(--bg-tertiary); +} + +.card-title { + font-weight: 600; + font-size: var(--font-base); + margin-bottom: var(--spacing-xs); + line-height: 1.3; +} + +.card-meta { + font-size: var(--font-sm); + color: var(--text-secondary); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-xs); +} + +.card-body { + font-size: var(--font-base); + color: var(--text-secondary); + margin-top: var(--spacing-sm); + line-height: 1.5; +} + +/* Labels / badges */ +.label { + display: inline-block; + font-size: 0.7rem; + padding: 1px 6px; + border-radius: var(--radius-pill); + font-weight: 500; + line-height: 1.5; + white-space: nowrap; +} + +.type-badge { + font-size: 0.65rem; + text-transform: uppercase; + font-weight: 700; + padding: 1px 5px; + border-radius: 4px; + white-space: nowrap; +} + +.type-issue { + background: rgba(31, 111, 235, 0.13); + color: var(--accent-blue); + border: 1px solid rgba(31, 111, 235, 0.27); +} + +.type-pull { + background: rgba(35, 134, 54, 0.13); + color: var(--accent-green); + border: 1px solid rgba(35, 134, 54, 0.27); +} + +.state-open { + color: var(--accent-green); +} + +.state-closed { + color: var(--accent-red); +} + +.state-merged { + color: var(--accent-purple); +} + +/* Priority labels */ +.priority-p1 { color: var(--accent-red); border-color: var(--accent-red); } +.priority-p2 { color: var(--accent-yellow); border-color: var(--accent-yellow); } +.priority-p3 { color: var(--accent-blue); border-color: var(--accent-blue); } + +/* Diff stats */ +.diff-add { color: var(--accent-green); font-size: var(--font-sm); } +.diff-del { color: var(--accent-red); font-size: var(--font-sm); } + +/* Bottom navigation */ +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: var(--bg-secondary); + border-top: 1px solid var(--border); + display: flex; + justify-content: space-around; + align-items: center; + height: var(--nav-height); + padding-bottom: env(safe-area-inset-bottom); + z-index: 100; +} + +.bottom-nav a { + color: var(--text-secondary); + text-decoration: none; + font-size: 0.7rem; + display: flex; + flex-direction: column; + align-items: center; + padding: 4px 0; + min-width: 64px; + -webkit-tap-highlight-color: transparent; +} + +.bottom-nav a.active { + color: var(--accent-blue); +} + +.bottom-nav svg { + width: 22px; + height: 22px; + margin-bottom: 2px; +} + +/* Filter bar */ +.filter-bar { + display: flex; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-lg); + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + padding-bottom: var(--spacing-xs); +} + +.filter-bar::-webkit-scrollbar { + display: none; +} + +.filter-bar select, +.filter-bar input { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--font-base); + min-width: 0; + flex-shrink: 0; + appearance: none; + -webkit-appearance: none; +} + +.filter-bar select:focus, +.filter-bar input:focus { + outline: none; + border-color: var(--accent-blue); +} + +/* Forms */ +.form-group { + margin-bottom: var(--spacing-lg); +} + +.form-group label { + display: block; + font-size: var(--font-sm); + color: var(--text-secondary); + margin-bottom: var(--spacing-sm); +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--font-lg); + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: inherit; +} + +.form-group textarea { + min-height: 120px; + resize: vertical; +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: var(--accent-blue); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--spacing-md) var(--spacing-lg); + font-size: var(--font-lg); + font-weight: 600; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: background 0.15s ease; +} + +.btn-primary { + background: #238636; + color: #fff; + width: 100%; +} + +.btn-primary:active { + background: #2ea043; +} + +.btn-secondary { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border); + width: 100%; +} + +.btn-secondary:active { + background: var(--border); +} + +.btn-danger { + background: #da363322; + color: var(--accent-red); + border: 1px solid #da363366; + width: 100%; +} + +/* Review form */ +.review-options { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-lg); +} + +.review-option { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm); + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; +} + +.review-option input[type="radio"] { + accent-color: var(--accent-blue); +} + +/* Comments thread */ +.comment { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: var(--spacing-sm); + overflow: hidden; +} + +.comment-header { + padding: var(--spacing-sm) var(--spacing-md); + background: var(--bg-tertiary); + font-size: var(--font-sm); + color: var(--text-secondary); + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.comment-body { + padding: var(--spacing-md); + font-size: var(--font-base); + line-height: 1.6; +} + +/* Avatar */ +.avatar { + width: 20px; + height: 20px; + border-radius: 50%; + vertical-align: middle; +} + +/* Empty state */ +.empty { + text-align: center; + color: var(--text-secondary); + padding: 3rem var(--spacing-lg); + font-size: var(--font-base); +} + +/* Messages */ +.message { + padding: var(--spacing-md); + border-radius: var(--radius-sm); + margin-bottom: var(--spacing-lg); + font-size: var(--font-base); +} + +.message.success { + background: #0d2818; + border: 1px solid #238636; + color: var(--accent-green); +} + +.message.error { + background: #2d1117; + border: 1px solid #da3633; + color: var(--accent-red); +} + +.message.info { + background: #0c1d2e; + border: 1px solid #1f6feb; + color: var(--accent-blue); +} + +/* Loading indicator */ +.htmx-indicator { + display: none; +} + +.htmx-request .htmx-indicator { + display: inline-block; +} + +.spinner { + width: 16px; + height: 16px; + border: 2px solid var(--border); + border-top-color: var(--accent-blue); + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Infinite scroll sentinel */ +.scroll-sentinel { + height: 1px; + margin-bottom: var(--spacing-lg); +} + +/* Tablet breakpoint: 2-column grid */ +@media (min-width: 640px) { + .content { + max-width: 960px; + } + + .card-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--spacing-sm); + } + + .card-grid .card { + margin-bottom: 0; + } +} + +/* Respect reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} + +/* Dark mode is default; light mode override if needed */ +@media (prefers-color-scheme: light) { + :root { + --bg-primary: #ffffff; + --bg-secondary: #f6f8fa; + --bg-tertiary: #eaeef2; + --border: #d0d7de; + --text-primary: #1f2328; + --text-secondary: #656d76; + --text-link: #0969da; + } +}