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)"
|
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 {
|
"varColor": func(planned, actual int64) string {
|
||||||
if planned == 0 {
|
if planned == 0 {
|
||||||
return "var(--text2)"
|
return "var(--text2)"
|
||||||
|
|||||||
@ -229,11 +229,16 @@ func (h *Handler) OrgTeamCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "name required", http.StatusBadRequest)
|
http.Error(w, "name required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
avatar := r.FormValue("avatar")
|
||||||
|
if avatar == "" {
|
||||||
|
avatar = "👥"
|
||||||
|
}
|
||||||
team := &OrgTeam{
|
team := &OrgTeam{
|
||||||
ID: bson.NewObjectID().Hex(),
|
ID: bson.NewObjectID().Hex(),
|
||||||
OrgID: org.ID,
|
OrgID: org.ID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: teamType,
|
Type: teamType,
|
||||||
|
Avatar: avatar,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
if err := h.store.createTeam(ctx, team); err != nil {
|
if err := h.store.createTeam(ctx, team); err != nil {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ type OrgTeam struct {
|
|||||||
OrgID string `bson:"org_id" json:"org_id"`
|
OrgID string `bson:"org_id" json:"org_id"`
|
||||||
Name string `bson:"name" json:"name"`
|
Name string `bson:"name" json:"name"`
|
||||||
Type TeamType `bson:"type" json:"type"`
|
Type TeamType `bson:"type" json:"type"`
|
||||||
|
Avatar string `bson:"avatar" json:"avatar"` // single emoji
|
||||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -396,11 +396,67 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
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; }
|
input[type="color"] { padding: 4px; height: 38px; cursor: pointer; }
|
||||||
select option,
|
|
||||||
.form-input option { background: var(--bg2); color: var(--text); }
|
.form-input option { background: var(--bg2); color: var(--text); }
|
||||||
textarea.form-input { resize: vertical; min-height: 72px; line-height: 1.5; }
|
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 ──────────────────────────────────────────────────────── */
|
/* ── Badges ──────────────────────────────────────────────────────── */
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-flex; align-items: center; gap: 5px;
|
display: inline-flex; align-items: center; gap: 5px;
|
||||||
|
|||||||
@ -118,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">TEAMS</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>
|
||||||
</div>
|
</div>
|
||||||
{{if $d.Event.Description}}
|
{{if $d.Event.Description}}
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight:600;">{{.Event.Name}}</td>
|
<td style="font-weight:600;">{{.Event.Name}}</td>
|
||||||
<td style="font-size:12px; color:var(--text3);">
|
<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>
|
||||||
<td style="font-size:12px; color:var(--text2);">{{dateShort .Event.DateStart}} — {{dateShort .Event.DateEnd}}</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>
|
<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>
|
<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}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td style="font-size:12px; color:var(--text2);">
|
<td style="font-size:12px;">
|
||||||
{{if $m.TeamIDs}}
|
{{if $m.TeamIDs}}
|
||||||
{{range $i, $tid := $m.TeamIDs}}
|
<div style="display:flex; flex-wrap:wrap; gap:4px;">
|
||||||
{{range $d.Teams}}{{if eq .ID $tid}}{{if $i}}, {{end}}{{.Name}}{{end}}{{end}}
|
{{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}}
|
{{end}}
|
||||||
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span style="color:var(--text3);">all teams</span>
|
<span style="color:var(--text3);">all teams</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@ -78,7 +78,7 @@
|
|||||||
<div style="font-size:12px; color:var(--text3);">{{dateShort $ev.DateStart}} — {{dateShort $ev.DateEnd}}</div>
|
<div style="font-size:12px; color:var(--text3);">{{dateShort $ev.DateStart}} — {{dateShort $ev.DateEnd}}</div>
|
||||||
{{if .Teams}}
|
{{if .Teams}}
|
||||||
<div style="margin-top:6px;">
|
<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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Team</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Members</th>
|
<th>Members</th>
|
||||||
{{if eq $d.MyRole "admin"}}<th></th>{{end}}
|
{{if eq $d.MyRole "admin"}}<th></th>{{end}}
|
||||||
@ -30,7 +30,12 @@
|
|||||||
{{$count := 0}}
|
{{$count := 0}}
|
||||||
{{range $d.Members}}{{range .TeamIDs}}{{if eq . $t.ID}}{{$count = add $count 1}}{{end}}{{end}}{{end}}
|
{{range $d.Members}}{{range .TeamIDs}}{{if eq . $t.ID}}{{$count = add $count 1}}{{end}}{{end}}{{end}}
|
||||||
<tr>
|
<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>
|
<td>
|
||||||
{{if eq (print $t.Type) "guest"}}
|
{{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>
|
<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}}
|
{{end}}
|
||||||
|
|
||||||
{{if eq $d.MyRole "admin"}}
|
{{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 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:400px; margin:16px;">
|
<div class="card" style="width:100%; max-width:420px;">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||||
<h2>New team</h2>
|
<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>
|
<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>
|
</div>
|
||||||
<form method="post" action="/orgs/{{$d.Org.Slug}}/teams">
|
<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;">
|
<div style="margin-bottom:14px;">
|
||||||
<label class="form-label">Team name</label>
|
<label class="form-label">Team name</label>
|
||||||
<input class="form-input" type="text" name="name" placeholder="Marketing" required autofocus>
|
<input class="form-input" type="text" name="name" placeholder="Marketing" required autofocus>
|
||||||
@ -86,5 +105,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user