refactor(miniapp): compact finance and admin editors

This commit is contained in:
2026-03-11 17:22:48 +04:00
parent 69a914711f
commit e07dfeadf5
4 changed files with 1831 additions and 1112 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,119 @@
import { Show, createEffect, onCleanup, type JSX, type ParentProps } from 'solid-js'
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost' | 'icon'
export function Button(
props: ParentProps<{
type?: 'button' | 'submit' | 'reset'
variant?: ButtonVariant
class?: string
disabled?: boolean
onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
}>
) {
return (
<button
type={props.type ?? 'button'}
class={`ui-button ui-button--${props.variant ?? 'secondary'} ${props.class ?? ''}`.trim()}
disabled={props.disabled}
onClick={props.onClick}
>
{props.children}
</button>
)
}
export function IconButton(
props: ParentProps<{
label: string
class?: string
disabled?: boolean
onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
}>
) {
const maybeClass = props.class ? { class: props.class } : {}
const maybeDisabled = props.disabled !== undefined ? { disabled: props.disabled } : {}
const maybeOnClick = props.onClick ? { onClick: props.onClick } : {}
return (
<Button variant="icon" {...maybeClass} {...maybeDisabled} {...maybeOnClick}>
<span aria-hidden="true">{props.children}</span>
<span class="sr-only">{props.label}</span>
</Button>
)
}
export function Field(
props: ParentProps<{
label: string
hint?: string
wide?: boolean
class?: string
}>
) {
return (
<label
class={`settings-field ${props.wide ? 'settings-field--wide' : ''} ${props.class ?? ''}`.trim()}
>
<span>{props.label}</span>
{props.children}
<Show when={props.hint}>{(hint) => <small>{hint()}</small>}</Show>
</label>
)
}
export function Modal(
props: ParentProps<{
open: boolean
title: string
description?: string
closeLabel: string
footer?: JSX.Element
onClose: () => void
}>
) {
createEffect(() => {
if (!props.open) {
return
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
props.onClose()
}
}
window.addEventListener('keydown', onKeyDown)
onCleanup(() => window.removeEventListener('keydown', onKeyDown))
})
return (
<Show when={props.open}>
<div class="modal-backdrop" onClick={() => props.onClose()}>
<section
class="modal-sheet"
role="dialog"
aria-modal="true"
aria-label={props.title}
onClick={(event) => event.stopPropagation()}
>
<header class="modal-sheet__header">
<div>
<h3>{props.title}</h3>
<Show when={props.description}>{(description) => <p>{description()}</p>}</Show>
</div>
<IconButton label={props.closeLabel} onClick={() => props.onClose()}>
x
</IconButton>
</header>
<div class="modal-sheet__body">{props.children}</div>
<Show when={props.footer}>
{(footer) => <footer class="modal-sheet__footer">{footer()}</footer>}
</Show>
</section>
</div>
</Show>
)
}

View File

