Files
accounted/.agents/skills/vercel-react-native-skills/rules/list-performance-function-references.md
T
Jakob WennbergandClaude Opus 4.7 f8f49f8426 Inbox page-count gate + DataList/DropdownMenu primitives (#554)
* 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>
2026-05-22 10:12:21 +02:00

4.0 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Optimize List Performance with Stable Object References CRITICAL virtualization relies on reference stability lists, performance, flatlist, virtualization

Optimize List Performance with Stable Object References

Don't map or filter data before passing to virtualized lists. Virtualization relies on object reference stability to know what changed—new references cause full re-renders of all visible items. Attempt to prevent frequent renders at the list-parent level.

Where needed, use context selectors within list items.

Incorrect (creates new object references on every keystroke):

function DomainSearch() {
  const { keyword, setKeyword } = useKeywordZustandState()
  const { data: tlds } = useTlds()

  // Bad: creates new objects on every render, reparenting the entire list on every keystroke
  const domains = tlds.map((tld) => ({
    domain: `${keyword}.${tld.name}`,
    tld: tld.name,
    price: tld.price,
  }))

  return (
    <>
      <TextInput value={keyword} onChangeText={setKeyword} />
      <LegendList
        data={domains}
        renderItem={({ item }) => <DomainItem item={item} keyword={keyword} />}
      />
    </>
  )
}

Correct (stable references, transform inside items):

const renderItem = ({ item }) => <DomainItem tld={item} />

function DomainSearch() {
  const { data: tlds } = useTlds()

  return (
    <LegendList
      // good: as long as the data is stable, LegendList will not re-render the entire list
      data={tlds}
      renderItem={renderItem}
    />
  )
}

function DomainItem({ tld }: { tld: Tld }) {
  // good: transform within items, and don't pass the dynamic data as a prop
  // good: use a selector function from zustand to receive a stable string back
  const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name)
  return <Text>{domain}</Text>
}

Updating parent array reference:

Creating a new array instance can be okay, as long as its inner object references are stable. For instance, if you sort a list of objects:

// good: creates a new array instance without mutating the inner objects
// good: parent array reference is unaffected by typing and updating "keyword"
const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name))

return <LegendList data={sortedTlds} renderItem={renderItem} />

Even though this creates a new array instance sortedTlds, the inner object references are stable.

With zustand for dynamic data (avoids parent re-renders):

const useSearchStore = create<{ keyword: string }>(() => ({ keyword: '' }))

function DomainSearch() {
  const { data: tlds } = useTlds()

  return (
    <>
      <SearchInput />
      <LegendList
        data={tlds}
        // if you aren't using React Compiler, wrap renderItem with useCallback
        renderItem={({ item }) => <DomainItem tld={item} />}
      />
    </>
  )
}

function DomainItem({ tld }: { tld: Tld }) {
  // Select only what you need—component only re-renders when keyword changes
  const keyword = useSearchStore((s) => s.keyword)
  const domain = `${keyword}.${tld.name}`
  return <Text>{domain}</Text>
}

Virtualization can now skip items that haven't changed when typing. Only visible items (~20) re-render on keystroke, rather than the parent.

Deriving state within list items based on parent data (avoids parent re-renders):

For components where the data is conditional based on the parent state, this pattern is even more important. For example, if you are checking if an item is favorited, toggling favorites only re-renders one component if the item itself is in charge of accessing the state rather than the parent:

function DomainItemFavoriteButton({ tld }: { tld: Tld }) {
  const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id))
  return <TldFavoriteButton isFavorited={isFavorited} />
}

Note: if you're using the React Compiler, you can read React Context values directly within list items. Although this is slightly slower than using a Zustand selector in most cases, the effect may be negligible.