Compare commits

..

No commits in common. "master" and "ui-mobile" have entirely different histories.

5 changed files with 32 additions and 124 deletions

View File

@ -68,8 +68,8 @@ Theme state lives in `ui/src/theme.jsx` — a React context (`ThemeContext`) wit
`ThemeProvider` that wraps the app in `main.jsx`. `ThemeProvider` that wraps the app in `main.jsx`.
- **Storage key:** `df_dark` in `localStorage`; falls back to `window.matchMedia('(prefers-color-scheme: dark)')` on first visit - **Storage key:** `df_dark` in `localStorage`; falls back to `window.matchMedia('(prefers-color-scheme: dark)')` on first visit
- **Toggle:** button at the foot of the sidebar (`Sidebar.jsx`), and in `BottomNav.jsx` on mobile; the effect writes `localStorage` and toggles the `.dark` class on `<html>` - **Toggle:** button in the sidebar header in `App.jsx`; effect writes `localStorage` and toggles the `.dark` class on `<html>`
- **CSS:** `ui/src/index.css` declares semantic tokens under `@theme` (`bg-surface`, `text-ink`, `text-muted`, `border-line`, `text-danger`, …) that resolve to CSS custom properties redefined by `.dark`. **Write components against the tokens, never against literal shades like `bg-white` or `text-gray-400`** — the old per-utility `.dark .bg-white { … }` overrides are gone and must not come back - **CSS:** `ui/src/index.css` defines CSS custom properties under `:root` (light) and `.dark`. All Tailwind color overrides are written as `.dark .bg-white { ... }` etc.
- **Palette:** dark mode uses Perspective's "Pro Dark" colours (`--bg-primary: #242526`, panels `#2a2c2f`, gridlines `#3b3f46`, text `#c5c9d0`) - **Palette:** dark mode uses Perspective's "Pro Dark" colours (`--bg-primary: #242526`, panels `#2a2c2f`, gridlines `#3b3f46`, text `#c5c9d0`)
- **Perspective viewer:** `Pivot.jsx` calls `viewer.setAttribute('theme', dark ? 'Pro Dark' : 'Pro Light')` on initial load and in a `useEffect([dark])` so the viewer stays in sync when the toggle fires - **Perspective viewer:** `Pivot.jsx` calls `viewer.setAttribute('theme', dark ? 'Pro Dark' : 'Pro Light')` on initial load and in a `useEffect([dark])` so the viewer stays in sync when the toggle fires
- **Consuming the theme:** `import useTheme from '../theme.jsx'` then `const { dark, setDark } = useTheme()` - **Consuming the theme:** `import useTheme from '../theme.jsx'` then `const { dark, setDark } = useTheme()`

View File

