Skip to content

Commit d28959a

Browse files
Merge pull request #157 from appunite/feat/invoice-show-access-redesign
feat: invoice show redesign, design-system kit, and analyst role access
2 parents 155bdc5 + 99be246 commit d28959a

51 files changed

Lines changed: 3857 additions & 1021 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ Every module **must** have:
186186
187187
Before writing inline HTML in a LiveView template, check `CoreComponents` first. Domain-specific components live in `InvoiceComponents`, `CertificateComponents`, and `SettingsComponents`. Extract shared patterns when they appear 3+ times. For the full component reference, color tokens, DaisyUI usage, layout system, typography, and common page patterns, see `docs/frontend.md`. When generating frontend code, use the `/frontend` skill.
188188
189+
For **visual** decisions (what a new surface should look like, empty-state copy, badge tones, layout density), the source of truth is the prototype kit under `docs/design_system/`. Invoke the `/ksef-hub-design` skill or read `DESIGN_SYSTEM.md` before designing any new UI — don't invent tokens, patterns, or copy that aren't already in the kit.
190+
189191
## Testing
190192
191193
We follow TDD (red-green-refactor). Prefer using ExUnit with `async: true` when safe (avoid for DB or integration tests that share resources), ExMachina for test data factories, and Mox for mocking external services. For test structure, factory patterns, Mox setup, and fixture conventions, see `docs/tests.md`.

DESIGN_SYSTEM.md

Lines changed: 70 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Key semantic pairs:
3838

3939
---
4040

41-
## 2. The 11 rules that make it look right
41+
## 2. The 10 rules that make it look right
4242

4343
These are the ones Claude Code gets wrong most often. Treat as constraints.
4444

@@ -47,12 +47,11 @@ These are the ones Claude Code gets wrong most often. Treat as constraints.
4747
3. **Status → always a `<Badge variant="…">`** (not a raw span). Variants: `success | warning | error | info | muted | purple | brand`. Never colored dots alone.
4848
4. **Row density is tight.** Table cells: `px-4 py-3`. Headers: `px-4 py-2.5 text-xs uppercase tracking-wide text-[var(--muted-foreground)] font-medium` on `bg-[var(--muted)]/50`. No extra padding "to breathe."
4949
5. **Row hover is `bg-[var(--accent)]`.** Selected row gets the same. A trailing chevron may appear on hover only: `opacity-0 group-hover:opacity-100`.
50-
6. **Empty state string: "No data for selected period".** Centered, `text-sm text-[var(--muted-foreground)]`, `py-12`. Don't invent copy.
50+
6. **Empty states follow the two-pattern rule in §7.** Filter-empty = terse string `"No data for selected period"`. Zero-data empty = `<EmptyState>` component. Never mix them.
5151
7. **Bulk action bar on selection**: inverted — `bg-[var(--foreground)] text-[var(--background)]` above the table header row, with count · summary · actions (secondary inline buttons, not full `Button` components inside).
5252
8. **Tabs above tables**: underline style. Active tab has a 2px bottom bar (`bg-[var(--foreground)]`), inactive is `text-[var(--muted-foreground)]`. Each tab shows a count pill (see §4).
5353
9. **Summary tiles are always 3-up** (`grid grid-cols-1 sm:grid-cols-3 gap-3 mb-5`). Structure: uppercase label, `text-2xl font-semibold tabular-nums` value, muted sub-line. Currency suffix in `font-mono text-[var(--muted-foreground)]`.
5454
10. **Focus rings are thin.** `focus:ring-1 focus:ring-[var(--ring)]`, never `ring-2` or `ring-offset-*`. No glowy focus.
55-
11. **Interaction feel is `transition-all` + `active:scale-[0.98]`, nothing else.** All interactive elements animate with `transition-all` (150ms ease-out default). Hover = `opacity-90` on filled variants, `bg-[var(--accent)]` on outline/ghost. Pressed = `active:scale-[0.98]` on buttons; outline/ghost also get `active:opacity-80`. Row hover stays `bg-[var(--accent)]` — no scale. Never add box-shadow lifts, color shifts, or `hover:-translate-*` transforms.
5655

5756
---
5857

