feat(settings): Fönster redesign - flat rows, ? help, dirty save bar (#1193)

* feat(settings): Fönster redesign - flat rows, help behind ?, dirty save bar

Founder-approved concept (2026-07-25) applied to the whole settings
surface, modal and full-page variants alike:

- New primitives in components/settings/SettingsRows.tsx: section header
  (serif title + one-line intro), eyebrow groups, hairline label/control
  rows, flat inputs/selects/textareas, segmented control, animated
  reveal for gated settings, danger zone.
- Every static explanation paragraph moved behind a "?" popover
  (HelpPopover) at row or group level; dynamic status stays visible.
- Modal chrome: company kicker over serif title, fixed 920x680 window.
- SettingsFormWrapper: save is a sticky bar that appears only when the
  form is dirty; collapses to zero height when clean.
- All 11 sections converted (Konto, Abonnemang, Företag, Bokföring,
  Skatt, Löner, Fakturering, Mallar, Bank incl. Enable Banking-panel,
  Assistenten, API) with handlers, validation, role/entitlement/sandbox
  gates and i18n keys preserved; checkboxes became switches, cards
  dissolved into groups.
- Fix: Escape with an open help popover closed the whole settings
  modal; it now closes the popover first.
- New i18n keys: settings_intro.*, group labels, wrapper_unsaved
  (sv+en).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(settings): founder feedback round 1 on the Fönster redesign

- Abonnemang paying state: status and manage split into two rows so the
  row no longer wraps awkwardly; the included-features list now shows
  for paying companies too.
- Logos where the counterpart has one: BankID mark on the security row
  and on the Koppla BankID button, Skatteverket mark on the connection
  rows.
- Buttons are unmistakably buttons: 27 text-labeled row actions went
  from ghost to outline pills; icon-only actions stay quiet.
- The agent-knowledge view (Regler & profil: Dina regler, Momsprofil,
  Konventioner) converted to the flat row language; it was the last
  old-style surface inside settings. Descriptions moved behind "?",
  rules render as hairline rows, the per-row "Regel" chip demoted to
  muted text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(settings): address review-bot findings on the Fönster redesign

- SettingsFormWrapper marks the form dirty on switch clicks too: Radix
  Switch is a button and fires no input event, so switch-only changes
  (f-skatt, KU, ROT/RUT, OSS...) never revealed the save bar.
- i18n: the migrated hardcoded strings got keys in both locales
  (fiscal-period start date/range/months, security set-password trio);
  dates in ApiKeysPanel/OAuthClientsPanel/CalendarFeedSettings now pass
  the active locale to formatDateLong.
- A11y: member remove/revoke buttons and the invite role select got
  correct accessible names; BankNameCombobox accepts aria-label wired
  from its row; the pinned-fact icon exposes role img.
- BankIdSettings: explicit Avbryt under the QR block so a cancelled
  BankID flow cannot strand isLinking.
- VoucherSeriesManager: clear the skeleton when no company is resolved.

Verified end to end in sandbox: switch-only dirty bar, PUT /api/settings
200 for text and switch saves, persistence across hard reload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-25 23:55:08 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent d54b43f80f
commit 6d9846b1e7
30 changed files with 400 additions and 367 deletions
+48 -27
View File
@@ -20,8 +20,6 @@
"load_more": "Load more",
"retry": "Try again",
"load_error": "Could not load data",
"popup_blocked_title": "The browser blocked the tab",
"popup_blocked_description": "Allow pop-ups for Accounted in your browser and try again.",
"confirm": "Confirm",
"yes": "Yes",
"no": "No",
@@ -66,7 +64,9 @@
"matched": "Matched",
"unmatched": "Unmatched"
},
"more_options": "More options"
"more_options": "More options",
"popup_blocked_title": "The browser blocked the tab",
"popup_blocked_description": "Allow pop-ups for Accounted in your browser and try again."
},
"nav": {
"dashboard": "Overview",
@@ -369,9 +369,16 @@
"sync_now": "Sync now",
"syncing": "Syncing…",
"sync_done_title": "Sync complete",
"sync_done_feed": "{fetched} transaction(s) fetched: {imported} new in the inbox, {linked} linked to vouchers.",
"sync_done_empty": "Stripe returned no transactions for the period. If you expected transactions, check that the right account is connected.",
"sync_done_description": "{settled} payment(s) booked, {review} need review.",
"sync_failed_title": "Sync failed",
"needs_review_title": "Needs review",
"needs_review_hint": "Payments that could not be matched automatically. Handle them manually via the invoice or in Stripe.",
"reason_invoice_not_found": "Payment without a matching invoice",
"reason_invoice_already_paid": "The invoice is already marked as paid",
"reason_amount_mismatch": "The amount does not match the invoice remaining balance",
"reason_currency_mismatch": "The currency does not match the invoice",
"reason_non_sek_invoice": "Foreign-currency invoice (book manually)",
"reason_unknown": "Unknown reason",
"transaction_sync_title": "Transactions from Stripe",
"transaction_sync_description": "Import all Stripe transactions (payments, fees, refunds and payouts) into the transactions inbox every night, like a bank feed for your Stripe balance. You book the rows from the inbox as usual.",
"transaction_sync_backfill_note": "The first sync fetches up to 90 days of history, but never before the bookkeeping lock date.",
@@ -379,7 +386,10 @@
"transaction_sync_never_synced": "Not synced yet",
"transaction_sync_enabled_toast": "Transaction sync enabled. History is fetched on the next sync.",
"transaction_sync_disabled_toast": "Transaction sync disabled.",
"transaction_sync_toggle_failed": "Could not save the setting. Please try again."
"transaction_sync_toggle_failed": "Could not save the setting. Please try again.",
"sync_done_transactions": "{imported} transaction(s) imported, {linked} linked to vouchers.",
"sync_done_feed": "{fetched} transaction(s) fetched: {imported} new in the inbox, {linked} linked to vouchers.",
"sync_done_empty": "Stripe returned no transactions for the period. If you expected transactions, check that the right account is connected."
},
"settings_modal": {
"title": "Settings",
@@ -1374,6 +1384,13 @@
"joined_generic": "You are now a member."
},
"settings_company": {
"members_remove_aria": "Remove member",
"members_revoke_aria": "Revoke invitation",
"members_role_label": "Role",
"fp_start_date_label": "Start date",
"fp_start_date_help": "The first fiscal year can start on any day.",
"fp_range": "{start} to {end}",
"fp_months": "{count} months",
"company_info_heading": "Company details",
"share_capital_heading": "Share capital",
"aktiekapital_label": "Share capital (SEK)",
@@ -2239,6 +2256,7 @@
"env_prod": "Production"
},
"settings_bankid": {
"cancel_linking": "Cancel",
"title": "BankID",
"toast_already_linked": "This BankID is already linked to another account.",
"toast_link_failed": "Could not link BankID.",
@@ -2256,6 +2274,9 @@
"link_button": "Link BankID"
},
"settings_security": {
"set_password_title": "Set a password",
"set_password_description": "You signed in with BankID and have no password yet. Set one to enable 2FA or to sign in when BankID is unavailable.",
"set_password_button": "Set password",
"group_security": "Security",
"toast_weak_password_title": "Password is too weak",
"toast_weak_password_description": "The password must be at least 8 characters and include uppercase, lowercase, digits and a special character.",
@@ -2893,8 +2914,6 @@
"preview_pdf": "Preview PDF",
"preview_pdf_generating": "Generating...",
"preview_pdf_failed": "Could not generate PDF",
"review_customer_missing_title": "Customer details could not be loaded",
"review_customer_missing_description": "Reload the page and try again. Contact support if the problem persists.",
"create_invoice_failed_title": "Could not create invoice",
"doc_created_title": "{docLabel} created",
"doc_created_description": "{docLabel} {number} has been created",
@@ -2936,7 +2955,9 @@
"deduction_cap_check": "The customer needs to check their remaining allowance themselves.",
"deduction_summary_label": "Tax reduction ROT/RUT",
"to_pay_label": "Amount to pay",
"total_incl_vat_label": "Total incl. VAT"
"total_incl_vat_label": "Total incl. VAT",
"review_customer_missing_title": "Customer details could not be loaded",
"review_customer_missing_description": "Reload the page and try again. Contact support if the problem persists."
},
"invoice_review": {
"assigned_number_prefix": "Will be assigned invoice number",
@@ -3028,21 +3049,6 @@
"delivery_status_sent": "Sent",
"delivery_status_failed": "Failed",
"delivery_status_marked_sent": "Manual",
"delivery_status_delivered": "Delivered",
"delivery_status_delayed": "Delayed",
"delivery_status_complained": "Marked as spam",
"delivery_status_bounced": "Bounced",
"delivery_status_suppressed": "Blocked",
"delivery_status_explanation_sent": "The email provider accepted the message. No word yet on whether the recipient's server took it.",
"delivery_status_explanation_delivered": "The recipient's server accepted the message. If the customer still cannot find it, ask them to check their spam folder. Quarantine on the recipient's side is never visible to the sender.",
"delivery_status_explanation_delayed": "The recipient's server has not accepted the message yet, but delivery is still being retried. If nothing changes within a few hours, contact the customer.",
"delivery_status_explanation_complained": "The recipient marked the message as spam. Further sends to this address may be blocked.",
"delivery_status_explanation_bounced": "The recipient's server rejected the message. The invoice did not arrive.",
"delivery_status_explanation_failed": "The message could not be sent. The invoice did not arrive.",
"delivery_status_explanation_suppressed": "The email provider has blocked this address after an earlier bounce or spam complaint, so the message was never sent.",
"delivery_status_whole_send_note": "This applies to the whole send, not to individual recipients.",
"delivery_provider_status_label": "Delivery status",
"delivery_provider_reason_label": "Reason from the recipient",
"delivery_manual_unknown_details": "The invoice was delivered outside Accounted, so its recipients, message, and delivered file are unknown.",
"delivery_to_label": "To",
"delivery_cc_label": "Cc",
@@ -3189,7 +3195,22 @@
"cancelled_draft": "The draft has been cancelled.",
"cancel_failed_title": "Could not cancel the invoice",
"paid_toast_title": "Paid",
"paid_toast_description": "Invoice {number} has been marked as paid and posted"
"paid_toast_description": "Invoice {number} has been marked as paid and posted",
"delivery_status_delivered": "Delivered",
"delivery_status_delayed": "Delayed",
"delivery_status_complained": "Marked as spam",
"delivery_status_bounced": "Bounced",
"delivery_status_suppressed": "Blocked",
"delivery_status_explanation_sent": "The email provider accepted the message. No word yet on whether the recipient's server took it.",
"delivery_status_explanation_delivered": "The recipient's server accepted the message. If the customer still cannot find it, ask them to check their spam folder. Quarantine on the recipient's side is never visible to the sender.",
"delivery_status_explanation_delayed": "The recipient's server has not accepted the message yet, but delivery is still being retried. If nothing changes within a few hours, contact the customer.",
"delivery_status_explanation_complained": "The recipient marked the message as spam. Further sends to this address may be blocked.",
"delivery_status_explanation_bounced": "The recipient's server rejected the message. The invoice did not arrive.",
"delivery_status_explanation_failed": "The message could not be sent. The invoice did not arrive.",
"delivery_status_explanation_suppressed": "The email provider has blocked this address after an earlier bounce or spam complaint, so the message was never sent.",
"delivery_status_whole_send_note": "This applies to the whole send, not to individual recipients.",
"delivery_provider_status_label": "Delivery status",
"delivery_provider_reason_label": "Reason from the recipient"
},
"invoice_credit": {
"back": "Back",
@@ -3954,7 +3975,6 @@
"remove_blocked_cancel_cta": "Close",
"replace_uploading": "Replacing...",
"remove_failed": "Could not remove the document.",
"download_failed": "Could not open the document",
"replace_failed": "Could not upload new version.",
"choose_from_inbox": "Choose from inbox",
"picker_title": "Choose a document from the inbox",
@@ -3970,7 +3990,8 @@
"picker_preview": "Preview",
"picker_attach": "Attach document",
"picker_close": "Close",
"picker_preview_unavailable": "Preview could not be displayed."
"picker_preview_unavailable": "Preview could not be displayed.",
"download_failed": "Could not open the document"
},
"journal_status": {
"status_draft": "Draft",