@ -59,14 +59,10 @@ api/
ui/ ui/
src/ src/
api.js — fetch wrapper, credential management api.js — fetch wrapper, credential management
App.jsx — root: login gate, routing, stale/reprocess banners App.jsx — root: login gate, sidebar, source selector, routing
index.css — semantic colour tokens for light and dark
pages/ pages/
Login.jsx — username/password form Login.jsx — username/password form
SourceList.jsx — source list and the create dialog Sources.jsx — source CRUD, field config, view generation
SourceDetail.jsx — one source: connection, fields, view, maintenance
Bridge.jsx — SimpleFIN accounts, balances, and subtotals
ImportHub.jsx — all sources with sync / upload actions
Import.jsx — CSV upload, SimpleFIN sync, and import log Import.jsx — CSV upload, SimpleFIN sync, and import log
Rules.jsx — rule CRUD with live pattern preview Rules.jsx — rule CRUD with live pattern preview
Mappings.jsx — mapping table with TSV import/export Mappings.jsx — mapping table with TSV import/export
@ -75,7 +71,7 @@ ui/
Stacks.jsx — multi-source union views with running balance Stacks.jsx — multi-source union views with running balance
Remap.jsx — bulk remap of an output field value across mappings Remap.jsx — bulk remap of an output field value across mappings
Log.jsx — global import log across all sources Log.jsx — global import log across all sources
components/ — Sidebar, BottomNav, navItems, SourceTabs, Section, SampleTable components/ — Sidebar, StatusBar
theme.jsx — light/dark context provider theme.jsx — light/dark context provider
public/ — compiled UI (output of npm run build in ui/) public/ — compiled UI (output of npm run build in ui/)
docs/ — this file, tutorial, UI and Perspective references docs/ — this file, tutorial, UI and Perspective references
@ -331,43 +327,11 @@ Built with React + Vite + Tailwind CSS. Compiled output goes to `public/`. The s
3. On 401 response, credentials are cleared and the login screen is shown. 3. On 401 response, credentials are cleared and the login screen is shown.
4. `localStorage` persists the selected source name across sessions. 4. `localStorage` persists the selected source name across sessions.
**Navigation.** The selected source is a route parameter, not global state: `/sources`
lists sources, `/sources/:name` owns one, and Import, Rules, Mappings, Records, and Pivot
are tabs beneath it (`components/SourceTabs.jsx`). The sidebar holds only top-level
destinations — Sources, Import, Bridge, Remap, Stacks, Log — defined once in
`components/navItems.jsx` and rendered by `Sidebar.jsx` on desktop and `BottomNav.jsx`
below the `md:` breakpoint. Out-of-sync and reprocess banners render above every page from
`App.jsx`.
**Colour.** Components use semantic tokens (`bg-surface`, `text-ink`, `text-muted`,
`border-line`, `text-danger`) declared in `index.css` under `@theme`, which resolve to CSS
variables redefined by `.dark`. There are no per-utility `.dark` override rules; a new
component gets both themes for free.
**Bundle.** `Pivot` is loaded with `React.lazy`, keeping Perspective (~4.7 MB gzipped) out
of the initial download and in a chunk fetched only when a pivot is opened.
**Pages:** **Pages:**
- **Sources** (`SourceList.jsx`) — Lists every source with its constraint fields and a - **Sources** — View and edit source configuration. Shows all known field names and their origins (raw data, schema, rules, mappings). Checkboxes control which fields are constraint fields and which appear in the output view. Supports CSV upload to auto-detect fields.
badge for bank feeds; clicking opens it. "New source" opens the create dialog, which can
seed fields from a CSV sample or from a linked SimpleFIN account.
- **Source detail** (`SourceDetail.jsx`) — The Setup tab, grouped into titled panels: - **Import** — Upload a CSV to import records into the selected source. Transformations run automatically on new records. Shows import log with inserted/duplicate counts, expandable key detail, checkbox selection, and delete with confirmation. Sources with `config.simplefin.account_id` also get a Sync panel — a window selector (10/30/90 days or everything) and a "Sync now" button that pulls from the bank API through the same import path.
Connection (link/unlink a SimpleFIN account, warns when constraint fields aren't `id`),
Fields and view (all known field names and their origins, with checkboxes for constraint
fields and view columns), Sample rows, Maintenance (reprocess), and Delete source.
- **Bridge** (`Bridge.jsx`) — Every account behind the SimpleFIN credential with balances,
the source each maps to, and subtotals split into banking versus retirement (recognised by
keyword on the account and institution names). Queries SimpleFIN only when Refresh is
pressed. Balances render in accounting style with negatives in parentheses.
- **Import** (`ImportHub.jsx`) — Top-level entry point for the frequent job. Lists every
source with record counts and last import date, a Sync button for bank feeds, and an
upload link for CSV sources.
- **Source Import tab** — Upload a CSV to import records into the selected source. Transformations run automatically on new records. Shows import log with inserted/duplicate counts, expandable key detail, checkbox selection, and delete with confirmation. Sources with `config.simplefin.account_id` also get a Sync panel — a window selector (10/30/45 days or a full backfill) and a "Sync now" button that pulls from the bank API through the same import path.
- **Rules** — Create and manage regex rules. Live preview fires automatically (debounced 500ms) as pattern/field/flags are edited, showing match results against real records. Rules can be enabled/disabled by toggle. - **Rules** — Create and manage regex rules. Live preview fires automatically (debounced 500ms) as pattern/field/flags are edited, showing match results against real records. Rules can be enabled/disabled by toggle.
@ -394,8 +358,7 @@ of the initial download and in a chunk fetched only when a pivot is opened.
See `docs/perspective.md` for the full technical reference on controlling Perspective programmatically. See `docs/perspective.md` for the full technical reference on controlling Perspective programmatically.
- **Stacks** — Named unions of multiple sources, each chip linking to its pivot at - **Stacks** — Named unions of multiple sources. Each stack defines a field mapping (how source fields map to common output columns), an amount field, a date field, and an optional balance offset. The view-data endpoint unions the underlying source views and computes a running balance sorted by date. The Pivot page supports stacks as well as individual sources, with layouts stored in the same `pivot_layouts` table.
`/stacks/:name/pivot`. Each stack defines a field mapping (how source fields map to common output columns), an amount field, a date field, and an optional balance offset. The view-data endpoint unions the underlying source views and computes a running balance sorted by date. The Pivot page supports stacks as well as individual sources, with layouts stored in the same `pivot_layouts` table.
- **Log** — Global import log across all sources. Same expandable key detail and delete capability as the Import page, plus a source name column. - **Log** — Global import log across all sources. Same expandable key detail and delete capability as the Import page, plus a source name column.

View File

@ -141,7 +141,6 @@ export default function App() {
setExpanded={setSidebarExpanded} setExpanded={setSidebarExpanded}
loginUser={loginUser} loginUser={loginUser}
onLogout={handleLogout} onLogout={handleLogout}
sources={sources}
/> />
</div> </div>
@ -202,8 +201,7 @@ export default function App() {
<Route path="/sources" element={<SourceList sources={sources} setSources={setSources} setSource={setSource} />} /> <Route path="/sources" element={<SourceList sources={sources} setSources={setSources} setSource={setSource} />} />
<Route path="/sources/:name" element={<SourceTabs sources={sources} />}> <Route path="/sources/:name" element={<SourceTabs sources={sources} />}>
<Route index element={<Navigate to="records" replace />} /> <Route index element={<SourceDetail sources={sources} setSources={setSources} />} />
<Route path="setup" element={<SourceDetail sources={sources} setSources={setSources} />} />
<Route path="import" element={<ScopedToSource component={Import} />} /> <Route path="import" element={<ScopedToSource component={Import} />} />
<Route path="rules" element={<ScopedToSource component={Rules} onStale={markSourceStale} />} /> <Route path="rules" element={<ScopedToSource component={Rules} onStale={markSourceStale} />} />
<Route path="mappings" element={<ScopedToSource component={Mappings} onNeedsReprocess={markNeedsReprocess} />} /> <Route path="mappings" element={<ScopedToSource component={Mappings} onNeedsReprocess={markNeedsReprocess} />} />

View File

@ -1,37 +1,10 @@
import { Fragment, useMemo } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import useTheme from '../theme.jsx' import useTheme from '../theme.jsx'
import { NAV } from './navItems.jsx' import { NAV } from './navItems.jsx'
// Same distinction the Import page makes: a source is either on a bank feed or export default function Sidebar({ expanded, setExpanded, loginUser, onLogout }) {
// it gets CSVs uploaded to it.
const feedIcon = (
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
<path d="M4 10.5a4.5 4.5 0 0 1 4.5 4.5"/>
<path d="M4 6a9 9 0 0 1 9 9"/>
<circle cx="4.2" cy="14.8" r="1.2" fill="currentColor" stroke="none"/>
</svg>
)
const csvIcon = (
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 1.5h5l3 3v10H4z"/>
<polyline points="9,1.5 9,4.5 12,4.5"/>
</svg>
)
export default function Sidebar({ expanded, setExpanded, loginUser, onLogout, sources = [] }) {
const { dark, setDark } = useTheme() const { dark, setDark } = useTheme()
// Bank feeds first, then CSV sources, alphabetical within each group
const navSources = useMemo(() => (
sources
.map(s => ({ ...s, isFeed: !!s.config?.simplefin?.account_id }))
.sort((a, b) =>
(b.isFeed - a.isFeed) || a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
)
), [sources])
return ( return (
<div <div
className="bg-surface border-r border-line flex flex-col shrink-0 overflow-hidden transition-all duration-150" className="bg-surface border-r border-line flex flex-col shrink-0 overflow-hidden transition-all duration-150"
@ -59,12 +32,11 @@ export default function Sidebar({ expanded, setExpanded, loginUser, onLogout, so
</div> </div>
{/* Nav */} {/* Nav */}
<nav className="flex flex-col gap-0.5 p-2 flex-1 overflow-y-auto"> <nav className="flex flex-col gap-0.5 p-2 flex-1">
{NAV.map(({ to, label, icon }) => ( {NAV.map(({ to, label, icon }) => (
<Fragment key={to}>
<NavLink <NavLink
key={to}
to={to} to={to}
end={to === '/sources'}
title={!expanded ? label : undefined} title={!expanded ? label : undefined}
className={({ isActive }) => className={({ isActive }) =>
`flex items-center gap-3 px-2 py-2 rounded w-full transition-colors ${ `flex items-center gap-3 px-2 py-2 rounded w-full transition-colors ${
@ -82,29 +54,6 @@ export default function Sidebar({ expanded, setExpanded, loginUser, onLogout, so
{label} {label}
</span> </span>
</NavLink> </NavLink>
{/* Jump straight to a source. Collapsed there is no room for names,
so the shortcut list only exists when the sidebar is open. */}
{to === '/sources' && expanded && navSources.map(({ isFeed, ...s }) => {
return (
<NavLink
key={s.name}
to={`/sources/${encodeURIComponent(s.name)}`}
title={`${s.name}${isFeed ? 'bank feed' : 'CSV'}`}
className={({ isActive }) =>
`flex items-center gap-2 ml-4 pl-2 pr-2 py-1 rounded border-l border-line-soft transition-colors ${
isActive
? 'bg-accent-soft text-accent'
: 'text-muted hover:bg-raised hover:text-ink'
}`
}
>
<span className="shrink-0 opacity-70">{isFeed ? feedIcon : csvIcon}</span>
<span className="text-xs truncate">{s.name}</span>
</NavLink>
)
})}
</Fragment>
))} ))}
</nav> </nav>

View File

@ -2,15 +2,13 @@ import { NavLink, Outlet, useParams, Link } from 'react-router-dom'
// Everything scoped to one source lives under /sources/:name, so the source is // Everything scoped to one source lives under /sources/:name, so the source is
// in the URL rather than in a global selector. // in the URL rather than in a global selector.
// Records is the tab you want nine times out of ten, so it leads and is what
// /sources/:name redirects to; Setup is the rare one and sits at the end.
const TABS = [ const TABS = [
{ to: 'records', label: 'Records' }, { to: '', label: 'Setup', end: true },
{ to: 'import', label: 'Import' }, { to: 'import', label: 'Import' },
{ to: 'rules', label: 'Rules' }, { to: 'rules', label: 'Rules' },
{ to: 'mappings', label: 'Mappings' }, { to: 'mappings', label: 'Mappings' },
{ to: 'records', label: 'Records' },
{ to: 'pivot', label: 'Pivot' }, { to: 'pivot', label: 'Pivot' },
{ to: 'setup', label: 'Setup' },
] ]
export default function SourceTabs({ sources }) { export default function SourceTabs({ sources }) {