Commit Graph

3 Commits

Author SHA1 Message Date
Jakob Wennberg c402421908 feat(billing): make the expired-trial state visible with a clear upgrade path (#1725)
getCompanyEntitlements now derives an entitlementState (trial /
trial_expired / lapsed_subscription / paid / none) plus trialExpiredAt
from the grants it already fetches, reading company_subscriptions.status
inside the existing Promise.all so churned payers get 'abonnemang' copy
instead of 'provperiod'. The state threads through CompanyContext and the
dashboard layout.

Two new surfaces, both hidden in sandbox:
- SubscriptionTouchpoint replaces the sidebar trial pill: countdown while
  the trial runs, a persistent muted upgrade link to /settings/billing
  once it lapses (visible even collapsed, icon-only with aria-label), and
  the first mobile bottom-sheet touchpoint.
- TrialExpiredDialog: one-time on-entry notice with 'Se abonnemang' and a
  ghost dismiss; acknowledgement persists per user+company in
  user_preferences.ui_state.trial_expired_ack (read server-side, no
  flash), set on dismiss and click-through alike.

Narrows the 2026-07-11 'no trial-expired nag' decision at the founder's
direction after a user could not find the upgrade path at all; see
DECISIONS.md.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 10:05:39 +02:00
Mattsson b4b7549004 feat(agent): resizable, undockable assistant panel (#1467)
* feat(agent): resizable, undockable assistant panel

User report: the assistant chat sheet sometimes covers the page content
the user is asking about, with no way to resize or move it.

- Docked mode is now drag-resizable from its left edge (380-800px,
  clamped so the page keeps a 480px readable column) and the page
  reflows beside it via the existing --agent-dock-w reservation.
- Expanded (focus) mode reserves page margin like the compact dock
  instead of overlaying up to 1100px of the page.
- New undock toggle turns the sheet into a floating window that can be
  dragged by its header and resized from edges/corners, clamped so the
  header always stays reachable. Desktop only; mobile keeps the
  full-screen sheet.
- Geometry (mode, dock width, float rect) persists per user in
  user_preferences.ui_state.agent_panel, server-seeded to avoid a
  first-paint jump; the ui-state API schema gains a strict agent_panel
  key with nested merge.
- Pure clamp/resize math lives in lib/agent-panel/geometry with unit
  tests; drag frames write styles imperatively and commit one
  preference update on release.

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

* fix(agent): address review findings on panel drag, a11y, and persistence

CodeRabbit round 1, all six findings fixed:

- Bind drag listeners to window (plus lostpointercapture) so a failed
  pointer capture or mid-drag unmount can never leave the transition
  suppression and data-agent-resizing stuck for the session.
- Keyboard resize now steps from the visible width (expandedW in focus
  mode) instead of jumping to the persisted dock width.
- The width handle exposes window-splitter semantics: aria-valuenow,
  aria-valuemin, aria-valuemax.
- --nav-w is read reactively via a MutationObserver on #dash-shell
  instead of computed-style reads in the render body and per drag frame.
- The ui-state POST in updatePanelPrefs gets a 300ms trailing debounce
  (state stays immediate) so key auto-repeat cannot produce one
  read-merge-write per repeat; pending write flushes on unmount.
- globals.css keeps one :root token block; the agent-resizing rule moved
  below it.

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

* fix(agent): filter drag events by pointer id, clear fired debounce timer

CodeRabbit round 2, both findings fixed:

- Window-level drag listeners now ignore events from pointers other than
  the initiating one, so a second touch or pen cannot move the panel or
  end the first pointer's drag.
- The persist debounce timer ref is nulled when the timer fires, so the
  unmount flush only writes genuinely pending values instead of
  replaying an already-persisted (possibly stale) geometry.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 16:13:02 +02:00
Jakob Wennberg d59e4708cf feat(nav): concept sidebar with folds, collapse rail, and user menu (UI migration PR 2) (#1133)
* feat(nav): concept sidebar with folds, collapse rail, and user menu (UI migration PR 2)

The concept's navigation, exactly, with all current functionality kept:

- Groups restructured per concept: top (Hem, Assistenten), ARBETA
  (Bokforing, Underlag, Transaktioner, Granskning, Kundfakturor,
  Leverantorsfakturor, Loner), ANALYS, DATA (Register fold +
  Importera/exportera), SKATT & BOKSLUT (Moms, Skattekonto, Viktiga
  datum, Bokslut fold). Entity/capability/dimension gating unchanged.
- Register and Bokslut are animated folds (grid-rows 0fr/1fr), children
  text-indented behind a hairline; closed by default, forced open by an
  active child route; state persists per user.
- Sidebar collapses to a 64px icon rail (toggle top of rail); width is
  one inline --nav-w CSS variable on #dash-shell that aside and <main>
  both read, so the panel follows in lockstep. Server-rendered from
  ui_state so first paint is right.
- Sticky bottom user block (avatar, name, active company) opening an
  upward user menu: identity, company-switcher flyout (search + building
  glyphs + roles + check, real switch mechanism via shared
  lib/company/switch-client), Installningar, Medlemmar och roller,
  Abonnemang, Hjalp, support, terracotta logout. Trial touchpoint kept.
- CompanySwitcher removed from desktop top (lives in the user menu now);
  mobile bottom nav + sheet unchanged.
- New migration 20260723120000: user_preferences.ui_state jsonb bag
  (founder-approved) + POST /api/user/ui-state (requireAuth, strict zod,
  merge semantics) with route tests.
- i18n: fold/collapse/menu keys added sv+en; deadlines -> "Viktiga
  datum", year_end -> "Arsbokslut" per concept.

Discord-community row deferred: no invite URL exists in the repo.
Badges stay the current two (Transaktioner, Granskning); an Underlag
count is a follow-up with lib/worklist.

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

* fix(nav): brand-mark sidebar header + auto-hiding scrollbars

Concept alignment feedback: the sidebar gets a header row (brand mark
left, collapse toggle right) hanging from the same top line as the
panel, instead of a lone right-aligned toggle.

Scrollbars go overlay-style app-wide: transparent at rest, revealed only
while their container scrolls (ScrollbarReveal stamps .is-scrolling via
one capture-phase document listener), fading out after 700ms idle. The
gutter stays reserved so revealing never shifts layout.

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

* fix(nav): company flyout opens downward + Discord community row

The flyout was bottom-anchored to its row and grew upward over the menu;
founder feedback: top-align with the row and grow downward. Adds the
Discord community row to the user menu (external invite link).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:28:07 +02:00