@@ -67,6 +67,7 @@ export const dictionary = {
'Purchase contributions will appear here after shared buys are logged.', 'Purchase contributions will appear here after shared buys are logged.',
purchaseShareLabel: 'Share', purchaseShareLabel: 'Share',
purchaseTotalLabel: 'Total shared buys', purchaseTotalLabel: 'Total shared buys',
participantsLabel: 'participants',
purchasesTitle: 'Shared purchases', purchasesTitle: 'Shared purchases',
purchasesEmpty: 'No shared purchases recorded for this cycle yet.', purchasesEmpty: 'No shared purchases recorded for this cycle yet.',
utilityLedgerTitle: 'Utility bills', utilityLedgerTitle: 'Utility bills',
@@ -90,21 +91,28 @@ export const dictionary = {
purchaseSplitEqual: 'Equal split', purchaseSplitEqual: 'Equal split',
purchaseSplitCustom: 'Custom amounts', purchaseSplitCustom: 'Custom amounts',
purchaseParticipantLabel: 'Participates', purchaseParticipantLabel: 'Participates',
participantIncluded: 'Included',
participantExcluded: 'Excluded',
purchaseCustomShareLabel: 'Custom amount', purchaseCustomShareLabel: 'Custom amount',
purchaseEditorBody: 'Review the purchase details and adjust the split only when needed.',
paymentsAdminTitle: 'Payments', paymentsAdminTitle: 'Payments',
paymentsAdminBody: 'Add, fix, or remove payment records for the current cycle.', paymentsAdminBody: 'Add, fix, or remove payment records for the current cycle.',
paymentsAddAction: 'Add payment', paymentsAddAction: 'Add payment',
addingPayment: 'Adding payment…', addingPayment: 'Adding payment…',
paymentCreateBody: 'Create a payment record in a focused editor instead of a long inline form.',
paymentKind: 'Payment kind', paymentKind: 'Payment kind',
paymentAmount: 'Payment amount', paymentAmount: 'Payment amount',
paymentMember: 'Member', paymentMember: 'Member',
paymentSaveAction: 'Save payment', paymentSaveAction: 'Save payment',
paymentDeleteAction: 'Delete payment', paymentDeleteAction: 'Delete payment',
paymentEditorBody: 'Review the payment record in one focused editor.',
deletingPayment: 'Deleting payment…', deletingPayment: 'Deleting payment…',
purchaseSaveAction: 'Save purchase', purchaseSaveAction: 'Save purchase',
purchaseDeleteAction: 'Delete purchase', purchaseDeleteAction: 'Delete purchase',
deletingPurchase: 'Deleting purchase…', deletingPurchase: 'Deleting purchase…',
savingPurchase: 'Saving purchase…', savingPurchase: 'Saving purchase…',
editEntryAction: 'Edit entry',
closeEditorAction: 'Close',
householdSettingsTitle: 'Household settings', householdSettingsTitle: 'Household settings',
householdSettingsBody: 'Control household defaults and approve roommates who requested access.', householdSettingsBody: 'Control household defaults and approve roommates who requested access.',
topicBindingsTitle: 'Topic bindings', topicBindingsTitle: 'Topic bindings',
@@ -127,6 +135,9 @@ export const dictionary = {
billingCycleStatus: 'Current cycle currency: {currency}', billingCycleStatus: 'Current cycle currency: {currency}',
billingCycleOpenHint: 'Open a cycle before entering rent and utility bills.', billingCycleOpenHint: 'Open a cycle before entering rent and utility bills.',
billingCyclePeriod: 'Cycle period', billingCyclePeriod: 'Cycle period',
manageCycleAction: 'Manage cycle',
cycleEditorBody:
'Keep the billing cycle controls in one focused editor instead of a long page.',
openCycleAction: 'Open cycle', openCycleAction: 'Open cycle',
openingCycle: 'Opening cycle…', openingCycle: 'Opening cycle…',
closeCycleAction: 'Close cycle', closeCycleAction: 'Close cycle',
@@ -147,19 +158,31 @@ export const dictionary = {
utilitiesDueDay: 'Utilities due day', utilitiesDueDay: 'Utilities due day',
utilitiesReminderDay: 'Utilities reminder day', utilitiesReminderDay: 'Utilities reminder day',
timezone: 'Timezone', timezone: 'Timezone',
manageSettingsAction: 'Manage settings',
billingSettingsEditorBody: 'Household billing defaults live here when you need to change them.',
saveSettingsAction: 'Save settings', saveSettingsAction: 'Save settings',
savingSettings: 'Saving settings…', savingSettings: 'Saving settings…',
utilityCategoriesTitle: 'Utility categories', utilityCategoriesTitle: 'Utility categories',
utilityCategoriesBody: 'Manage the categories admins use for monthly utility entry.', utilityCategoriesBody: 'Manage the categories admins use for monthly utility entry.',
utilityCategoryName: 'Category name', utilityCategoryName: 'Category name',
utilityCategoryActive: 'Active', utilityCategoryActive: 'Active',
utilityBillsEditorBody:
'Keep utility bills short in the list and edit the details only when needed.',
utilityBillCreateBody: 'Add a utility bill in a focused editor.',
utilityBillEditorBody: 'Adjust utility bill details here.',
editUtilityBillAction: 'Edit utility bill',
addCategoryAction: 'Add category', addCategoryAction: 'Add category',
saveCategoryAction: 'Save category', saveCategoryAction: 'Save category',
savingCategory: 'Saving…', savingCategory: 'Saving…',
categoryCreateBody: 'Create a new utility category without stretching the page.',
categoryEditorBody: 'Rename or disable the category in a focused editor.',
editCategoryAction: 'Edit category',
adminsTitle: 'Admins', adminsTitle: 'Admins',
adminsBody: 'Promote trusted household members so they can manage billing and approvals.', adminsBody: 'Promote trusted household members so they can manage billing and approvals.',
displayNameLabel: 'Household display name', displayNameLabel: 'Household display name',
displayNameHint: 'This name appears in balances, ledger entries, and assistant replies.', displayNameHint: 'This name appears in balances, ledger entries, and assistant replies.',
memberEditorBody: 'Member billing state and admin controls stay grouped in one editor.',
editMemberAction: 'Edit member',
saveDisplayName: 'Save name', saveDisplayName: 'Save name',
savingDisplayName: 'Saving name…', savingDisplayName: 'Saving name…',
memberStatusLabel: 'Member status', memberStatusLabel: 'Member status',
@@ -262,6 +285,7 @@ export const dictionary = {
'Вклады в общие покупки появятся здесь после первых записанных покупок.', 'Вклады в общие покупки появятся здесь после первых записанных покупок.',
purchaseShareLabel: 'Доля', purchaseShareLabel: 'Доля',
purchaseTotalLabel: 'Всего общих покупок', purchaseTotalLabel: 'Всего общих покупок',
participantsLabel: 'участника',
purchasesTitle: 'Общие покупки', purchasesTitle: 'Общие покупки',
purchasesEmpty: 'Пока нет общих покупок в этом цикле.', purchasesEmpty: 'Пока нет общих покупок в этом цикле.',
utilityLedgerTitle: 'Коммунальные платежи', utilityLedgerTitle: 'Коммунальные платежи',
@@ -286,21 +310,29 @@ export const dictionary = {
purchaseSplitEqual: 'Поровну', purchaseSplitEqual: 'Поровну',
purchaseSplitCustom: 'Свои суммы', purchaseSplitCustom: 'Свои суммы',
purchaseParticipantLabel: 'Участвует', purchaseParticipantLabel: 'Участвует',
participantIncluded: 'Участвует',
participantExcluded: 'Не участвует',
purchaseCustomShareLabel: 'Своя сумма', purchaseCustomShareLabel: 'Своя сумма',
purchaseEditorBody:
'Проверь покупку и меняй детали разделения только если это действительно нужно.',
paymentsAdminTitle: 'Оплаты', paymentsAdminTitle: 'Оплаты',
paymentsAdminBody: 'Добавляй, исправляй или удаляй оплаты за текущий цикл.', paymentsAdminBody: 'Добавляй, исправляй или удаляй оплаты за текущий цикл.',
paymentsAddAction: 'Добавить оплату', paymentsAddAction: 'Добавить оплату',
addingPayment: 'Добавляем оплату…', addingPayment: 'Добавляем оплату…',
paymentCreateBody: 'Создай оплату в отдельном окне вместо длинной встроенной формы.',
paymentKind: 'Тип оплаты', paymentKind: 'Тип оплаты',
paymentAmount: 'Сумма оплаты', paymentAmount: 'Сумма оплаты',
paymentMember: 'Участник', paymentMember: 'Участник',
paymentSaveAction: 'Сохранить оплату', paymentSaveAction: 'Сохранить оплату',
paymentDeleteAction: 'Удалить оплату', paymentDeleteAction: 'Удалить оплату',
paymentEditorBody: 'Проверь оплату в отдельном редакторе.',
deletingPayment: 'Удаляем оплату…', deletingPayment: 'Удаляем оплату…',
purchaseSaveAction: 'Сохранить покупку', purchaseSaveAction: 'Сохранить покупку',
purchaseDeleteAction: 'Удалить покупку', purchaseDeleteAction: 'Удалить покупку',
deletingPurchase: 'Удаляем покупку…', deletingPurchase: 'Удаляем покупку…',
savingPurchase: 'Сохраняем покупку…', savingPurchase: 'Сохраняем покупку…',
editEntryAction: 'Редактировать запись',
closeEditorAction: 'Закрыть',
householdSettingsTitle: 'Настройки household', householdSettingsTitle: 'Настройки household',
householdSettingsBody: 'Здесь можно менять язык household и подтверждать новых соседей.', householdSettingsBody: 'Здесь можно менять язык household и подтверждать новых соседей.',
topicBindingsTitle: 'Привязанные топики', topicBindingsTitle: 'Привязанные топики',
@@ -323,6 +355,8 @@ export const dictionary = {
billingCycleStatus: 'Валюта текущего цикла: {currency}', billingCycleStatus: 'Валюта текущего цикла: {currency}',
billingCycleOpenHint: 'Открой цикл перед тем, как вносить аренду и коммунальные счета.', billingCycleOpenHint: 'Открой цикл перед тем, как вносить аренду и коммунальные счета.',
billingCyclePeriod: 'Период цикла', billingCyclePeriod: 'Период цикла',
manageCycleAction: 'Управлять циклом',
cycleEditorBody: 'Все действия по циклу собраны в отдельном окне, а не растянуты по странице.',
openCycleAction: 'Открыть цикл', openCycleAction: 'Открыть цикл',
openingCycle: 'Открываем цикл…', openingCycle: 'Открываем цикл…',
closeCycleAction: 'Закрыть цикл', closeCycleAction: 'Закрыть цикл',
@@ -343,6 +377,8 @@ export const dictionary = {
utilitiesDueDay: 'День оплаты коммуналки', utilitiesDueDay: 'День оплаты коммуналки',
utilitiesReminderDay: 'День напоминания по коммуналке', utilitiesReminderDay: 'День напоминания по коммуналке',
timezone: 'Часовой пояс', timezone: 'Часовой пояс',
manageSettingsAction: 'Управлять настройками',
billingSettingsEditorBody: 'Основные правила биллинга собраны в отдельном окне.',
saveSettingsAction: 'Сохранить настройки', saveSettingsAction: 'Сохранить настройки',
savingSettings: 'Сохраняем настройки…', savingSettings: 'Сохраняем настройки…',
utilityCategoriesTitle: 'Категории коммуналки', utilityCategoriesTitle: 'Категории коммуналки',
@@ -350,14 +386,24 @@ export const dictionary = {
'Управляй категориями, которые админы используют для ежемесячного ввода коммунальных счетов.', 'Управляй категориями, которые админы используют для ежемесячного ввода коммунальных счетов.',
utilityCategoryName: 'Название категории', utilityCategoryName: 'Название категории',
utilityCategoryActive: 'Активна', utilityCategoryActive: 'Активна',
utilityBillsEditorBody:
'В списке остаются только короткие карточки, а детали редактируются отдельно.',
utilityBillCreateBody: 'Добавь коммунальный счёт в отдельном окне.',
utilityBillEditorBody: 'Исправь детали коммунального счёта здесь.',
editUtilityBillAction: 'Редактировать счёт',
addCategoryAction: 'Добавить категорию', addCategoryAction: 'Добавить категорию',
saveCategoryAction: 'Сохранить категорию', saveCategoryAction: 'Сохранить категорию',
savingCategory: 'Сохраняем…', savingCategory: 'Сохраняем…',
categoryCreateBody: 'Создай новую категорию без длинной встроенной формы.',
categoryEditorBody: 'Переименуй категорию или отключи её в отдельном окне.',
editCategoryAction: 'Редактировать категорию',
adminsTitle: 'Админы', adminsTitle: 'Админы',
adminsBody: adminsBody:
'Повышай доверенных участников, чтобы они могли управлять биллингом и подтверждениями.', 'Повышай доверенных участников, чтобы они могли управлять биллингом и подтверждениями.',
displayNameLabel: 'Имя в household', displayNameLabel: 'Имя в household',
displayNameHint: 'Это имя будет видно в балансе, леджере и ответах ассистента.', displayNameHint: 'Это имя будет видно в балансе, леджере и ответах ассистента.',
memberEditorBody: 'Статус, политика и админские действия по участнику собраны в одном окне.',
editMemberAction: 'Редактировать участника',
saveDisplayName: 'Сохранить имя', saveDisplayName: 'Сохранить имя',
savingDisplayName: 'Сохраняем имя…', savingDisplayName: 'Сохраняем имя…',
memberStatusLabel: 'Статус участника', memberStatusLabel: 'Статус участника',

View File

@@ -13,6 +13,18 @@
box-sizing: border-box; box-sizing: border-box;
} }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
@@ -204,6 +216,51 @@ button {
padding: 12px 16px; padding: 12px 16px;
} }
.ui-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid rgb(255 255 255 / 0.12);
border-radius: 14px;
min-height: 44px;
padding: 10px 14px;
background: rgb(255 255 255 / 0.05);
color: inherit;
transition:
transform 140ms ease,
border-color 140ms ease,
background 140ms ease,
color 140ms ease;
}
.ui-button:disabled {
opacity: 0.6;
}
.ui-button--primary {
border-color: rgb(247 179 137 / 0.42);
background: rgb(247 179 137 / 0.16);
color: #fff4ea;
}
.ui-button--secondary,
.ui-button--ghost,
.ui-button--icon {
background: rgb(255 255 255 / 0.04);
}
.ui-button--danger {
border-color: rgb(247 115 115 / 0.28);
background: rgb(247 115 115 / 0.08);
color: #ffc5c5;
}
.ui-button--icon {
min-width: 44px;
padding-inline: 0;
}
.nav-grid { .nav-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -397,7 +454,7 @@ button {
.purchase-chart__figure { .purchase-chart__figure {
position: relative; position: relative;
width: min(220px, 100%); width: min(180px, 100%);
justify-self: center; justify-self: center;
} }
@@ -433,7 +490,7 @@ button {
.purchase-chart__center span { .purchase-chart__center span {
color: #c6c2bb; color: #c6c2bb;
font-size: 0.82rem; font-size: 0.72rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
} }
@@ -451,7 +508,7 @@ button {
.purchase-chart__legend-item { .purchase-chart__legend-item {
border: 1px solid rgb(255 255 255 / 0.08); border: 1px solid rgb(255 255 255 / 0.08);
border-radius: 16px; border-radius: 16px;
padding: 12px; padding: 10px 12px;
background: rgb(255 255 255 / 0.02); background: rgb(255 255 255 / 0.02);
} }
@@ -584,6 +641,11 @@ button {
line-height: 1.2; line-height: 1.2;
} }
.settings-field select {
appearance: none;
-webkit-appearance: none;
}
.settings-field__value { .settings-field__value {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -615,6 +677,186 @@ button {
color: #ffc5c5; color: #ffc5c5;
} }
.panel-toolbar {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.ledger-compact-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 14px;
align-items: start;
border: 1px solid rgb(255 255 255 / 0.08);
border-radius: 18px;
padding: 14px;
background: rgb(255 255 255 / 0.02);
}
.ledger-compact-card__main {
min-width: 0;
}
.ledger-compact-card header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
margin-bottom: 6px;
}
.ledger-compact-card p {
margin: 0;
color: #d6d3cc;
}
.ledger-compact-card__meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.ledger-compact-card__actions {
display: flex;
align-items: start;
}
.mini-chip {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 6px 10px;
background: rgb(247 179 137 / 0.12);
color: #ffe6d2;
font-size: 0.82rem;
}
.mini-chip--muted {
background: rgb(255 255 255 / 0.05);
color: #dad5ce;
}
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: end center;
padding: 18px 14px;
background: rgb(8 10 16 / 0.7);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.modal-sheet {
width: min(720px, 100%);
max-height: min(88vh, 920px);
overflow: auto;
border: 1px solid rgb(255 255 255 / 0.1);
border-radius: 24px;
padding: 18px;
background:
linear-gradient(180deg, rgb(255 255 255 / 0.07), rgb(255 255 255 / 0.03)), rgb(18 26 36 / 0.96);
box-shadow: 0 28px 80px rgb(0 0 0 / 0.35);
}
.modal-sheet__header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: start;
}
.modal-sheet__header h3 {
margin: 0;
font-family: 'Space Grotesk', 'IBM Plex Sans', sans-serif;
letter-spacing: -0.04em;
font-size: 1.4rem;
}
.modal-sheet__header p {
margin-top: 8px;
}
.modal-sheet__body {
display: grid;
gap: 16px;
margin-top: 18px;
}
.modal-sheet__footer {
margin-top: 18px;
}
.editor-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
.editor-panel {
display: grid;
gap: 12px;
border: 1px solid rgb(255 255 255 / 0.08);
border-radius: 18px;
padding: 14px;
background: rgb(255 255 255 / 0.03);
}
.editor-panel__header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: start;
}
.participant-list {
display: grid;
gap: 10px;
}
.participant-card {
border: 1px solid rgb(255 255 255 / 0.08);
border-radius: 16px;
padding: 12px;
background: rgb(255 255 255 / 0.02);
}
.participant-card header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
margin-bottom: 10px;
}
.participant-card__controls {
display: grid;
gap: 10px;
}
.participant-card__field {
margin-top: 2px;
}
.modal-action-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
}
.modal-action-row--single {
justify-content: flex-end;
}
.modal-action-row__primary {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.panel--wide { .panel--wide {
min-height: 170px; min-height: 170px;
} }
@@ -709,6 +951,10 @@ button {
.settings-grid { .settings-grid {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.editor-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
} }
@media (max-width: 759px) { @media (max-width: 759px) {
@@ -742,6 +988,7 @@ button {
} }
.activity-row header, .activity-row header,
.ledger-compact-card header,
.ledger-item header, .ledger-item header,
.utility-bill-row header, .utility-bill-row header,
.balance-item header { .balance-item header {