# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Released versions
are CalVer — `YYYY.M.D` of the publish date, with leading zeros dropped (npm's semver parser rejects
them), matching the `entropy-auth` crate. The `0.1.0` entry below predates the switch and was never
published to npm.

## 2026.911.1

### Added
- `DataTable` takes `resizable`, which lets the operator drag the boundary
  between two columns. Widths seed from the measured container by each column's
  `flex`, so a resizable table still fills its space without pixel widths at
  the call site, and a drag moves width between the pair either side of the
  handle rather than growing the table — the total is unchanged, so columns to
  the right do not crawl sideways. A manual drag survives a window resize.
  `distributeColumnWidths` and `resizeColumnPair` are exported and pure, with
  seven tests covering flex weighting, fixed columns, the minimum width, and
  the total-preserving property.

## 2026.910.3

### Added
- `SocialConnections` takes a `custom` list of providers this package ships no
  brand mark for — Okta, Keycloak, a second Access instance — rendered after
  the built-ins so learned button positions do not move. Each carries the
  operator's own `iconUrl`; a blank, malformed or unreachable URL falls back to
  a neutral key glyph rather than an empty square, so the button is always
  pressable and always labelled. `customIcon` is exported and pure so the
  fallback rule is testable without a render runtime.

## 2026.8.42

### Fixed

- Custom uploaded icons never rendered on a phone. `useUploadBlobUrl` built its URI with
  `URL.createObjectURL`, which is a browser API — React Native's `URL` does not implement it in any
  way that can be relied on, so the call either threw or produced something no image loader would
  open, and every upload fell through to the fallback glyph on every native consumer. Web still gets
  a blob URL (no copy, and revocable); native now reads the bytes and builds
  `data:<type>;base64,...`, which every RN image loader accepts. Base64 inflates by a third and the
  string lives as long as the row is mounted, which is affordable for icons — vault caps an upload
  at 256KB — and is why the two platforms differ rather than both taking the data URI.
- The base64 encoder is hand-rolled rather than `btoa`. React Native ships no `btoa`, so whether one
  exists depends on which polyfill the consuming app installed, which is not something a library can
  assume.

### Added

- `useUploadBlobUrl` takes an options object — `{ uploadsPath, resolve }` — in place of the bare
  path, which still works. `resolve` produces the URI for an upload id without going through
  `apiFetch` at all, for a consumer whose uploads are not behind a server: vault's device-only
  vaults keep theirs in on-device SQLite. Returning `null` reports `errored`, so an id that does not
  resolve draws the same fallback a failed fetch does. The resolver is held in a ref and only
  `uploadId` / `uploadsPath` re-run the effect, so an arrow defined inline at the call site cannot
  turn every render into a refetch.
- `ItemIcon` takes `resolveUpload`, passed straight through. Without it such a consumer had to
  pre-resolve the reference into a `data:` URI and hand it over as `iconUrl` — which works only
  because anything that is not `custom:` is treated as a plain URL, and hides the intent completely.

## 2026.8.41

### Fixed

