diff --git a/apps/miniapp/package.json b/apps/miniapp/package.json
index ff2d7ce..eed07c8 100644
--- a/apps/miniapp/package.json
+++ b/apps/miniapp/package.json
@@ -10,8 +10,13 @@
"lint": "oxlint \"src\""
},
"dependencies": {
+ "@kobalte/core": "0.13.11",
+ "@tanstack/solid-query": "5.90.23",
"@twa-dev/sdk": "8.0.2",
- "solid-js": "^1.9.9"
+ "class-variance-authority": "0.7.1",
+ "clsx": "2.1.1",
+ "solid-js": "^1.9.9",
+ "zod": "4.3.6"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.13",
diff --git a/apps/miniapp/src/App.tsx b/apps/miniapp/src/App.tsx
index d2f4e7d..1806e6f 100644
--- a/apps/miniapp/src/App.tsx
+++ b/apps/miniapp/src/App.tsx
@@ -36,6 +36,20 @@ import {
type MiniAppPendingMember
} from './miniapp-api'
import { Button, Field, IconButton, Modal } from './components/ui'
+import { HeroBanner } from './components/layout/hero-banner'
+import { NavigationTabs } from './components/layout/navigation-tabs'
+import { ProfileCard } from './components/layout/profile-card'
+import { TopBar } from './components/layout/top-bar'
+import { FinanceSummaryCards } from './components/finance/finance-summary-cards'
+import { FinanceVisuals } from './components/finance/finance-visuals'
+import {
+ demoAdminSettings,
+ demoCycleState,
+ demoDashboard,
+ demoMember,
+ demoPendingMembers,
+ demoTelegramUser
+} from './demo/miniapp-demo'
import { getTelegramWebApp } from './telegram-webapp'
type SessionState =
@@ -106,19 +120,8 @@ const chartPalette = ['#f7b389', '#6fd3c0', '#f06a8d', '#94a8ff', '#f3d36f', '#7
const demoSession: Extract = {
status: 'ready',
mode: 'demo',
- member: {
- id: 'demo-member',
- displayName: 'Demo Resident',
- status: 'active',
- isAdmin: false,
- preferredLocale: 'en',
- householdDefaultLocale: 'en'
- },
- telegramUser: {
- firstName: 'Demo',
- username: 'demo_user',
- languageCode: 'en'
- }
+ member: demoMember,
+ telegramUser: demoTelegramUser
}
function detectLocale(): Locale {
@@ -952,6 +955,68 @@ function App() {
}
}
+ function applyDemoState() {
+ setDisplayNameDraft(demoSession.member.displayName)
+ setSession(demoSession)
+ setDashboard(demoDashboard)
+ setPendingMembers([...demoPendingMembers])
+ setAdminSettings(demoAdminSettings)
+ setCycleState(demoCycleState)
+ setPurchaseDraftMap(purchaseDrafts(demoDashboard.ledger))
+ setPaymentDraftMap(paymentDrafts(demoDashboard.ledger))
+ setMemberDisplayNameDrafts(
+ Object.fromEntries(demoAdminSettings.members.map((member) => [member.id, member.displayName]))
+ )
+ setRentWeightDrafts(
+ Object.fromEntries(
+ demoAdminSettings.members.map((member) => [member.id, String(member.rentShareWeight)])
+ )
+ )
+ setMemberStatusDrafts(
+ Object.fromEntries(demoAdminSettings.members.map((member) => [member.id, member.status]))
+ )
+ setMemberAbsencePolicyDrafts(
+ Object.fromEntries(
+ demoAdminSettings.members.map((member) => [
+ member.id,
+ resolvedMemberAbsencePolicy(member.id, member.status, demoAdminSettings).policy
+ ])
+ )
+ )
+ setBillingForm({
+ settlementCurrency: demoAdminSettings.settings.settlementCurrency,
+ paymentBalanceAdjustmentPolicy: demoAdminSettings.settings.paymentBalanceAdjustmentPolicy,
+ rentAmountMajor: demoAdminSettings.settings.rentAmountMinor
+ ? (Number(demoAdminSettings.settings.rentAmountMinor) / 100).toFixed(2)
+ : '',
+ rentCurrency: demoAdminSettings.settings.rentCurrency,
+ rentDueDay: demoAdminSettings.settings.rentDueDay,
+ rentWarningDay: demoAdminSettings.settings.rentWarningDay,
+ utilitiesDueDay: demoAdminSettings.settings.utilitiesDueDay,
+ utilitiesReminderDay: demoAdminSettings.settings.utilitiesReminderDay,
+ timezone: demoAdminSettings.settings.timezone
+ })
+ setCycleForm((current) => ({
+ ...current,
+ period: demoCycleState.cycle?.period ?? current.period,
+ rentCurrency: demoAdminSettings.settings.rentCurrency,
+ utilityCurrency: demoAdminSettings.settings.settlementCurrency,
+ rentAmountMajor: demoAdminSettings.settings.rentAmountMinor
+ ? (Number(demoAdminSettings.settings.rentAmountMinor) / 100).toFixed(2)
+ : '',
+ utilityCategorySlug:
+ demoAdminSettings.categories.find((category) => category.isActive)?.slug ?? '',
+ utilityAmountMajor: ''
+ }))
+ setPaymentForm({
+ memberId: demoAdminSettings.members[0]?.id ?? '',
+ kind: 'rent',
+ amountMajor: '',
+ currency: demoAdminSettings.settings.settlementCurrency
+ })
+ setUtilityBillDrafts(cycleUtilityBillDrafts(demoCycleState.utilityBills))
+ }
+
async function bootstrap() {
const fallbackLocale = detectLocale()
setLocale(fallbackLocale)
@@ -962,7 +1027,7 @@ function App() {
const initData = webApp?.initData?.trim()
if (!initData) {
if (import.meta.env.DEV) {
- setSession(demoSession)
+ applyDemoState()
return
}
@@ -1017,99 +1082,7 @@ function App() {
}
} catch {
if (import.meta.env.DEV) {
- setDisplayNameDraft(demoSession.member.displayName)
- setSession(demoSession)
- setDashboard({
- period: '2026-03',
- currency: 'GEL',
- totalDueMajor: '1030.00',
- totalPaidMajor: '501.00',
- totalRemainingMajor: '529.00',
- rentSourceAmountMajor: '700.00',
- rentSourceCurrency: 'USD',
- rentDisplayAmountMajor: '1932.00',
- rentFxRateMicros: '2760000',
- rentFxEffectiveDate: '2026-03-17',
- members: [
- {
- memberId: 'demo-member',
- displayName: 'Demo Resident',
- rentShareMajor: '483.00',
- utilityShareMajor: '32.00',
- purchaseOffsetMajor: '-14.00',
- netDueMajor: '501.00',
- paidMajor: '501.00',
- remainingMajor: '0.00',
- explanations: ['Equal utility split', 'Shared purchase offset']
- },
- {
- memberId: 'member-2',
- displayName: 'Alice',
- rentShareMajor: '483.00',
- utilityShareMajor: '32.00',
- purchaseOffsetMajor: '14.00',
- netDueMajor: '529.00',
- paidMajor: '0.00',
- remainingMajor: '529.00',
- explanations: ['Equal utility split']
- }
- ],
- ledger: [
- {
- id: 'purchase-1',
- kind: 'purchase',
- title: 'Soap',
- memberId: 'member-2',
- paymentKind: null,
- amountMajor: '30.00',
- currency: 'GEL',
- displayAmountMajor: '30.00',
- displayCurrency: 'GEL',
- fxRateMicros: null,
- fxEffectiveDate: null,
- actorDisplayName: 'Alice',
- occurredAt: '2026-03-12T11:00:00.000Z'
- },
- {
- id: 'utility-1',
- kind: 'utility',
- title: 'Electricity',
- memberId: null,
- paymentKind: null,
- amountMajor: '120.00',
- currency: 'GEL',
- displayAmountMajor: '120.00',
- displayCurrency: 'GEL',
- fxRateMicros: null,
- fxEffectiveDate: null,
- actorDisplayName: 'Alice',
- occurredAt: '2026-03-12T12:00:00.000Z'
- },
- {
- id: 'payment-1',
- kind: 'payment',
- title: 'rent',
- memberId: 'demo-member',
- paymentKind: 'rent',
- amountMajor: '501.00',
- currency: 'GEL',
- displayAmountMajor: '501.00',
- displayCurrency: 'GEL',
- fxRateMicros: null,
- fxEffectiveDate: null,
- actorDisplayName: 'Demo Resident',
- occurredAt: '2026-03-18T15:10:00.000Z'
- }
- ]
- })
- setPendingMembers([
- {
- telegramUserId: '555777',
- displayName: 'Mia',
- username: 'mia',
- languageCode: 'ru'
- }
- ])
+ applyDemoState()
return
}
@@ -1874,142 +1847,6 @@ function App() {
}))
}
- function renderFinanceSummaryCards(data: MiniAppDashboard): JSX.Element {
- return (
- <>
-
- {copy().remainingLabel}
-
- {data.totalRemainingMajor} {data.currency}
-
-
-
- {copy().shareRent}
-
- {data.rentDisplayAmountMajor} {data.currency}
-
-
-
- {copy().shareUtilities}
-
- {utilityTotalMajor()} {data.currency}
-
-
-
- {copy().purchasesTitle}
-
- {purchaseTotalMajor()} {data.currency}
-
-
- >
- )
- }
-
- function renderFinanceVisuals(data: MiniAppDashboard): JSX.Element {
- const purchaseChart = purchaseInvestmentChart()
-
- return (
- <>
-
-
- {copy().financeVisualsTitle}
-
- {copy().membersCount}: {String(data.members.length)}
-
-
- {copy().financeVisualsBody}
-
- {memberBalanceVisuals().map((item) => (
-
-
- {item.member.displayName}
-
- {item.member.remainingMajor} {data.currency}
-
-
-
-
- {item.segments.map((segment) => (
-
- ))}
-
-
-
- {item.segments.map((segment) => (
-
- {segment.label}: {segment.amountMajor} {data.currency}
-
- ))}
-
-
- ))}
-
-
-
-
-
- {copy().purchaseInvestmentsTitle}
-
- {copy().purchaseTotalLabel}: {purchaseChart.totalMajor} {data.currency}
-
-
- {copy().purchaseInvestmentsBody}
- {purchaseChart.slices.length === 0 ? (
- {copy().purchaseInvestmentsEmpty}
- ) : (
-
-
-
- {purchaseChart.slices.map((slice) => (
-
-
-
- {slice.label}
-
-
- {slice.amountMajor} {data.currency} · {copy().purchaseShareLabel}{' '}
- {slice.percentage}%
-
-
- ))}
-
-
- )}
-
- >
- )
- }
-
const renderPanel = () => {
switch (activeNav()) {
case 'balances':
@@ -2063,8 +1900,35 @@ function App() {
) : null}
- {renderFinanceSummaryCards(data)}
- {renderFinanceVisuals(data)}
+
+
+
+
{copy().householdBalancesTitle}
@@ -3739,7 +3603,19 @@ function App() {
>
}
- render={(data) => renderFinanceSummaryCards(data)}
+ render={(data) => (
+
+ )}
/>
{readySession()?.member.isAdmin ? (
@@ -3814,7 +3690,24 @@ function App() {
renderFinanceVisuals(data)}
+ render={(data) => (
+
+ )}
/>
@@ -3856,66 +3749,46 @@ function App() {
-
-
-
{copy().appSubtitle}
-
{copy().appTitle}
-
-
-
-
+ void handleMemberLocaleChange(nextLocale)}
+ />
-
- {copy().loadingBadge}
- {copy().loadingTitle}
- {copy().loadingBody}
-
+
-
- {copy().loadingBadge}
-
- {blockedSession()?.reason === 'telegram_only'
+
-
- {blockedSession()?.reason === 'telegram_only'
+ : copy().unexpectedErrorTitle
+ }
+ body={
+ blockedSession()?.reason === 'telegram_only'
? copy().telegramOnlyBody
- : copy().unexpectedErrorBody}
-
-
-
+ : copy().unexpectedErrorBody
+ }
+ action={{ label: copy().reload, onClick: () => window.location.reload() }}
+ />
- {copy().loadingBadge}
+
+ {copy().loadingBadge}
+
{onboardingSession()?.mode === 'pending'
? copy().pendingTitle
@@ -3938,18 +3811,13 @@ function App() {
-
-
-
- {readySession()?.mode === 'demo' ? copy().demoBadge : copy().liveBadge}
-
-
- {readySession()?.member.isAdmin ? copy().adminTag : copy().residentTag}
-
-
- {readySession()?.member.status
- ? memberStatusLabel(readySession()!.member.status)
- : copy().memberStatusActive}
-
-
+ setProfileEditorOpen(true)
+ }
+ : undefined
+ }
+ />
-
- {copy().welcome},{' '}
- {readySession()?.telegramUser.firstName ?? readySession()?.member.displayName}
-
- {copy().overviewBody}
-
-
-
-
-
-
-
-
+ }
+ active={activeNav()}
+ onChange={setActiveNav}
+ />
-
-
- {readySession()?.member.displayName}
- {readySession()?.member.isAdmin ? copy().adminTag : copy().residentTag}
-
-
- {copy().memberStatusSummary.replace(
- '{status}',
- readySession()?.member.status
- ? memberStatusLabel(readySession()!.member.status)
- : copy().memberStatusActive
- )}
-
-
-
- {readySession()?.mode === 'demo' ? copy().demoBadge : copy().liveBadge}
-
- {locale().toUpperCase()}
-
-
+
{renderPanel()}
+ {(item) => (
+
+ {item.label}
+ {item.value}
+
+ )}
+
+ )
+}
diff --git a/apps/miniapp/src/components/finance/finance-visuals.tsx b/apps/miniapp/src/components/finance/finance-visuals.tsx
new file mode 100644
index 0000000..4f0c350
--- /dev/null
+++ b/apps/miniapp/src/components/finance/finance-visuals.tsx
@@ -0,0 +1,163 @@
+import { For, Match, Switch } from 'solid-js'
+
+import type { MiniAppDashboard } from '../../miniapp-api'
+
+type MemberVisual = {
+ member: MiniAppDashboard['members'][number]
+ totalMinor: bigint
+ barWidthPercent: number
+ segments: {
+ key: string
+ label: string
+ amountMajor: string
+ amountMinor: bigint
+ widthPercent: number
+ }[]
+}
+
+type PurchaseSlice = {
+ key: string
+ label: string
+ amountMajor: string
+ color: string
+ percentage: number
+ dasharray: string
+ dashoffset: string
+}
+
+type Props = {
+ dashboard: MiniAppDashboard
+ memberVisuals: readonly MemberVisual[]
+ purchaseChart: {
+ totalMajor: string
+ slices: readonly PurchaseSlice[]
+ }
+ labels: {
+ financeVisualsTitle: string
+ financeVisualsBody: string
+ membersCount: string
+ purchaseInvestmentsTitle: string
+ purchaseInvestmentsBody: string
+ purchaseInvestmentsEmpty: string
+ purchaseTotalLabel: string
+ purchaseShareLabel: string
+ }
+ remainingClass: (member: MiniAppDashboard['members'][number]) => string
+}
+
+export function FinanceVisuals(props: Props) {
+ return (
+ <>
+
+
+ {props.labels.financeVisualsTitle}
+
+ {props.labels.membersCount}: {String(props.dashboard.members.length)}
+
+
+ {props.labels.financeVisualsBody}
+
+
+ {(item) => (
+
+
+ {item.member.displayName}
+
+ {item.member.remainingMajor} {props.dashboard.currency}
+
+
+
+
+
+ {(segment) => (
+
+ )}
+
+
+
+
+
+ {(segment) => (
+
+ {segment.label}: {segment.amountMajor} {props.dashboard.currency}
+
+ )}
+
+
+
+ )}
+
+
+
+
+
+
+ {props.labels.purchaseInvestmentsTitle}
+
+ {props.labels.purchaseTotalLabel}: {props.purchaseChart.totalMajor}{' '}
+ {props.dashboard.currency}
+
+
+ {props.labels.purchaseInvestmentsBody}
+
+
+ {props.labels.purchaseInvestmentsEmpty}
+
+ 0}>
+
+
+
+
+ {(slice) => (
+
+
+
+ {slice.label}
+
+
+ {slice.amountMajor} {props.dashboard.currency} ·{' '}
+ {props.labels.purchaseShareLabel} {slice.percentage}%
+
+
+ )}
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/miniapp/src/components/layout/hero-banner.tsx b/apps/miniapp/src/components/layout/hero-banner.tsx
new file mode 100644
index 0000000..a37bd73
--- /dev/null
+++ b/apps/miniapp/src/components/layout/hero-banner.tsx
@@ -0,0 +1,38 @@
+import { Show, type JSX } from 'solid-js'
+
+import { Button, MiniChip } from '../ui'
+
+type Props = {
+ badges: readonly string[]
+ title: string
+ body: string
+ action?:
+ | {
+ label: string
+ onClick: () => void
+ }
+ | undefined
+}
+
+export function HeroBanner(props: Props): JSX.Element {
+ return (
+
+
+ {props.badges.map((badge, index) => (
+ 0}>{badge}
+ ))}
+
+ {props.title}
+ {props.body}
+
+ {(action) => (
+
+
+
+ )}
+
+
+ )
+}
diff --git a/apps/miniapp/src/components/layout/navigation-tabs.tsx b/apps/miniapp/src/components/layout/navigation-tabs.tsx
new file mode 100644
index 0000000..4495e23
--- /dev/null
+++ b/apps/miniapp/src/components/layout/navigation-tabs.tsx
@@ -0,0 +1,28 @@
+import type { JSX } from 'solid-js'
+
+type TabItem = {
+ key: T
+ label: string
+}
+
+type Props = {
+ items: readonly TabItem[]
+ active: T
+ onChange: (key: T) => void
+}
+
+export function NavigationTabs(props: Props): JSX.Element {
+ return (
+
+ )
+}
diff --git a/apps/miniapp/src/components/layout/profile-card.tsx b/apps/miniapp/src/components/layout/profile-card.tsx
new file mode 100644
index 0000000..a1ae035
--- /dev/null
+++ b/apps/miniapp/src/components/layout/profile-card.tsx
@@ -0,0 +1,25 @@
+import { Card, MiniChip } from '../ui'
+
+type Props = {
+ displayName: string
+ roleLabel: string
+ statusSummary: string
+ modeBadge: string
+ localeBadge: string
+}
+
+export function ProfileCard(props: Props) {
+ return (
+
+
+ {props.displayName}
+ {props.roleLabel}
+
+ {props.statusSummary}
+
+ {props.modeBadge}
+ {props.localeBadge}
+
+
+ )
+}
diff --git a/apps/miniapp/src/components/layout/top-bar.tsx b/apps/miniapp/src/components/layout/top-bar.tsx
new file mode 100644
index 0000000..0142f93
--- /dev/null
+++ b/apps/miniapp/src/components/layout/top-bar.tsx
@@ -0,0 +1,43 @@
+import type { Locale } from '../../i18n'
+
+type Props = {
+ subtitle: string
+ title: string
+ languageLabel: string
+ locale: Locale
+ saving: boolean
+ onChange: (locale: Locale) => void
+}
+
+export function TopBar(props: Props) {
+ return (
+
+
+
{props.subtitle}
+
{props.title}
+
+
+
+
+ )
+}
diff --git a/apps/miniapp/src/components/ui.tsx b/apps/miniapp/src/components/ui.tsx
deleted file mode 100644
index f7222c4..0000000
--- a/apps/miniapp/src/components/ui.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-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
- }>
-) {
- return (
-
- )
-}
-
-export function IconButton(
- props: ParentProps<{
- label: string
- class?: string
- disabled?: boolean
- onClick?: JSX.EventHandlerUnion
- }>
-) {
- const maybeClass = props.class ? { class: props.class } : {}
- const maybeDisabled = props.disabled !== undefined ? { disabled: props.disabled } : {}
- const maybeOnClick = props.onClick ? { onClick: props.onClick } : {}
-
- return (
-
- )
-}
-
-export function Field(
- props: ParentProps<{
- label: string
- hint?: string
- wide?: boolean
- class?: string
- }>
-) {
- return (
-
- )
-}
-
-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 (
-
- props.onClose()}>
-
event.stopPropagation()}
- >
-
-
- {props.children}
-
-
- {(footer) => }
-
-
-
-
- )
-}
diff --git a/apps/miniapp/src/components/ui/button.tsx b/apps/miniapp/src/components/ui/button.tsx
new file mode 100644
index 0000000..718314b
--- /dev/null
+++ b/apps/miniapp/src/components/ui/button.tsx
@@ -0,0 +1,60 @@
+import { cva, type VariantProps } from 'class-variance-authority'
+import type { JSX, ParentProps } from 'solid-js'
+
+import { cn } from '../../lib/cn'
+
+const buttonVariants = cva('ui-button', {
+ variants: {
+ variant: {
+ primary: 'ui-button--primary',
+ secondary: 'ui-button--secondary',
+ danger: 'ui-button--danger',
+ ghost: 'ui-button--ghost',
+ icon: 'ui-button--icon'
+ }
+ },
+ defaultVariants: {
+ variant: 'secondary'
+ }
+})
+
+type ButtonProps = ParentProps<{
+ type?: 'button' | 'submit' | 'reset'
+ class?: string
+ disabled?: boolean
+ onClick?: JSX.EventHandlerUnion
+}> &
+ VariantProps
+
+export function Button(props: ButtonProps) {
+ return (
+
+ )
+}
+
+export function IconButton(
+ props: ParentProps<{
+ label: string
+ class?: string
+ disabled?: boolean
+ onClick?: JSX.EventHandlerUnion
+ }>
+) {
+ const maybeClass = props.class ? { class: props.class } : {}
+ const maybeDisabled = props.disabled !== undefined ? { disabled: props.disabled } : {}
+ const maybeOnClick = props.onClick ? { onClick: props.onClick } : {}
+
+ return (
+
+ )
+}
diff --git a/apps/miniapp/src/components/ui/card.tsx b/apps/miniapp/src/components/ui/card.tsx
new file mode 100644
index 0000000..53b519c
--- /dev/null
+++ b/apps/miniapp/src/components/ui/card.tsx
@@ -0,0 +1,23 @@
+import type { ParentProps } from 'solid-js'
+
+import { cn } from '../../lib/cn'
+
+export function Card(props: ParentProps<{ class?: string; accent?: boolean }>) {
+ return (
+
+ {props.children}
+
+ )
+}
+
+export function StatCard(props: ParentProps<{ class?: string }>) {
+ return {props.children}
+}
+
+export function MiniChip(props: ParentProps<{ muted?: boolean; class?: string }>) {
+ return (
+
+ {props.children}
+
+ )
+}
diff --git a/apps/miniapp/src/components/ui/dialog.tsx b/apps/miniapp/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..49810f1
--- /dev/null
+++ b/apps/miniapp/src/components/ui/dialog.tsx
@@ -0,0 +1,41 @@
+import * as Dialog from '@kobalte/core/dialog'
+import { Show, type JSX, type ParentProps } from 'solid-js'
+
+export function Modal(
+ props: ParentProps<{
+ open: boolean
+ title: string
+ description?: string
+ closeLabel: string
+ footer?: JSX.Element
+ onClose: () => void
+ }>
+) {
+ return (
+ !open && props.onClose()}>
+
+
+
+
+
+ {props.children}
+
+ {(footer) => }
+
+
+
+
+
+ )
+}
diff --git a/apps/miniapp/src/components/ui/field.tsx b/apps/miniapp/src/components/ui/field.tsx
new file mode 100644
index 0000000..ff38076
--- /dev/null
+++ b/apps/miniapp/src/components/ui/field.tsx
@@ -0,0 +1,20 @@
+import { Show, type ParentProps } from 'solid-js'
+
+import { cn } from '../../lib/cn'
+
+export function Field(
+ props: ParentProps<{
+ label: string
+ hint?: string
+ wide?: boolean
+ class?: string
+ }>
+) {
+ return (
+
+ )
+}
diff --git a/apps/miniapp/src/components/ui/index.ts b/apps/miniapp/src/components/ui/index.ts
new file mode 100644
index 0000000..534a555
--- /dev/null
+++ b/apps/miniapp/src/components/ui/index.ts
@@ -0,0 +1,4 @@
+export * from './button'
+export * from './card'
+export * from './dialog'
+export * from './field'
diff --git a/apps/miniapp/src/demo/miniapp-demo.ts b/apps/miniapp/src/demo/miniapp-demo.ts
new file mode 100644
index 0000000..cfb3abb
--- /dev/null
+++ b/apps/miniapp/src/demo/miniapp-demo.ts
@@ -0,0 +1,285 @@
+import type {
+ MiniAppAdminCycleState,
+ MiniAppAdminSettingsPayload,
+ MiniAppDashboard,
+ MiniAppPendingMember,
+ MiniAppSession
+} from '../miniapp-api'
+
+export const demoMember: NonNullable = {
+ id: 'demo-member',
+ householdId: 'demo-household',
+ displayName: 'Stas',
+ status: 'active',
+ isAdmin: true,
+ preferredLocale: 'en',
+ householdDefaultLocale: 'en'
+}
+
+export const demoTelegramUser: NonNullable = {
+ firstName: 'Stas',
+ username: 'stas_demo',
+ languageCode: 'en'
+}
+
+export const demoDashboard: MiniAppDashboard = {
+ period: '2026-03',
+ currency: 'GEL',
+ totalDueMajor: '2410.00',
+ totalPaidMajor: '650.00',
+ totalRemainingMajor: '1760.00',
+ rentSourceAmountMajor: '875.00',
+ rentSourceCurrency: 'USD',
+ rentDisplayAmountMajor: '2415.00',
+ rentFxRateMicros: '2760000',
+ rentFxEffectiveDate: '2026-03-17',
+ members: [
+ {
+ memberId: 'demo-member',
+ displayName: 'Stas',
+ rentShareMajor: '603.75',
+ utilityShareMajor: '78.00',
+ purchaseOffsetMajor: '-66.00',
+ netDueMajor: '615.75',
+ paidMajor: '615.75',
+ remainingMajor: '0.00',
+ explanations: ['Weighted rent share', 'Custom purchase split credit']
+ },
+ {
+ memberId: 'member-chorb',
+ displayName: 'Chorbanaut',
+ rentShareMajor: '603.75',
+ utilityShareMajor: '78.00',
+ purchaseOffsetMajor: '12.00',
+ netDueMajor: '693.75',
+ paidMajor: '0.00',
+ remainingMajor: '693.75',
+ explanations: ['Standard resident share']
+ },
+ {
+ memberId: 'member-el',
+ displayName: 'El',
+ rentShareMajor: '1207.50',
+ utilityShareMajor: '0.00',
+ purchaseOffsetMajor: '54.00',
+ netDueMajor: '1261.50',
+ paidMajor: '34.25',
+ remainingMajor: '1227.25',
+ explanations: ['Away policy applied to utilities']
+ }
+ ],
+ ledger: [
+ {
+ id: 'purchase-1',
+ kind: 'purchase',
+ title: 'Bought kitchen towels',
+ memberId: 'demo-member',
+ paymentKind: null,
+ amountMajor: '24.00',
+ currency: 'GEL',
+ displayAmountMajor: '24.00',
+ displayCurrency: 'GEL',
+ fxRateMicros: null,
+ fxEffectiveDate: null,
+ actorDisplayName: 'Stas',
+ occurredAt: '2026-03-04T11:00:00.000Z',
+ purchaseSplitMode: 'equal',
+ purchaseParticipants: [
+ { memberId: 'demo-member', included: true, shareAmountMajor: null },
+ { memberId: 'member-chorb', included: true, shareAmountMajor: null },
+ { memberId: 'member-el', included: false, shareAmountMajor: null }
+ ]
+ },
+ {
+ id: 'purchase-2',
+ kind: 'purchase',
+ title: 'Electric kettle',
+ memberId: 'member-chorb',
+ paymentKind: null,
+ amountMajor: '96.00',
+ currency: 'GEL',
+ displayAmountMajor: '96.00',
+ displayCurrency: 'GEL',
+ fxRateMicros: null,
+ fxEffectiveDate: null,
+ actorDisplayName: 'Chorbanaut',
+ occurredAt: '2026-03-08T16:20:00.000Z',
+ purchaseSplitMode: 'custom_amounts',
+ purchaseParticipants: [
+ { memberId: 'demo-member', included: true, shareAmountMajor: '42.00' },
+ { memberId: 'member-chorb', included: true, shareAmountMajor: '24.00' },
+ { memberId: 'member-el', included: true, shareAmountMajor: '30.00' }
+ ]
+ },
+ {
+ id: 'utility-1',
+ kind: 'utility',
+ title: 'Electricity',
+ memberId: null,
+ paymentKind: null,
+ amountMajor: '154.00',
+ currency: 'GEL',
+ displayAmountMajor: '154.00',
+ displayCurrency: 'GEL',
+ fxRateMicros: null,
+ fxEffectiveDate: null,
+ actorDisplayName: 'Stas',
+ occurredAt: '2026-03-09T12:00:00.000Z'
+ },
+ {
+ id: 'utility-2',
+ kind: 'utility',
+ title: 'Internet',
+ memberId: null,
+ paymentKind: null,
+ amountMajor: '80.00',
+ currency: 'GEL',
+ displayAmountMajor: '80.00',
+ displayCurrency: 'GEL',
+ fxRateMicros: null,
+ fxEffectiveDate: null,
+ actorDisplayName: 'Stas',
+ occurredAt: '2026-03-10T10:30:00.000Z'
+ },
+ {
+ id: 'payment-1',
+ kind: 'payment',
+ title: 'rent',
+ memberId: 'demo-member',
+ paymentKind: 'rent',
+ amountMajor: '615.75',
+ currency: 'GEL',
+ displayAmountMajor: '615.75',
+ displayCurrency: 'GEL',
+ fxRateMicros: null,
+ fxEffectiveDate: null,
+ actorDisplayName: 'Stas',
+ occurredAt: '2026-03-11T18:10:00.000Z'
+ },
+ {
+ id: 'payment-2',
+ kind: 'payment',
+ title: 'utilities',
+ memberId: 'member-el',
+ paymentKind: 'utilities',
+ amountMajor: '34.25',
+ currency: 'GEL',
+ displayAmountMajor: '34.25',
+ displayCurrency: 'GEL',
+ fxRateMicros: null,
+ fxEffectiveDate: null,
+ actorDisplayName: 'El',
+ occurredAt: '2026-03-13T09:00:00.000Z'
+ }
+ ]
+}
+
+export const demoPendingMembers: readonly MiniAppPendingMember[] = [
+ {
+ telegramUserId: '555777',
+ displayName: 'Mia',
+ username: 'mia',
+ languageCode: 'ru'
+ },
+ {
+ telegramUserId: '777999',
+ displayName: 'Dima',
+ username: 'dima',
+ languageCode: 'en'
+ }
+]
+
+export const demoAdminSettings: MiniAppAdminSettingsPayload = {
+ settings: {
+ householdId: 'demo-household',
+ settlementCurrency: 'GEL',
+ paymentBalanceAdjustmentPolicy: 'utilities',
+ rentAmountMinor: '241500',
+ rentCurrency: 'USD',
+ rentDueDay: 20,
+ rentWarningDay: 17,
+ utilitiesDueDay: 4,
+ utilitiesReminderDay: 3,
+ timezone: 'Asia/Tbilisi'
+ },
+ topics: [
+ { role: 'purchase', telegramThreadId: '101', topicName: 'Purchases' },
+ { role: 'feedback', telegramThreadId: '102', topicName: 'Anonymous feedback' },
+ { role: 'reminders', telegramThreadId: '103', topicName: 'Reminders' },
+ { role: 'payments', telegramThreadId: '104', topicName: 'Payments' }
+ ],
+ categories: [
+ {
+ id: 'cat-electricity',
+ householdId: 'demo-household',
+ slug: 'electricity',
+ name: 'Electricity',
+ sortOrder: 0,
+ isActive: true
+ },
+ {
+ id: 'cat-internet',
+ householdId: 'demo-household',
+ slug: 'internet',
+ name: 'Internet',
+ sortOrder: 1,
+ isActive: true
+ },
+ {
+ id: 'cat-gas',
+ householdId: 'demo-household',
+ slug: 'gas',
+ name: 'Gas',
+ sortOrder: 2,
+ isActive: false
+ }
+ ],
+ members: [
+ { id: 'demo-member', displayName: 'Stas', status: 'active', rentShareWeight: 1, isAdmin: true },
+ {
+ id: 'member-chorb',
+ displayName: 'Chorbanaut',
+ status: 'active',
+ rentShareWeight: 1,
+ isAdmin: false
+ },
+ { id: 'member-el', displayName: 'El', status: 'away', rentShareWeight: 2, isAdmin: false }
+ ],
+ memberAbsencePolicies: [
+ {
+ memberId: 'member-el',
+ effectiveFromPeriod: '2026-03',
+ policy: 'away_rent_only'
+ }
+ ]
+}
+
+export const demoCycleState: MiniAppAdminCycleState = {
+ cycle: {
+ id: 'cycle-demo-2026-03',
+ period: '2026-03',
+ currency: 'GEL'
+ },
+ rentRule: {
+ amountMinor: '241500',
+ currency: 'USD'
+ },
+ utilityBills: [
+ {
+ id: 'utility-bill-1',
+ billName: 'Electricity',
+ amountMinor: '15400',
+ currency: 'GEL',
+ createdByMemberId: 'demo-member',
+ createdAt: '2026-03-09T12:00:00.000Z'
+ },
+ {
+ id: 'utility-bill-2',
+ billName: 'Internet',
+ amountMinor: '8000',
+ currency: 'GEL',
+ createdByMemberId: 'demo-member',
+ createdAt: '2026-03-10T10:30:00.000Z'
+ }
+ ]
+}
diff --git a/apps/miniapp/src/index.tsx b/apps/miniapp/src/index.tsx
index 7847bac..3d52a0a 100644
--- a/apps/miniapp/src/index.tsx
+++ b/apps/miniapp/src/index.tsx
@@ -1,6 +1,8 @@
/* @refresh reload */
+import { QueryClientProvider } from '@tanstack/solid-query'
import { render } from 'solid-js/web'
+import { miniAppQueryClient } from './app/query-client'
import './index.css'
import App from './App'
@@ -10,4 +12,11 @@ if (!root) {
throw new Error('Root element not found')
}
-render(() => , root)
+render(
+ () => (
+
+
+
+ ),
+ root
+)
diff --git a/apps/miniapp/src/lib/cn.ts b/apps/miniapp/src/lib/cn.ts
new file mode 100644
index 0000000..40ed99b
--- /dev/null
+++ b/apps/miniapp/src/lib/cn.ts
@@ -0,0 +1,5 @@
+import { clsx, type ClassValue } from 'clsx'
+
+export function cn(...inputs: ClassValue[]) {
+ return clsx(inputs)
+}
diff --git a/bun.lock b/bun.lock
index dd532b8..dd3bbd3 100644
--- a/bun.lock
+++ b/bun.lock
@@ -30,8 +30,13 @@
"apps/miniapp": {
"name": "@household/miniapp",
"dependencies": {
+ "@kobalte/core": "0.13.11",
+ "@tanstack/solid-query": "5.90.23",
"@twa-dev/sdk": "8.0.2",
+ "class-variance-authority": "0.7.1",
+ "clsx": "2.1.1",
"solid-js": "^1.9.9",
+ "zod": "4.3.6",
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.13",
@@ -141,6 +146,8 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+ "@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="],
+
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
@@ -199,6 +206,12 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+ "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
+
+ "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
+
+ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
+
"@grammyjs/types": ["@grammyjs/types@3.25.0", "", {}, "sha512-iN9i5p+8ZOu9OMxWNcguojQfz4K/PDyMPOnL7PPCON+SoA/F8OKMH3uR7CVUkYfdNe0GCz8QOzAWrnqusQYFOg=="],
"@household/adapters-db": ["@household/adapters-db@workspace:packages/adapters-db"],
@@ -223,6 +236,10 @@
"@household/scripts": ["@household/scripts@workspace:scripts"],
+ "@internationalized/date": ["@internationalized/date@3.12.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ=="],
+
+ "@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
+
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
@@ -237,6 +254,10 @@
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
+ "@kobalte/core": ["@kobalte/core@0.13.11", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@internationalized/date": "^3.4.0", "@internationalized/number": "^3.2.1", "@kobalte/utils": "^0.9.1", "@solid-primitives/props": "^3.1.8", "@solid-primitives/resize-observer": "^2.0.26", "solid-presence": "^0.1.8", "solid-prevent-scroll": "^0.1.4" }, "peerDependencies": { "solid-js": "^1.8.15" } }, "sha512-hK7TYpdib/XDb/r/4XDBFaO9O+3ZHz4ZWryV4/3BfES+tSQVgg2IJupDnztKXB0BqbSRy/aWlHKw1SPtNPYCFQ=="],
+
+ "@kobalte/utils": ["@kobalte/utils@0.9.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w=="],
+
"@nothing-but/utils": ["@nothing-but/utils@0.17.0", "", {}, "sha512-TuCHcHLOqDL0SnaAxACfuRHBNRgNJcNn9X0GiH5H3YSDBVquCr3qEIG3FOQAuMyZCbu9w8nk2CHhOsn7IvhIwQ=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.51.0", "", { "os": "android", "cpu": "arm" }, "sha512-jJYIqbx4sX+suIxWstc4P7SzhEwb4ArWA2KVrmEuu9vH2i0qM6QIHz/ehmbGE4/2fZbpuMuBzTl7UkfNoqiSgw=="],
@@ -341,8 +362,14 @@
"@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sav+l+PL+74z3yaftVs7qd8c2SXkqzuxPOVibUe5wYMt+U5Hxp3V3XCPgBPN2I6cANjvoFtz0NiU8uHVLdi9FQ=="],
+ "@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA=="],
+
+ "@solid-primitives/map": ["@solid-primitives/map@0.4.13", "", { "dependencies": { "@solid-primitives/trigger": "^1.1.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew=="],
+
"@solid-primitives/media": ["@solid-primitives/media@2.3.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-LX9fB5WDaK87FMDtUB1qokBOfT2et9Uobv/zZaKLH9caFSz4+P70MBKEIBHcZQy+9MV5M2XvGYLTbLskjkzMjA=="],
+ "@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="],
+
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="],
"@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw=="],
@@ -355,8 +382,12 @@
"@solid-primitives/styles": ["@solid-primitives/styles@0.1.3", "", { "dependencies": { "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-7YdA21prMeCX+oOF/1RAn02+cGz/pG4dyPWtHBC2H8aZvnC7IfThBt80mP+TioejrdfE7Lc54Uh18f7Pig+gRQ=="],
+ "@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng=="],
+
"@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="],
+ "@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="],
+
"@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
@@ -389,6 +420,10 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
+ "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
+
+ "@tanstack/solid-query": ["@tanstack/solid-query@5.90.23", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-pbZc4+Kgm7ktzIuu01R3KOWfazQKgNp4AZvW0RSvv+sNMpYoileUDAkXEcjDJe6RJmb3fVvTR4LlcSL5pxDElQ=="],
+
"@twa-dev/sdk": ["@twa-dev/sdk@8.0.2", "", { "dependencies": { "@twa-dev/types": "^8.0.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Pp5GxnxP2blboVZFiM9aWjs4cb8IpW3x2jP3kLOMvIqy0jzNUTuFHkwHtx+zEvh/UcF2F+wmS8G6ebIA0XPXcg=="],
"@twa-dev/types": ["@twa-dev/types@8.0.2", "", {}, "sha512-ICQ6n4NaUPPzV3/GzflVQS6Nnu5QX2vr9OlOG8ZkFf3rSJXzRKazrLAbZlVhCPPWkIW3MMuELPsE6tByrA49qA=="],
@@ -457,6 +492,10 @@
"caniuse-lite": ["caniuse-lite@1.0.30001776", "", {}, "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw=="],
+ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -681,6 +720,10 @@
"solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="],
+ "solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="],
+
+ "solid-prevent-scroll": ["solid-prevent-scroll@0.1.10", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw=="],
+
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
@@ -711,6 +754,8 @@
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],