style: global dark form inputs + team emoji avatars (#23)
Form inputs:
- Add a catch-all CSS rule targeting all text-type inputs, selects, and
textareas not already covered by .form-group or .form-input — sets
background:--bg2, color:--text, focus ring. Fixes white-box appearance
in transactions, goals, settings, portfolio, import, people, and tax
templates without touching any HTML.
Team avatars:
- OrgTeam.Avatar string (emoji) — persisted in MongoDB, defaults to 👥
- OrgTeamCreate handler reads "avatar" form field
- org_teams.html: emoji picker (30 options) in new-team modal; preview
updates live; selected emoji highlighted with accent border
- avatarEmojis() and teamAvatar() template functions registered in FuncMap
- Team badges in org_members, org_events, org_event_detail, org_report
all show the emoji inline with the team name
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ceeee2a46a
commit
26a7236494
@ -118,6 +118,19 @@ func parseTmpl(files ...string) *template.Template {
|
||||
return "var(--bg3); color:var(--text3)"
|
||||
}
|
||||
},
|
||||
"avatarEmojis": func() []string {
|
||||
return []string{
|
||||
"👥", "🚀", "⚡", "🎯", "🏆", "💡", "🔥", "🌊", "🎨", "🛠️",
|
||||
"📊", "🎪", "🌿", "🦋", "🏄", "🎸", "🔬", "📡", "🏗️", "🎭",
|
||||
"🌍", "🦁", "🐉", "🦅", "🐋", "🌙", "⭐", "🍀", "🔮", "🎲",
|
||||
}
|
||||
},
|
||||
"teamAvatar": func(t OrgTeam) string {
|
||||
if t.Avatar != "" {
|
||||
return t.Avatar
|
||||
}
|
||||
return "👥"
|
||||
},
|
||||
"varColor": func(planned, actual int64) string {
|
||||
if planned == 0 {
|
||||
return "var(--text2)"
|
||||
|
||||
@ -229,11 +229,16 @@ func (h *Handler) OrgTeamCreate(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
avatar := r.FormValue("avatar")
|
||||
if avatar == "" {
|
||||
avatar = "👥"
|
||||
}
|
||||
team := &OrgTeam{
|
||||
ID: bson.NewObjectID().Hex(),
|
||||
OrgID: org.ID,
|
||||
Name: name,
|
||||
Type: teamType,
|
||||
Avatar: avatar,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := h.store.createTeam(ctx, team); err != nil {
|
||||
|
||||
@ -35,6 +35,7 @@ type OrgTeam struct {
|
||||
OrgID string `bson:"org_id" json:"org_id"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Type TeamType `bson:"type" json:"type"`
|
||||
Avatar string `bson:"avatar" json:"avatar"` // single emoji
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
|
||||
@ -396,11 +396,67 @@
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
/* Global catch-all: any bare input/select/textarea gets dark theme.
|
||||
.form-group and .form-input rules above still win for their elements
|
||||
due to specificity; this only fixes uncovered stragglers. */
|
||||
input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=submit]):not([type=button]):not([type=reset]):not([type=hidden]):not([type=color]),
|
||||
select,
|
||||
textarea {
|
||||
background: var(--bg2);
|
||||
color: var(--text);
|
||||
border-color: var(--border2);
|
||||
font-family: inherit;
|
||||
}
|
||||
input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=submit]):not([type=button]):not([type=reset]):not([type=hidden]):not([type=color]):focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
input::placeholder, textarea::placeholder { color: var(--text3); opacity: 0.7; }
|
||||
select option { background: var(--bg2); color: var(--text); }
|
||||
input[type="color"] { padding: 4px; height: 38px; cursor: pointer; }
|
||||
select option,
|
||||
.form-input option { background: var(--bg2); color: var(--text); }
|
||||
textarea.form-input { resize: vertical; min-height: 72px; line-height: 1.5; }
|
||||
|
||||
/* Team / org avatars */
|
||||
.team-avatar {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 8px;
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg3);
|
||||
border: 1px solid var(--border2);
|
||||
user-select: none;
|
||||
}
|
||||
.team-avatar-sm {
|
||||
width: 22px; height: 22px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.team-avatar-lg {
|
||||
width: 48px; height: 48px;
|
||||
border-radius: 12px;
|
||||
font-size: 26px;
|
||||
}
|
||||
/* Emoji picker row */
|
||||
.emoji-picker { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.emoji-opt {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
background: var(--bg3);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: border-color 0.12s, background 0.12s;
|
||||
}
|
||||
.emoji-opt:hover { background: var(--surface2); }
|
||||
.emoji-opt.selected { border-color: var(--accent); background: var(--accent-glow); }
|
||||
|
||||
/* ── Badges ──────────────────────────────────────────────────────── */
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
|
||||
@ -118,7 +118,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">TEAMS</div>
|
||||
<div>{{if $d.EventTeams}}{{range $d.EventTeams}}<span style="display:inline-block; padding:1px 6px; border-radius:4px; background:var(--bg3); margin-right:3px; font-size:11px;">{{.Name}}</span>{{end}}{{else}}—{{end}}</div>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:4px;">{{if $d.EventTeams}}{{range $d.EventTeams}}<span style="display:inline-flex; align-items:center; gap:3px; padding:2px 8px; border-radius:4px; background:var(--bg3); font-size:11px;"><span style="font-size:13px;">{{if .Avatar}}{{.Avatar}}{{else}}👥{{end}}</span>{{.Name}}</span>{{end}}{{else}}<span style="color:var(--text3);">—</span>{{end}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if $d.Event.Description}}
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
<tr>
|
||||
<td style="font-weight:600;">{{.Event.Name}}</td>
|
||||
<td style="font-size:12px; color:var(--text3);">
|
||||
{{if .Teams}}{{range .Teams}}<span style="display:inline-block; padding:1px 6px; border-radius:4px; background:var(--bg3); margin-right:3px; font-size:11px;">{{.Name}}</span>{{end}}{{else}}—{{end}}
|
||||
{{if .Teams}}{{range .Teams}}<span style="display:inline-flex; align-items:center; gap:3px; padding:2px 7px; border-radius:4px; background:var(--bg3); margin-right:3px; font-size:11px;"><span style="font-size:13px;">{{if .Avatar}}{{.Avatar}}{{else}}👥{{end}}</span>{{.Name}}</span>{{end}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td style="font-size:12px; color:var(--text2);">{{dateShort .Event.DateStart}} — {{dateShort .Event.DateEnd}}</td>
|
||||
<td style="text-align:right; color:var(--green); font-size:13px;">{{cents .TotalIncome}}</td>
|
||||
|
||||
@ -43,11 +43,17 @@
|
||||
<span style="font-size:11px; font-weight:600; padding:3px 9px; border-radius:20px; background:var(--accent-glow); color:var(--accent2);">{{$m.Role}}</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td style="font-size:12px; color:var(--text2);">
|
||||
<td style="font-size:12px;">
|
||||
{{if $m.TeamIDs}}
|
||||
{{range $i, $tid := $m.TeamIDs}}
|
||||
{{range $d.Teams}}{{if eq .ID $tid}}{{if $i}}, {{end}}{{.Name}}{{end}}{{end}}
|
||||
<div style="display:flex; flex-wrap:wrap; gap:4px;">
|
||||
{{range $tid := $m.TeamIDs}}
|
||||
{{range $d.Teams}}{{if eq .ID $tid}}
|
||||
<span style="display:inline-flex; align-items:center; gap:3px; padding:2px 7px; border-radius:4px; background:var(--bg3); color:var(--text2);">
|
||||
<span style="font-size:13px; line-height:1;">{{if .Avatar}}{{.Avatar}}{{else}}👥{{end}}</span>{{.Name}}
|
||||
</span>
|
||||
{{end}}{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<span style="color:var(--text3);">all teams</span>
|
||||
{{end}}
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
<div style="font-size:12px; color:var(--text3);">{{dateShort $ev.DateStart}} — {{dateShort $ev.DateEnd}}</div>
|
||||
{{if .Teams}}
|
||||
<div style="margin-top:6px;">
|
||||
{{range .Teams}}<span style="display:inline-block; padding:1px 6px; border-radius:4px; background:var(--bg3); margin-right:3px; font-size:11px; color:var(--text2);">{{.Name}}</span>{{end}}
|
||||
{{range .Teams}}<span style="display:inline-flex; align-items:center; gap:3px; padding:2px 7px; border-radius:4px; background:var(--bg3); margin-right:3px; font-size:11px; color:var(--text2);"><span style="font-size:13px;">{{if .Avatar}}{{.Avatar}}{{else}}👥{{end}}</span>{{.Name}}</span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Team</th>
|
||||
<th>Type</th>
|
||||
<th>Members</th>
|
||||
{{if eq $d.MyRole "admin"}}<th></th>{{end}}
|
||||
@ -30,7 +30,12 @@
|
||||
{{$count := 0}}
|
||||
{{range $d.Members}}{{range .TeamIDs}}{{if eq . $t.ID}}{{$count = add $count 1}}{{end}}{{end}}{{end}}
|
||||
<tr>
|
||||
<td style="font-weight:600;">{{$t.Name}}</td>
|
||||
<td>
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<span class="team-avatar">{{if $t.Avatar}}{{$t.Avatar}}{{else}}👥{{end}}</span>
|
||||
<span style="font-weight:600;">{{$t.Name}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{if eq (print $t.Type) "guest"}}
|
||||
<span style="font-size:11px; font-weight:600; padding:3px 9px; border-radius:20px; background:rgba(251,191,36,0.1); color:#fbbf24;">guest</span>
|
||||
@ -64,13 +69,27 @@
|
||||
{{end}}
|
||||
|
||||
{{if eq $d.MyRole "admin"}}
|
||||
<div id="new-team-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:500; align-items:center; justify-content:center;">
|
||||
<div class="card" style="width:100%; max-width:400px; margin:16px;">
|
||||
<div id="new-team-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:500; align-items:center; justify-content:center; padding:16px;">
|
||||
<div class="card" style="width:100%; max-width:420px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h2>New team</h2>
|
||||
<button onclick="document.getElementById('new-team-modal').style.display='none'" style="background:none; border:none; color:var(--text3); font-size:20px; cursor:pointer;">×</button>
|
||||
</div>
|
||||
<form method="post" action="/orgs/{{$d.Org.Slug}}/teams">
|
||||
<!-- Avatar picker -->
|
||||
<div style="margin-bottom:16px;">
|
||||
<label class="form-label">Avatar</label>
|
||||
<input type="hidden" name="avatar" id="avatar-val" value="👥">
|
||||
<div style="display:flex; align-items:center; gap:12px; margin-bottom:10px;">
|
||||
<span class="team-avatar team-avatar-lg" id="avatar-preview">👥</span>
|
||||
<span style="font-size:12px; color:var(--text3);">Pick an emoji below</span>
|
||||
</div>
|
||||
<div class="emoji-picker">
|
||||
{{range (avatarEmojis)}}
|
||||
<button type="button" class="emoji-opt" onclick="pickEmoji(this, '{{.}}')">{{.}}</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:14px;">
|
||||
<label class="form-label">Team name</label>
|
||||
<input class="form-input" type="text" name="name" placeholder="Marketing" required autofocus>
|
||||
@ -86,5 +105,13 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function pickEmoji(btn, emoji) {
|
||||
document.querySelectorAll('.emoji-opt').forEach(b => b.classList.remove('selected'));
|
||||
btn.classList.add('selected');
|
||||
document.getElementById('avatar-val').value = emoji;
|
||||
document.getElementById('avatar-preview').textContent = emoji;
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user