- `AuthScreen`'s form-height lock skipped its measuring pass for any consumer that resolves
  `signUpEnabled` asynchronously. `probed` was initialised from `signUpEnabled`, which is right when
  the consumer knows its registration policy at mount — but one that has to ASK for it (Access's
  portal fetches the tenant's policy and starts at the conservative "closed") mounted with sign-up
  off, skipped the pre-measure, and turned sign-up on a moment later with nothing measured. The
  first real toggle was then the one that grew the card: it resized once, then held still forever,
  which is exactly the symptom the lock exists to remove. The probe now re-arms on the false -> true
  edge.

## 2026.8.40

### Added

- `AuthScreen` takes `lockFormHeight` (default `true`). The card reserves the height of the taller
  of Sign In / Sign Up so the two toggle without the layout moving; the cost is slack, left as empty
  space above the shorter mode's submit button. Where Sign Up carries several more fields that gap
  is the more visible flaw of the two — most people only ever sign in, and they all see it — so a
  consumer can now let the card resize instead. The body is already wrapped in a layout animation,
  so the change eases rather than jumps.

## 2026.8.39

### Added

- `SocialConnections` takes `showEntropy` (default `true`), which drops the first-party "Continue
  with Entropy" button from the row. Unlike the other three, that one is not a staged-rollout
  placeholder waiting for its connector: on Entropy Access's own sign-in surfaces it offers to sign
  in to the identity provider with the identity provider, which is a button that can never mean
  anything. A flag rather than inferring intent from a missing `onEntropy`, because rendering an
  unhandled provider is deliberate here — simple-finance renders the whole row with no handlers at
  all, and inference would silently take that button away from it.

## 2026.8.35

### Fixed

- `AuthService.getCurrentUser()` destroyed the session it was asked about. It returned
  `loadPersistedUser()` and nothing else, and on web that reads a module-level variable which is
  null in every freshly loaded document — so it answered "nobody" on each page load and refresh
  while the session cookie was perfectly valid. `useAuth.checkAuth` treats that as a session it
  cannot resolve and calls `logout()`, which POSTs `/auth/logout` and ends the session server-side:
  the user was signed out by the act of loading the page.

  Observed on Access, whose portal and admin console are separate SPAs on one origin — so every
  navigation between them is a fresh document and therefore a guaranteed logout, presenting as an
  infinite sign-in loop. Its audit log recorded the logouts nobody had asked for.

  Native was affected too, if less visibly: `isAuthenticated()` there is satisfied by a token in
  SecureStore, so a token that outlived its cached user record produced the same spurious logout.

  `getCurrentUser()` now falls back to `GET /auth/me` when nothing is persisted. It deliberately
  does not route through `isAuthenticated()`, whose web branch falls back to calling
  `getCurrentUser()` — going the other way would recurse until the stack gave out, on exactly the
  signed-out path this makes cheap. The request is shared with `refreshUserData()` through a private
  `fetchMe`, parameterised on whether a 401 should log out: a refresh of a session believed live
  that 401s means the session died, while a lookup that 401s is just an anonymous visitor and must
  not tear anything down.

## 2026.8.17

### Fixed

- The systemic bare-CSS-var-no-`dark:`-fallback pattern flagged as a known follow-up in 2026.8.15 —
  completed across the rest of the package: `Card`, `ContextMenu`, `DataTable`, `DropdownMenu`,
  `Popover`, `SettingsRow`, `PinPad`, `PasswordGenerator`, `EntropyByline`, `ErrorBoundary` (severe:
  its retry button's label was invisible white-on-white in dark mode), `Pagination`, `LockOverlay`,
  `ProtectedRoute`, and every screen under `src/screens/auth/*` (root backgrounds, plus scattered
  destructive-text and `ServerConfigScreen`'s from-scratch raw-`Text` markup).
- `CardFooter`'s base `items-center` was never overridden by the `flex-col` className every
  consuming screen passes it (`AuthScreen`, `LoginScreen`, `RegisterScreen`, and ~10 others) —
  `flex-row`→`flex-col` and `border-t`→ `border-t-0` correctly override via tailwind-merge's
  same-property matching, but `items-center` has no `items-*` counterpart in that className to
  conflict with, so it silently survived. Left the submit button's wrapping view with ambiguous
  (content-fit) width instead of stretching to match its own `w-full` Pressable child — surfaced as
  the "Sign In" button rendering with a runaway width specifically once its content swapped from
  label text to the loading spinner (the one thing that actually changes Yoga's layout resolution
  for that ambiguous case). Added `items-stretch` alongside `flex-col` at every call site.

## 2026.8.15

### Fixed

- Several components colored themselves with a bare CSS-variable utility (`bg-primary`,
  `bg-background`, `border-border`, `bg-destructive`, …) and no `dark:` fallback — on native Android
  that value stays stuck on its light setting regardless of actual system theme; only components
  carrying an explicit `dark:`-prefixed class (the existing pattern in `Heading`/`Text`/
  `MutedText`) reacted correctly. Added matching `dark:` variants — one per CSS variable's own
  `.dark` value in `styles/globals.css` — to `Button`, `Input`, `Dialog`'s destructive variant,
  `legal-dialog`'s body text, and `AuthScreen`'s root background. Surfaced as vault's "Get Started"
  screen going render-broken in dark mode: invisible button, invisible heading/legal text, and a
  background that stayed white.
- `AuthScreen`'s tagline and "← Back" link used the plain `Text` component with a
  `text-muted-foreground` override that fought `Text`'s own base `dark:text-white` — the override
  lost to the base class's `dark:` variant, so both rendered bright white instead of muted in dark
  mode. Switched both to `MutedText`, which carries the correct `dark:text-neutral-400`.
- Known follow-up, not fixed here: the same bare-CSS-var-no-dark:-fallback pattern is present in
  roughly 15 more files in this package (`password-generator.tsx`, `pin-pad.tsx`, `tag-chip.tsx`,
  several other still-active auth screens, …) — scoped out of this release to unblock the reported
  bug; needs its own pass.

## 2026.8.2

### Fixed

- `registerTelemetry` and the rest of the telemetry registry were exported from the root barrel
  twice — once through `export * from "./services"` and once by name. Metro's CommonJS interop
  resolved the duplicate ambiguously and consumers got `undefined` at runtime despite the types
  checking. The root barrel now re-exports them only through `./services`.

## 2026.8.1

### Added

- Telemetry registry (`registerTelemetry`, `trackClick`, `withClickTracking`). The package reports
  interactions to a sink the host app supplies; it knows nothing about batching, authentication or
  transport. No sink registered means every entry point is a no-op, so consumers that do not opt in
  are unaffected.
- `telemetryId` prop on `Button` and `IconButton`. Reports a stable, author-assigned id when the
  control is activated. Wired through `onPress`, so a disabled or loading button records nothing.
- All 69 interactive controls across the 18 auth screens carry a `telemetryId`, making sign-in,
  sign-up, MFA and consent measurable by consuming apps without each of them re-tagging the packaged
  screens.

## [Unreleased]

## [2026.7.31] — 2026-07-31

First npm release. Contents unchanged from the `0.1.0` entry below, which was tagged but never
published.

### Added

- **Admin-dashboard primitives**: `DataTable` (generic typed table with column defs, custom cell
  renderers, fixed-`width`/`flex` sizing, alignment, row press, skeleton loading state, and empty
  state; virtualised over react-native `FlatList` with an always-visible header row — works on RN
  web and native), `Pagination` (prev/next controls, `X–Y of Z` summary, clamped page state,
  accessible disabled controls, and an optional `DropdownMenu`-based page-size selector), and
  `StatCard` (label + value + optional icon slot and trend/delta line, composed from `Card` +
  `Text`). Exposes the pure helpers `getPaginationState`, `resolveColumnStyle`, `dataTableBodyMode`,
  and `trendColorClass`.

## [0.1.0] - 2026-05-26

### Added

- Initial release.
- **UI primitives** (ported from vault mobile): `Button`, `IconButton`, `Card` family, `Checkbox`,
  `ContextMenu` family, `Dialog` / `DialogContent` / `DialogFooter` / `DialogButton` /
  `ConfirmDialog`, `DropdownMenu` family, `ErrorBoundary`, `Input` / `SearchInput` / `EmailInput` /
  `PasswordInput` / `TextArea`, `FormLabel` / `FormLabelText`, `Popover` family, `Separator` /
  `SeparatorWithText`, `Skeleton`, `Text` / `Heading` / `Label` / `MutedText` / `MonoText` /
  `ErrorText`, `Tooltip` family.
- **Promoted shared widgets**: `BrandHeader`, `TagChip`, `BottomNav`, `ItemIcon`, `TotpDisplay`,
  `UserAvatar`, `SocialConnections`, `ServerSettings`, `PasswordGenerator`, `TagMultiSelect`,
  `SettingsRow` / `SettingsSection`, `AccountSection`, `GeneralSection`, `SupportSection`,
  `DangerZoneSection`, `ProfileDialog` / `ProfileTab` / `SecurityTab`, `ToastPortal` (+ web
  variant), `AuthScreen` / `StaggeredAuthScreen` / `FadeTransition` / `ScaleTransition`.
- **Auth helpers**: `LockOverlay`, `PasswordStrengthMeter` (+ `calculatePasswordStrength`),
  `PwnedPasswordWarning`, `ProtectedRoute` / `withAuth` / `useProtectedContent`.
- **Auth screens** (sub-path `@entropy-softworks/ui/screens/auth`): `WelcomeScreen`, `LoginScreen`
  (with optional passkey flow), `RegisterScreen`, `ForgotPasswordScreen`, `ResetPasswordScreen`,
  `VerifyEmailScreen`, `MfaChallengeScreen`, `MfaEnrollScreen`, `ServerConfigScreen`,
  `TermsOfServiceScreen`, `PrivacyPolicyScreen`, `ImportDataScreen`. All AppConfig-driven (logo /
  appName / route map). Vault-specific bits (server-settings cog, password-demo animation,
  social-auth, authenticator-app promo, Bitwarden import flow) exposed as optional render slots /
  props.
- **Services**: `configureApi({ apiBaseUrl, storagePrefix, runtimeConfigKey })` runtime
  configuration (state isolated on an `ApiConfigStore` for testability); `AuthService` with
  login/register/logout/me/MFA/ email-verify/password-reset/forgot-password/delete-account methods;
  `fetchHibpPolicy` + `getHibpPolicy`; `checkPasswordPwned` HIBP k-anonymity lookup; favicon helpers
  (`buildFaviconUrl`, `customIconRef`, `customIconId`, `isCustomIconRef`, `resolveIcon`).
- **Transport**: `apiFetch` with cookie-based CSRF on web + Bearer tokens on native;
  `fetchWithTimeout` helper using `AbortSignal.any`.
- **Context**: `AppConfigProvider`, `ThemeProvider` with `light` / `dark` / `system` preference
  persistence, `NotificationProvider` exposing a `push()` toast helper, `NavigationProvider` +
  `useNavigation` for pluggable routing (defaults to expo-router; consumers without it inject their
  own stack-based adapter).
- **Hooks**: `useAuth` / `useRole` / `useRequireAuth` / `useSession` parameterised through
  `AppConfig` and optional `onAfterLogin` / `onAfterRegister` / `onBeforeLogout` /
  `onBeforeCheckAuth` / `loginWithPasskey` / `unlockWithPassword` callbacks; `useShakeAnimation`
  invalid-input feedback; `useAutoLock` generic idle-lock hook; `useUploadBlobUrl` auth-aware blob
  fetcher.
- **Forms**: `FormError` fixed-height error slot, `ShakeField` invalid-shake wrapper.
- **Utilities** (`@entropy-softworks/ui/utils`): `cn`, `getErrorMessage`, `extractErrorMessage`,
  `debounce`, `copyToClipboard` / `clearClipboardNow` (auto-clear timer), `loadSetting` /
  `saveSetting`, `generatePassword` (CSPRNG, rejection-sampled), `validateEmail` /
  `validatePassword` / `validateUsername`, `darkPalette` / `lightPalette` / `usePalette` /
  `getPalette`.

### Security

- Web user records persist in memory only (not `sessionStorage`). XSS attacks cannot read the cached
  email / display name; the HttpOnly session cookie remains the auth source of truth.
  `isAuthenticated()` on web probes the non-HttpOnly `csrf_token` cookie set by the server alongside
  the session cookie.
- `extractErrorMessage` caps detail arrays at 5 entries to bound the size of error toasts.
- `buildFaviconUrl` rejects inputs with non-http(s) explicit schemes before normalisation,
  preventing scheme-confusion attacks against the favicon proxy.
- Password / email / username validators in `utils/validation` are UI-only previews. The application
  server enforces the same rules authoritatively (see `entropy-auth`'s `password::policy` /
  `Validation`).

### Tests

- 82 unit tests across services, utilities, hooks, and contexts. Jest preset uses
  `babel-preset-expo` with module mocks for `react-native` / `expo-secure-store` / `expo-crypto` /
  `expo-clipboard` / `nativewind`.

[Unreleased]: https://gitlab.com/entropysoftworks/packages/entropy-ui/-/compare/v0.1.0...HEAD
[0.1.0]: https://gitlab.com/entropysoftworks/packages/entropy-ui/-/tags/v0.1.0