@@ -66,9 +65,6 @@ Use these exact components from `Primitives.jsx`. Don't re-implement.
6665
- Heights are fixed: `default h-9`, `sm h-7`, `icon h-9 w-9`
6766
- Always `rounded-md` (not pill, not square)
6867
- Icon before label with `gap-2`
69-
- Base always includes: `transition-all active:scale-[0.98] focus:ring-1 focus:ring-[var(--ring)]`
70-
- Filled variants (`primary`, `destructive`, `brand`): `hover:opacity-90`
71-
- Unfilled variants (`outline`, `ghost`): `hover:bg-[var(--accent)]` + `active:opacity-80`
7268

7369
### `<Badge variant>`
7470
- Variants as listed in rule 3
@@ -87,6 +83,12 @@ Use these exact components from `Primitives.jsx`. Don't re-implement.
8783
- Fixed 24×24 viewBox, `stroke="currentColor" strokeWidth="1.5"`. Size in px.
8884
- Only use names present in `Primitives.jsx`. If you need a new one, add it there — don't inline SVGs elsewhere.
8985

86+
### `<EmptyState icon title sub action tone>`
87+
- The canonical zero-data surface. See §7 for when to use it vs. the filter-empty string.
88+
- `tone`: `default | locked | warning`
89+
- `action` is an optional `<Button size="sm">` — typically `variant="primary"` for a direct CTA (e.g. *Add note*) or `variant="outline"` for a softer nudge (e.g. *Write a comment*).
90+
- `title` max ~40 chars; `sub` max ~140 chars. No marketing fluff.
91+
9092
### `<PageHeader title subtitle actions>`
9193
- Every page starts with this. Bottom border, `mb-6`.
9294
- Actions are right-aligned; primary always rightmost.
@@ -172,45 +174,7 @@ Bottom-right, auto-dismiss 3.2s. `fixed bottom-6 right-6 z-50 px-3.5 py-2.5 roun
172174

173175
---
174176

175-
## 5. Typography inventory (table-specific)
176-
177-
The 4-tier rule — use this to decide any unlisted element:
178-
- `text-sm (14px) sans` — human-readable primary text (names, titles)
179-
- `text-xs (12px) mono tabular-nums` — identifiers, dates, currency codes, copy-paste strings
180-
- `text-[11px] mono tabular-nums muted` — secondary line under a primary (NIP, IBAN, net amount)
181-
- `text-[10px]` — tertiary labels (date sub-labels, confidence %, void reasons)
182-
183-
### Table header (`<thead>`)
184-
| Element | Size | Weight | Notes |
185-
|---------|------|--------|-------|
186-
| Column label | `text-xs` | `font-medium` | `uppercase tracking-wide text-muted-foreground` |
187-
| Header row | `py-2.5` || `bg-muted/50 border-b` |
188-
189-
### Table body (`<tbody>`)
190-
| Cell content | Size | Font | Notes |
191-
|-------------|------|------|-------|
192-
| Primary text (seller, counterparty name) | `text-sm` | sans | truncated |
193-
| IDs / invoice numbers / references | `text-xs` | mono | `tabular-nums` |
194-
| Secondary mono line (NIP, IBAN under name) | `text-[11px]` | mono | `text-muted-foreground` |
195-
| Amount (gross / primary) | `text-sm` | mono | `tabular-nums whitespace-nowrap leading-tight` |
196-
| Currency suffix | `text-xs` || `text-muted-foreground ml-1` |
197-
| Net amount (under gross) | `text-[11px]` | mono | `tabular-nums text-muted-foreground` |
198-
| Date | `text-xs` | mono | `tabular-nums text-muted-foreground whitespace-nowrap` |
199-
| Status / Kind badge text | `text-xs` | `font-medium` | via `<Badge>` |
200-
| Category badge text | `text-xs` | sans | |
201-
| Category confidence (`· 82%`) | `text-[10px]` | sans | `text-muted-foreground` |
202-
203-
### Above/below the table
204-
| Element | Size | Notes |
205-
|---------|------|-------|
206-
| Tab label | `text-sm` | active: `font-medium` |
207-
| Tab count pill | `text-[11px] mono` | `min-w-[20px] h-[18px]` |
208-
| Filter chips / date picker / search | `text-xs` | `h-8` controls |
209-
| Pagination text | `text-xs` | `text-muted-foreground` |
210-
211-
---
212-
213-
## 6. Column conventions (invoice-like tables)
177+
## 5. Column conventions (invoice-like tables)
214178

215179
Fixed column order when applicable:
216180

@@ -225,7 +189,7 @@ Fixed column order when applicable:
225189

226190
---
227191

228-
## 7. Do / Don't
192+
## 6. Do / Don't
229193

230194
**Do**
231195
- Reach for existing primitives first (`Badge`, `Button`, `Card`, `Input`, `Icon`).
@@ -236,8 +200,6 @@ Fixed column order when applicable:
236200

237201
**Don't**
238202
- Don't add gradients, emoji, drop shadows on cards, or colored left borders on rows.
239-
- Don't add `hover:-translate-*`, shadow lifts, or color-shift hover effects on buttons — use `hover:opacity-90` (filled) or `hover:bg-[var(--accent)]` (outline/ghost) only.
240-
- Don't use `transition-colors` on buttons — always `transition-all` so the native `:active` state animates too.
241203
- Don't introduce solid-filled status pills (always tinted).
242204
- Don't use `<div>`s where `<Badge>` should go.
243205
- Don't add new colors outside the token set — compose with `color-mix(in oklch, var(--x) 10%, transparent)` if you need a faint surface.
@@ -248,13 +210,69 @@ Fixed column order when applicable:
248210

249211
---
250212

213+
## 7. Empty states
214+
215+
Two distinct patterns. Pick by asking: **"Would clearing filters reveal content?"**
216+
217+
### Pattern A — Filter-empty
218+
*A list has data, but current filters hide all of it.*
219+
220+
- Copy: `"No data for selected period"` — exact string, no variations.
221+
- Styling: `text-sm text-[var(--muted-foreground)] py-12` centered inside the table body.
222+
- **No icon, no CTA.** The resolution (clear filters) lives above the table.
223+
224+
Used by: Invoices list, Payments list, Sync jobs, Companies search, any future table.
225+
226+
### Pattern B — Zero-data
227+
*There is genuinely nothing here yet, for this record or feature.*
228+
229+
Use the `<EmptyState>` component. Three tones:
230+
231+
| Tone | When | CTA? |
232+
|---|---|---|
233+
| `default` | Nothing here yet, but the user could create something | Optional primary CTA |
234+
| `locked` | Feature doesn't apply to this record (e.g. Payments on an income invoice) | Never — don't fake-tease a feature |
235+
| `warning` | Something failed (OCR, sync) and needs user action | Always — `Retry …` |
236+
237+
### Rules
238+
239+
1. **One paragraph, max 140 chars** in `sub`. No marketing fluff.
240+
2. **Never use the filter-empty string for a zero-data state** — copy must match the real cause.
241+
3. **CTAs from empty states are never destructive.** No "Delete invoice" on empty Payments.
242+
4. **Locked states don't show disabled CTAs.** State *why* it doesn't apply, then stop.
243+
5. **No illustrations, no confetti, no color.** Same muted circle icon used everywhere else. *Exception:* `tone="warning"` (see `EmptyState` in `Primitives.jsx`) applies the warning tint to the circle icon because the state signals a real failure that needs attention; it must still pair with a succinct `sub` and a retry CTA. All other tones stay colorless.
244+
6. **Activity logs are never empty.** A fetch event always exists — don't ship an empty state for Activity.
245+
246+
### Invoice detail tab examples
247+
248+
| Tab | Condition | Tone | Copy |
249+
|---|---|---|---|
250+
| Line items | `extraction=failed` | `warning` | *Line items couldn't be extracted* — The OCR run failed. Retry or enter items manually. |
251+
| Payments | No requests, invoice is expense + approved | `default` | *No payment requests yet* — Create one to schedule a transfer for this invoice. |
252+
| Payments | Invoice is income or rejected | `locked` | *Payments don't apply here* — Income and rejected invoices don't generate outgoing transfers. |
253+
| Notes | None | `default` | *No notes yet* — Notes are private to your team. Use them to record decisions or context. |
254+
| Comments | None | `default` | *Start the conversation*@mention a teammate to pull them in. |
255+
| Access | Owner only | `default` | *You're the sole user with access* — Share this invoice with teammates to collaborate. |
256+
257+
### App-wide examples
258+
259+
| Surface | State | Tone | Copy |
260+
|---|---|---|---|
261+
| Dashboard tile | New tenant, no invoices yet | `default` | *No invoices this month yet* — Sync runs hourly, or add one manually. |
262+
| Companies | New tenant | `default` | *Add your first company* — Register a NIP to start syncing invoices, or wait for an invitation from an existing company. |
263+
| Settings → Certificates | Not uploaded | `warning` | *No certificate uploaded* — Upload a XAdES certificate to enable KSeF sync. |
264+
| Sync jobs | New tenant | `default` | *No sync jobs yet* — KSeF sync runs hourly, or trigger one manually. |
265+
| Payments list | New tenant | `default` | *No payment requests yet* — Approve an expense to generate your first payment. |
266+
267+
---
268+
251269
## 8. Handoff workflow (recommended)
252270

253-
1. **Commit this file + the prototype** into the real repo under `docs/design-system/`.
271+
1. **Commit this file + the prototype** into the real repo under `docs/design_system/` (this file lives at the repo root as `DESIGN_SYSTEM.md`; the HTML/JSX kit lives under `docs/design_system/`).
254272
2. **Per screen** in the real app, open a PR that links to the matching prototype file. In the PR description, paste:
255273
- Screenshot of prototype screen
256274
- Screenshot of current real screen
257-
- Checklist of the 11 rules from §2
275+
- Checklist of the 10 rules from §2
258276
3. **Ask Claude Code one screen at a time**, e.g.:
259-
> Port `app/views/payments/index.tsx` to match `ui_kits/admin/Payments.jsx`. Follow `docs/design-system/DESIGN_SYSTEM.md`. Reuse `Button`, `Badge`, `Card` from `app/components/ui/`. Keep existing data hooks. Output a diff, don't touch other files.
260-
4. **Review against §2**. Almost every miss is one of those 11 rules.
277+
> Port `app/views/payments/index.tsx` to match `docs/design_system/ui_kits/admin/Payments.jsx`. Follow `DESIGN_SYSTEM.md` at the repo root. Reuse `Button`, `Badge`, `Card` from `app/components/ui/`. Keep existing data hooks. Output a diff, don't touch other files.
278+
4. **Review against §2**. Almost every miss is one of those 10 rules.

assets/js/app.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ const LocalTime = {
173173
}
174174
}
175175

176+
// Selects the entire contents of a text input on click — convenient for
177+
// read-only URLs users typically want to copy.
178+
const SelectOnClick = {
179+
mounted() {
180+
this._handler = () => this.el.select()
181+
this.el.addEventListener("click", this._handler)
182+
},
183+
destroyed() {
184+
this.el.removeEventListener("click", this._handler)
185+
}
186+
}
187+
176188
// Shared calendar picker hook logic.
177189
// calendarSelector: CSS selector for the Cally web component
178190
// onSelect: (hook, calendarValue) => void — updates hidden inputs from the selected value
@@ -340,7 +352,7 @@ const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute
340352
const liveSocket = new LiveSocket("/live", Socket, {
341353
longPollFallbackMs: 2500,
342354
params: {_csrf_token: csrfToken},
343-
hooks: {...colocatedHooks, ExpenseBarChart, CategoryDonutChart, MultiSelectSearch, LocalTime, DateRangePicker, DatePicker, MonthRangePicker},
355+
hooks: {...colocatedHooks, ExpenseBarChart, CategoryDonutChart, MultiSelectSearch, LocalTime, SelectOnClick, DateRangePicker, DatePicker, MonthRangePicker},
344356
})
345357

346358
// Show progress bar on live navigation and form submits

docs/design_system/README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Use this skill whenever you're designing **anything KSeF Hub-branded** -- admin
6666
| Expiring soon | "Your KSeF certificate expires in 5 days." |
6767
| Duplicate suspected | "This invoice may be a duplicate. View original." |
6868
| Connection lost flash | "We can't find the internet" * "Attempting to reconnect" |
69-
| Empty state | "No data for selected period" |
69+
| Empty state (filter result, table has data elsewhere) | "No data for selected period"*for zero-data states (`<EmptyState>`), see `DESIGN_SYSTEM.md` §7 at the repo root* |
7070
| Pagination | "Showing 1-25 of 412 invoices" * "Page 3 of 17" |
7171
| Manual action | "Sync Now" * "Not a duplicate" * "Confirm duplicate" |
7272
| Prediction hint | "Predicted with 87.3% probability, feel free to adjust" * "Manually adjusted" |
@@ -129,15 +129,14 @@ Use this skill whenever you're designing **anything KSeF Hub-branded** -- admin
129129

130130
### Animation
131131

132-
- **Only** what LiveView already does: `topbar` loading indicator (`#29d` blue, 300ms show delay), `motion-safe:animate-spin` on loading icons, CSS transitions on hover/focus/press (`transition-all`, ~150ms ease-out default).
132+
- **Only** what LiveView already does: `topbar` loading indicator (`#29d` blue, 300ms show delay), `motion-safe:animate-spin` on loading icons, CSS transitions on hover/focus (`transition-colors`, ~150ms default).
133133
- Flash toasts: `transition-all ease-out 300ms` on show, `ease-in 200ms` on hide, combined translate-y + scale + opacity. See `show/hide` helpers.
134134
- No bounces, no entrance choreography, no scroll-jacking in marketing.
135135

