refactor(miniapp): unify settings design with editable-list pattern

This commit is contained in:
2026-03-15 03:48:40 +04:00
parent ce082b0a31
commit 594c370677
3 changed files with 144 additions and 218 deletions

View File

@@ -1455,18 +1455,20 @@ a {
gap: var(--spacing-md); gap: var(--spacing-md);
} }
.ledger-actions { /* ── Editable List (used in Ledger, Settings, etc.) ───── */
.editable-list-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
padding-bottom: var(--spacing-sm); padding-bottom: var(--spacing-sm);
} }
.ledger-list { .editable-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.ledger-entry { .editable-list-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -1484,48 +1486,48 @@ a {
transition: background var(--transition-fast); transition: background var(--transition-fast);
} }
.ledger-entry:hover:not(:disabled) { .editable-list-row:hover:not(:disabled) {
background: var(--bg-input); background: var(--bg-input);
} }
.ledger-entry:disabled { .editable-list-row:disabled {
cursor: default; cursor: default;
} }
.ledger-entry:last-child { .editable-list-row:last-child {
border-bottom: 0; border-bottom: 0;
} }
.ledger-entry__main { .editable-list-row__main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1px; gap: 1px;
} }
.ledger-entry__title { .editable-list-row__title {
font-size: var(--text-sm); font-size: var(--text-sm);
font-weight: 500; font-weight: 500;
color: var(--text-primary); color: var(--text-primary);
} }
.ledger-entry__actor { .editable-list-row__subtitle {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-muted); color: var(--text-muted);
} }
.ledger-entry__amounts { .editable-list-row__meta {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 1px; gap: 1px;
} }
.ledger-entry__amounts strong { .editable-list-row__meta strong {
font-size: var(--text-sm); font-size: var(--text-sm);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.ledger-entry__secondary { .editable-list-row__secondary {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-muted); color: var(--text-muted);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
@@ -1617,85 +1619,7 @@ a {
color: var(--text-muted); color: var(--text-muted);
} }
/* ── Members ──────────────────────────────────────────── */ /* ── Settings Route ───────────────────────────────────── */
.members-list,
.pending-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.member-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.member-row.interactive {
margin: 0 calc(var(--spacing-lg) * -1);
padding: var(--spacing-sm) var(--spacing-lg);
cursor: pointer;
transition: background var(--transition-fast);
}
.member-row.interactive:hover {
background: var(--bg-input);
}
.member-row__info {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.member-row__info strong {
font-size: var(--text-sm);
}
.member-row__badges {
display: flex;
gap: var(--spacing-xs);
}
.member-row__weight {
font-size: var(--text-xs);
color: var(--text-muted);
}
.pending-member-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.pending-member-row__handle {
font-size: var(--text-xs);
color: var(--text-muted);
display: block;
}
.pending-member-actions {
display: flex;
gap: var(--spacing-sm);
}
/* ── Topics ───────────────────────────────────────────── */
.topics-list {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.topic-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-xs) 0;
font-size: var(--text-sm);
color: var(--text-secondary);
}
/* ── Testing Card ─────────────────────────────────────── */ /* ── Testing Card ─────────────────────────────────────── */

View File

@@ -571,7 +571,7 @@ export default function LedgerRoute() {
defaultOpen defaultOpen
> >
<Show when={effectiveIsAdmin()}> <Show when={effectiveIsAdmin()}>
<div class="ledger-actions"> <div class="editable-list-actions">
<Button <Button
variant="primary" variant="primary"
size="sm" size="sm"
@@ -603,23 +603,25 @@ export default function LedgerRoute() {
when={purchaseLedger().length > 0} when={purchaseLedger().length > 0}
fallback={<p class="empty-state">{copy().purchasesEmpty}</p>} fallback={<p class="empty-state">{copy().purchasesEmpty}</p>}
> >
<div class="ledger-list"> <div class="editable-list">
<For each={purchaseLedger()}> <For each={purchaseLedger()}>
{(entry) => ( {(entry) => (
<button <button
class="ledger-entry" class="editable-list-row"
onClick={() => effectiveIsAdmin() && openPurchaseEditor(entry)} onClick={() => effectiveIsAdmin() && openPurchaseEditor(entry)}
disabled={!effectiveIsAdmin()} disabled={!effectiveIsAdmin()}
> >
<div class="ledger-entry__main"> <div class="editable-list-row__main">
<span class="ledger-entry__title">{entry.title}</span> <span class="editable-list-row__title">{entry.title}</span>
<span class="ledger-entry__actor">{entry.actorDisplayName}</span> <span class="editable-list-row__subtitle">
{entry.actorDisplayName}
</span>
</div> </div>
<div class="ledger-entry__amounts"> <div class="editable-list-row__meta">
<strong>{ledgerPrimaryAmount(entry)}</strong> <strong>{ledgerPrimaryAmount(entry)}</strong>
<Show when={ledgerSecondaryAmount(entry)}> <Show when={ledgerSecondaryAmount(entry)}>
{(secondary) => ( {(secondary) => (
<span class="ledger-entry__secondary">{secondary()}</span> <span class="editable-list-row__secondary">{secondary()}</span>
)} )}
</Show> </Show>
</div> </div>
@@ -633,7 +635,7 @@ export default function LedgerRoute() {
{/* ── Utility bills ──────────────────────── */} {/* ── Utility bills ──────────────────────── */}
<Collapsible title={copy().utilityLedgerTitle}> <Collapsible title={copy().utilityLedgerTitle}>
<Show when={effectiveIsAdmin()}> <Show when={effectiveIsAdmin()}>
<div class="ledger-actions"> <div class="editable-list-actions">
<Button variant="primary" size="sm" onClick={() => setAddUtilityOpen(true)}> <Button variant="primary" size="sm" onClick={() => setAddUtilityOpen(true)}>
<Plus size={14} /> <Plus size={14} />
{copy().addUtilityBillAction} {copy().addUtilityBillAction}
@@ -644,19 +646,21 @@ export default function LedgerRoute() {
when={utilityLedger().length > 0} when={utilityLedger().length > 0}
fallback={<p class="empty-state">{copy().utilityLedgerEmpty}</p>} fallback={<p class="empty-state">{copy().utilityLedgerEmpty}</p>}
> >
<div class="ledger-list"> <div class="editable-list">
<For each={utilityLedger()}> <For each={utilityLedger()}>
{(entry) => ( {(entry) => (
<button <button
class="ledger-entry" class="editable-list-row"
onClick={() => effectiveIsAdmin() && openUtilityEditor(entry)} onClick={() => effectiveIsAdmin() && openUtilityEditor(entry)}
disabled={!effectiveIsAdmin()} disabled={!effectiveIsAdmin()}
> >
<div class="ledger-entry__main"> <div class="editable-list-row__main">
<span class="ledger-entry__title">{entry.title}</span> <span class="editable-list-row__title">{entry.title}</span>
<span class="ledger-entry__actor">{entry.actorDisplayName}</span> <span class="editable-list-row__subtitle">
{entry.actorDisplayName}
</span>
</div> </div>
<div class="ledger-entry__amounts"> <div class="editable-list-row__meta">
<strong>{ledgerPrimaryAmount(entry)}</strong> <strong>{ledgerPrimaryAmount(entry)}</strong>
</div> </div>
</button> </button>
@@ -674,7 +678,7 @@ export default function LedgerRoute() {
: {})} : {})}
> >
<Show when={effectiveIsAdmin()}> <Show when={effectiveIsAdmin()}>
<div class="ledger-actions"> <div class="editable-list-actions">
<Button variant="primary" size="sm" onClick={() => setAddPaymentOpen(true)}> <Button variant="primary" size="sm" onClick={() => setAddPaymentOpen(true)}>
<Plus size={14} /> <Plus size={14} />
{copy().paymentsAddAction} {copy().paymentsAddAction}
@@ -685,23 +689,25 @@ export default function LedgerRoute() {
when={paymentLedger().length > 0} when={paymentLedger().length > 0}
fallback={<p class="empty-state">{copy().paymentsEmpty}</p>} fallback={<p class="empty-state">{copy().paymentsEmpty}</p>}
> >
<div class="ledger-list"> <div class="editable-list">
<For each={paymentLedger()}> <For each={paymentLedger()}>
{(entry) => ( {(entry) => (
<button <button
class="ledger-entry" class="editable-list-row"
onClick={() => effectiveIsAdmin() && openPaymentEditor(entry)} onClick={() => effectiveIsAdmin() && openPaymentEditor(entry)}
disabled={!effectiveIsAdmin()} disabled={!effectiveIsAdmin()}
> >
<div class="ledger-entry__main"> <div class="editable-list-row__main">
<span class="ledger-entry__title"> <span class="editable-list-row__title">
{entry.paymentKind === 'rent' {entry.paymentKind === 'rent'
? copy().paymentLedgerRent ? copy().paymentLedgerRent
: copy().paymentLedgerUtilities} : copy().paymentLedgerUtilities}
</span> </span>
<span class="ledger-entry__actor">{entry.actorDisplayName}</span> <span class="editable-list-row__subtitle">
{entry.actorDisplayName}
</span>
</div> </div>
<div class="ledger-entry__amounts"> <div class="editable-list-row__meta">
<strong>{ledgerPrimaryAmount(entry)}</strong> <strong>{ledgerPrimaryAmount(entry)}</strong>
</div> </div>
</button> </button>

View File

@@ -1,5 +1,5 @@
import { Show, For, createSignal } from 'solid-js' import { Show, For, createSignal } from 'solid-js'
import { ArrowLeft, Globe, User } from 'lucide-solid' import { ArrowLeft, Globe, Plus, User } from 'lucide-solid'
import { useNavigate } from '@solidjs/router' import { useNavigate } from '@solidjs/router'
import { useSession } from '../contexts/session-context' import { useSession } from '../contexts/session-context'
@@ -366,6 +366,42 @@ export default function SettingsRoute() {
</Card> </Card>
</Collapsible> </Collapsible>
{/* Utility Categories */}
<Collapsible title={copy().utilityCategoriesTitle} body={copy().utilityCategoriesBody}>
<div class="editable-list-actions">
<Button variant="primary" size="sm" onClick={() => openAddCategory()}>
<Plus size={14} />
{copy().addCategoryAction}
</Button>
</div>
<Show
when={adminSettings()?.categories}
fallback={<p class="empty-state">{copy().utilityCategoriesBody}</p>}
>
{(categories) => (
<Show
when={categories().length > 0}
fallback={<p class="empty-state">{copy().utilityCategoriesBody}</p>}
>
<div class="editable-list">
<For each={categories()}>
{(category) => (
<button class="editable-list-row" onClick={() => openEditCategory(category)}>
<div class="editable-list-row__main">
<span class="editable-list-row__title">{category.name}</span>
</div>
<Badge variant={category.isActive ? 'accent' : 'muted'}>
{category.isActive ? copy().onLabel : copy().offLabel}
</Badge>
</button>
)}
</For>
</div>
</Show>
)}
</Show>
</Collapsible>
{/* Billing cycle */} {/* Billing cycle */}
<Collapsible title={copy().billingCycleTitle}> <Collapsible title={copy().billingCycleTitle}>
<Card> <Card>
@@ -395,20 +431,19 @@ export default function SettingsRoute() {
when={pendingMembers().length > 0} when={pendingMembers().length > 0}
fallback={<p class="empty-state">{copy().pendingMembersEmpty}</p>} fallback={<p class="empty-state">{copy().pendingMembersEmpty}</p>}
> >
<div class="pending-list"> <div class="editable-list">
<For each={pendingMembers()}> <For each={pendingMembers()}>
{(member) => ( {(member) => (
<Card> <div class="editable-list-row">
<div class="pending-member-row"> <div class="editable-list-row__main">
<div> <span class="editable-list-row__title">{member.displayName}</span>
<strong>{member.displayName}</strong>
<Show when={member.username}> <Show when={member.username}>
{(username) => ( {(username) => (
<span class="pending-member-row__handle">@{username()}</span> <span class="editable-list-row__subtitle">@{username()}</span>
)} )}
</Show> </Show>
</div> </div>
<div class="pending-member-actions"> <div class="editable-list-row__meta">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -433,7 +468,6 @@ export default function SettingsRoute() {
</Button> </Button>
</div> </div>
</div> </div>
</Card>
)} )}
</For> </For>
</div> </div>
@@ -444,18 +478,17 @@ export default function SettingsRoute() {
<Collapsible title={copy().houseSectionMembers} body={copy().membersBody}> <Collapsible title={copy().houseSectionMembers} body={copy().membersBody}>
<Show when={adminSettings()?.members}> <Show when={adminSettings()?.members}>
{(members) => ( {(members) => (
<div class="members-list"> <div class="editable-list">
<For each={members()}> <For each={members()}>
{(member) => ( {(member) => (
<Card> <button class="editable-list-row" onClick={() => openEditMember(member)}>
<div <div class="editable-list-row__main">
class="member-row interactive" <span class="editable-list-row__title">{member.displayName}</span>
style={{ cursor: 'pointer' }} <span class="editable-list-row__subtitle">
onClick={() => openEditMember(member)} {copy().rentWeightLabel}: {member.rentShareWeight}
> </span>
<div class="member-row__info"> </div>
<strong>{member.displayName}</strong> <div class="editable-list-row__meta">
<div class="member-row__badges">
<Badge variant={member.isAdmin ? 'accent' : 'muted'}> <Badge variant={member.isAdmin ? 'accent' : 'muted'}>
{member.isAdmin ? copy().adminTag : copy().residentTag} {member.isAdmin ? copy().adminTag : copy().residentTag}
</Badge> </Badge>
@@ -467,14 +500,7 @@ export default function SettingsRoute() {
: copy().memberStatusLeft} : copy().memberStatusLeft}
</Badge> </Badge>
</div> </div>
</div> </button>
<div class="member-row__weight">
<span>
{copy().rentWeightLabel}: {member.rentShareWeight}
</span>
</div>
</div>
</Card>
)} )}
</For> </For>
</div> </div>
@@ -486,7 +512,7 @@ export default function SettingsRoute() {
<Collapsible title={copy().houseSectionTopics} body={copy().topicBindingsBody}> <Collapsible title={copy().houseSectionTopics} body={copy().topicBindingsBody}>
<Show when={adminSettings()?.topics}> <Show when={adminSettings()?.topics}>
{(topics) => ( {(topics) => (
<div class="topics-list"> <div class="editable-list">
<For each={topics()}> <For each={topics()}>
{(topic) => { {(topic) => {
const roleLabel = () => { const roleLabel = () => {
@@ -500,12 +526,16 @@ export default function SettingsRoute() {
return labels[topic.role] ?? topic.role return labels[topic.role] ?? topic.role
} }
return ( return (
<div class="topic-row"> <div class="editable-list-row">
<span>{roleLabel()}</span> <div class="editable-list-row__main">
<span class="editable-list-row__title">{roleLabel()}</span>
</div>
<div class="editable-list-row__meta">
<Badge variant={topic.telegramThreadId ? 'accent' : 'muted'}> <Badge variant={topic.telegramThreadId ? 'accent' : 'muted'}>
{topic.telegramThreadId ? copy().topicBound : copy().topicUnbound} {topic.telegramThreadId ? copy().topicBound : copy().topicUnbound}
</Badge> </Badge>
</div> </div>
</div>
) )
}} }}
</For> </For>
@@ -513,40 +543,6 @@ export default function SettingsRoute() {
)} )}
</Show> </Show>
</Collapsible> </Collapsible>
{/* Utility Categories */}
<Collapsible title={copy().utilityCategoriesTitle} body={copy().utilityCategoriesBody}>
<Show
when={adminSettings()?.categories}
fallback={<p class="empty-state">{copy().utilityCategoriesBody}</p>}
>
{(categories) => (
<div class="categories-list">
<For each={categories()}>
{(category) => (
<Card>
<div
class="category-row interactive"
style={{ cursor: 'pointer' }}
onClick={() => openEditCategory(category)}
>
<div class="category-row__info">
<strong>{category.name}</strong>
<Badge variant={category.isActive ? 'accent' : 'muted'}>
{category.isActive ? copy().onLabel : copy().offLabel}
</Badge>
</div>
</div>
</Card>
)}
</For>
<Button variant="secondary" size="sm" onClick={() => openAddCategory()}>
{copy().addCategoryAction}
</Button>
</div>
)}
</Show>
</Collapsible>
</Show> </Show>
{/* ── Billing Settings Editor Modal ────────────── */} {/* ── Billing Settings Editor Modal ────────────── */}