* feat(inbox): skip AI extraction for multi-page PDFs (#553) Bedrock churns for minutes on multi-page PDFs (sales reports, bank statements, contracts) and returns nothing useful. Above 3 pages we now skip extraction entirely and mark the row with extraction_skipped=true; the document still lands in the inbox and can be attached or converted manually. Same gate applies to the /items/:id/attach path. Client can also opt out via skip_extraction=true (skip_reason=client_opt_out). The InvoiceInboxWorkspace renders an "Inte AI-tolkad" badge for skipped rows, distinct from the "Felaktig" failure state (status='error'). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(ui): introduce DataList + DropdownMenu primitives, roll out across list pages DataList replaces the per-row Card pattern across Granskning, Transactions, Invoices, Supplier invoices, and Pending. One bordered container with hairline rows matches the flat-with-hairlines aesthetic in CLAUDE.md — no shadows, no state-tinted borders, secondary token for selected/hover. DropdownMenu fills the gap for row-level action menus on TransactionInboxCard, TransactionHistoryList, and the page-level action menus on /transactions and /pending. Replaces ad-hoc Popover + buttons constructions. Migrates list pages and the transaction inbox/history components onto the new primitives. No behavior change beyond the visual unification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: add agent skills + gnubok domain skills, gitignore compliance reports .agents/skills/ + skills-lock.json + symlinks under .claude/skills/ check in the vercel-labs/agent-skills set pinned by the local skill manager (deploy-to-vercel, vercel-cli-with-tokens, react-best-practices, composition-patterns, react-native-skills, react-view-transitions, web-design-guidelines). Keeps the team on the same versions. .claude/skills/industry/ + .claude/skills/modifier/ are hand-authored vertical and entity-modifier skills for the specialized accountant agent — industries (konsult-it, e-handel, bygg-hantverk, reklambyra, saas-ai) and entity overlays (holding-ab, single-shareholder-ab-fmb, mixed- verksamhet). Project-owned content; lives in the repo by design. Also gitignores .compliance-reports/ — those are large generated SARIF/dossier artifacts from the compliance scanner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(transactions): unify inbox/history chrome, drop swipe flow The transactions page mixed two-tier filtering, a swipe-view detour, and per-row Card chrome that didn't carry its weight. This pass collapses those into a single editorial-list surface and removes the unused swipe path entirely. User-visible changes: - Mode toggle (Att bokföra / Alla transaktioner) moved from a Tabs row under the header into a dropdown to the right of a unified search bar. Search now persists when switching modes. - Removed the swipe categorization view ("Gå igenom alla") and its trigger button. The 800-line SwipeCategorizationView component is deleted; suggestion-fetching shrinks to what the template picker still consumes. - Inbox rows now show one primary action: invoice/supplier-invoice match shortcut when auto-detected, else "Bokför". A new visible Link2 icon button opens the customer or supplier invoice picker manually (chosen by amount sign). Delete becomes a plain trash button — no overflow menu since it only ever held one item. - Bulk action bar swaps "Markera som privat" for "Ta bort" with a single combined confirmation. - Built SupplierInvoicePicker mirroring InvoicePicker so expense transactions can be matched to supplier invoices from the inbox. Wired through /api/transactions/{id}/match-supplier-invoice. - Template picker dialog renamed to "Bokför transaktion"; "Bokför manuellt…" and "Matcha med faktura…" promoted from muted ghost buttons at the bottom to outline buttons at the top, above the template list. - Breathing room: row padding py-3 → py-4, primary text text-sm → text-base, amount text-base, button heights h-8 → h-9, trailing gap-2 → gap-3 (in the DataList primitive itself, so every list benefits slightly). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: align package-lock.json with merged package.json The merge resolution took origin/main's package-lock.json (which dropped pdf-lib) but kept our package.json (which still requires pdf-lib for the invoice-inbox extension's PDFDocument import). `npm ci` rejected the mismatch. Regenerate the lock from the merged package.json so both files agree. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: regenerate package-lock.json with npm@10 for CI compat Local npm@11 produced a lock that npm@10 (CI) rejected with "Missing: @swc/helpers@0.5.21". Regenerated with npm@10 --package-lock-only so CI can install. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): supplier-invoice status-leak guard + drop dead prop Two follow-ups from the merge-risk audit: - SupplierInvoicePicker now mirrors InvoicePicker's status-leak guard: if a supplier invoice is still 'approved'/'overdue' but already has a payment voucher attached (journal_entry_id on supplier_invoice_payments), hide it. Closes a UX race window between payment and status flip. Partially-paid invoices still pass through. - Drop the unused onMarkPrivate prop on TransactionInboxCard and the matching handleMarkPrivate wrapper in the parent. Both became dead when the swipe-categorisation flow was removed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.9 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Define Generic Context Interfaces for Dependency Injection | HIGH | enables dependency-injectable state across use-cases | composition, context, state, typescript, dependency-injection |
Define Generic Context Interfaces for Dependency Injection
Define a generic interface for your component context with three parts:
state, actions, and meta. This interface is a contract that any provider
can implement—enabling the same UI components to work with completely different
state implementations.
Core principle: Lift state, compose internals, make state dependency-injectable.
Incorrect (UI coupled to specific state implementation):
function ComposerInput() {
// Tightly coupled to a specific hook
const { input, setInput } = useChannelComposerState()
return <TextInput value={input} onChangeText={setInput} />
}
Correct (generic interface enables dependency injection):
// Define a GENERIC interface that any provider can implement
interface ComposerState {
input: string
attachments: Attachment[]
isSubmitting: boolean
}
interface ComposerActions {
update: (updater: (state: ComposerState) => ComposerState) => void
submit: () => void
}
interface ComposerMeta {
inputRef: React.RefObject<TextInput>
}
interface ComposerContextValue {
state: ComposerState
actions: ComposerActions
meta: ComposerMeta
}
const ComposerContext = createContext<ComposerContextValue | null>(null)
UI components consume the interface, not the implementation:
function ComposerInput() {
const {
state,
actions: { update },
meta,
} = use(ComposerContext)
// This component works with ANY provider that implements the interface
return (
<TextInput
ref={meta.inputRef}
value={state.input}
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
/>
)
}
Different providers implement the same interface:
// Provider A: Local state for ephemeral forms
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(initialState)
const inputRef = useRef(null)
const submit = useForwardMessage()
return (
<ComposerContext
value={{
state,
actions: { update: setState, submit },
meta: { inputRef },
}}
>
{children}
</ComposerContext>
)
}
// Provider B: Global synced state for channels
function ChannelProvider({ channelId, children }: Props) {
const { state, update, submit } = useGlobalChannel(channelId)
const inputRef = useRef(null)
return (
<ComposerContext
value={{
state,
actions: { update, submit },
meta: { inputRef },
}}
>
{children}
</ComposerContext>
)
}
The same composed UI works with both:
// Works with ForwardMessageProvider (local state)
<ForwardMessageProvider>
<Composer.Frame>
<Composer.Input />
<Composer.Submit />
</Composer.Frame>
</ForwardMessageProvider>
// Works with ChannelProvider (global synced state)
<ChannelProvider channelId="abc">
<Composer.Frame>
<Composer.Input />
<Composer.Submit />
</Composer.Frame>
</ChannelProvider>
Custom UI outside the component can access state and actions:
The provider boundary is what matters—not the visual nesting. Components that
need shared state don't have to be inside the Composer.Frame. They just need
to be within the provider.
function ForwardMessageDialog() {
return (
<ForwardMessageProvider>
<Dialog>
{/* The composer UI */}
<Composer.Frame>
<Composer.Input placeholder="Add a message, if you'd like." />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
</Composer.Footer>
</Composer.Frame>
{/* Custom UI OUTSIDE the composer, but INSIDE the provider */}
<MessagePreview />
{/* Actions at the bottom of the dialog */}
<DialogActions>
<CancelButton />
<ForwardButton />
</DialogActions>
</Dialog>
</ForwardMessageProvider>
)
}
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
function ForwardButton() {
const {
actions: { submit },
} = use(ComposerContext)
return <Button onPress={submit}>Forward</Button>
}
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
function MessagePreview() {
const { state } = use(ComposerContext)
return <Preview message={state.input} attachments={state.attachments} />
}
The ForwardButton and MessagePreview are not visually inside the composer
box, but they can still access its state and actions. This is the power of
lifting state into providers.
The UI is reusable bits you compose together. The state is dependency-injected by the provider. Swap the provider, keep the UI.