136136
### Hover & press
137137

138138
- **Hover:** `hover:bg-shad-accent hover:text-shad-accent-foreground` for ghost/outline buttons and nav items. For primary buttons: `hover:bg-shad-primary/90`. Links: `underline-offset-4 hover:underline`.
139-
- **Press / active:** buttons use `active:scale-[0.98]`; outline/ghost variants additionally get `active:opacity-80`.
140-
- **Focus:** `focus-visible:ring-1 focus-visible:ring-ring`.
139+
- **Press / active:** no custom active state; rely on browser focus + `focus-visible:ring-1 focus-visible:ring-ring`.
141140
- **Disabled:** `disabled:pointer-events-none disabled:opacity-50`.
142141
- **Cursor:** `cursor-pointer` is explicitly applied to interactive elements -- it is not the browser default here because Tailwind resets it.
143142

docs/design_system/preview/components-buttons.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
.sm{height:28px;padding:0 10px;font-size:12px}
1515
.icon{height:36px;width:36px;padding:0}
1616
.brand{background:var(--brand);color:var(--brand-foreground)}
17-
.btn:disabled{opacity:.5;pointer-events:none}
1817
</style></head><body>
1918
<div class="row" style="margin-bottom:10px">
2019
<button class="btn primary">Sync Now</button>
@@ -27,6 +26,6 @@
2726
<button class="btn primary sm">Approve</button>
2827
<button class="btn outline sm">Dismiss</button>
2928
<button class="btn ghost icon" aria-label="Close">×</button>
30-
<button class="btn primary" disabled>Disabled</button>
29+
<button class="btn primary" disabled style="opacity:.5;pointer-events:none">Disabled</button>
3130
</div>
3231
</body></html>

docs/design_system/ui_kits/admin/AppShell.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ const MobileNav = ({ page, onNav }) => {
114114
);
115115
};
116116

117-
const AppShell = ({ page, onNav, children, company, companies, onPickCompany, user, onLogout = () => {} }) => (
117+
const AppShell = ({ page, onNav, children, company, companies, onPickCompany, user }) => (
118118
<div className="min-h-screen flex flex-col bg-[var(--background)] text-[var(--foreground)]">
119119
<header className="sticky top-0 z-30 w-full border-b border-[var(--border)] bg-[color-mix(in_oklch,var(--background)_95%,transparent)] backdrop-blur">
120120
<div className="flex h-14 items-center px-4 lg:px-6 gap-4">
@@ -131,7 +131,7 @@ const AppShell = ({ page, onNav, children, company, companies, onPickCompany, us
131131
</nav>
132132
<div className="flex-1" />
133133
<CompanySelector current={company} companies={companies} onPick={onPickCompany} />
134-
<AvatarMenu email={user.email} onLogout={onLogout} />
134+
<AvatarMenu email={user.email} onLogout={() => {}} />
135135
</div>
136136
</header>
137137
<main className="flex-1 p-4 sm:p-6 lg:p-8">

0 commit comments

Comments
 (0)