uhc-portal-components-react 0.2.20 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.22 - 2026-10-22
6
+
7
+ - Place the verified login identifier beneath the application name, keep the
8
+ current subject immediately before the account control and render the
9
+ account trigger as an icon only across Personal and Professional hosts.
10
+
11
+ ## 0.2.21 - 2026-10-21
12
+
13
+ - Move the card preview, profile recovery, elapsed-operation status and
14
+ collapsible portal section into the shared package and remove their cloned
15
+ consumer implementations and duplicate elapsed-status tests.
16
+ - Keep package metadata and current usage guidance brand-neutral; consumer
17
+ names belong in host repositories rather than this reusable package.
18
+ - Centralize the platform-authenticator capability check and mandatory
19
+ passkey-before-wallet ordering so Personal and Professional hosts cannot
20
+ silently diverge on biometric enrollment policy.
21
+
5
22
  ## 0.2.20 - 2026-10-21
6
23
 
7
24
  - Add one accessible Personal subject workspace shared by UHC, PetChain and
package/README.md CHANGED
@@ -1,25 +1,19 @@
1
- # UHC Portal Components React
1
+ # Shared Portal Components React
2
2
 
3
3
  Historical reusable UI and flow anchors are tracked in
4
4
  [`docs/LEGACY_FRONTEND_RECOVERY_INVENTORY.md`](docs/LEGACY_FRONTEND_RECOVERY_INVENTORY.md).
5
5
 
6
- Publishable, brand-neutral React components shared by:
7
-
8
- - UHC Personal
9
- - UHC Professional
10
- - VetChain Personal
11
- - VetChain Professional
12
- - SOSChain Personal
13
- - SOSChain Professional
6
+ Publishable, brand-neutral React components shared by independent Personal and
7
+ Professional portal deployments.
14
8
 
15
9
  The package owns visual shell behavior only. Authentication, protected-profile
16
10
  storage, PIN verification and actor authorization remain in the host portal
17
11
  through `ProfileRuntime`, `LoadedProfileWorkspace` and actor-scoped SDK facades.
18
12
 
19
13
  `AnimalIdentityOnboardingV2` and `AnimalIdentityDetailsV2` define one shared
20
- progressive create/read pattern. VetChain uses it for an animal and responsible
21
- party. UHC Professional must reuse the same contract for a newborn and
22
- responsible person, changing only governed role policy and copy: an attending
14
+ progressive create/read pattern. A host can use the same contract for an animal
15
+ and responsible party or a newborn and responsible person, changing only
16
+ governed role policy and copy: an attending
23
17
  physician, midwife, nurse or paramedic may prepare the identity and default
24
18
  permission proposal where the jurisdiction authorizes that occupation. The
25
19
  proposal remains pending until the responsible authority confirms it; showing
@@ -40,17 +34,17 @@ Brand differences are supplied through `PortalBrand`: product name,
40
34
  organization, official artwork and CSS variables. Artwork declares its
41
35
  light/dark-surface files and intrinsic dimensions; the shell preserves those
42
36
  proportions and never synthesizes a text monogram. Components do not import
43
- Firebase, Next.js or any GDC/UHC SDK runtime.
37
+ Firebase, Next.js or any product SDK runtime.
44
38
 
45
39
  ```tsx
46
40
  const brand = {
47
41
  family: 'professional',
48
- productName: 'UNID Professional',
49
- organizationName: 'Fundación UNID',
42
+ productName: 'Example Professional',
43
+ organizationName: 'Example Organization',
50
44
  homeHref: '/es',
51
45
  mark: {
52
- lightSurface: { src: '/logos/uhc-logo.svg', alt: 'UHC', width: 100, height: 100 },
53
- darkSurface: { src: '/logos/uhc-logo.svg', alt: 'UHC', width: 100, height: 100 },
46
+ lightSurface: { src: '/logos/product.svg', alt: 'Product', width: 100, height: 100 },
47
+ darkSurface: { src: '/logos/product.svg', alt: 'Product', width: 100, height: 100 },
54
48
  },
55
49
  } satisfies PortalBrand
56
50
  ```
@@ -196,12 +190,12 @@ token immediately; otherwise it records a `Communication` request for the
196
190
  individual controller to review. The GW remains authoritative for the exact
197
191
  intersection of requested and consented sections.
198
192
 
199
- UHC Personal and PetChain Personal must ultimately compose the same reusable
200
- personal flow modules. UHC Professional and VetChain Professional must do the
201
- same for professional flows. This package does **not yet expose those complete
193
+ Personal deployments must compose the same reusable Personal flow modules;
194
+ Professional deployments must do the same for Professional flows. This package
195
+ does **not yet expose every complete
202
196
  route modules**: login, versioned terms, profile protection, individual/member
203
197
  management, licences, clinical data, care and permissions still live in the
204
- UHC host application and must be extracted behind host-owned adapters. A brand
198
+ host application and must be extracted behind host-owned adapters. A brand
205
199
  adapter may change tokens, logo, product vocabulary, animal-specific forms and
206
200
  route destinations; it must not fork shared flow or authorization behavior.
207
201
  Independent deployments are expected: reuse means versioned complete screens,
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ /** Keeps the canonical physical-card aspect ratio inside a responsive preview shell. */
3
+ export declare function CardPreview({ children }: Readonly<{
4
+ children: ReactNode;
5
+ }>): import("react").JSX.Element;
6
+ //# sourceMappingURL=CardPreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CardPreview.d.ts","sourceRoot":"","sources":["../src/CardPreview.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC,+BAI1E"}
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /** Keeps the canonical physical-card aspect ratio inside a responsive preview shell. */
3
+ export function CardPreview({ children }) {
4
+ return _jsx("div", { className: "uhc-card-preview-shell", "data-testid": "card-preview", children: _jsx("div", { className: "uhc-card-preview", children: children }) });
5
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Keeps one portal phase mounted while allowing its content to be collapsed. */
3
+ export declare function CollapsiblePortalSection({ title, description, children, className, initiallyOpen, }: Readonly<{
4
+ title: string;
5
+ description?: string;
6
+ children: ReactNode;
7
+ className?: string;
8
+ initiallyOpen?: boolean;
9
+ }>): import("react").JSX.Element;
10
+ //# sourceMappingURL=CollapsiblePortalSection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CollapsiblePortalSection.d.ts","sourceRoot":"","sources":["../src/CollapsiblePortalSection.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAA;AAErE,iFAAiF;AACjF,wBAAgB,wBAAwB,CAAC,EACvC,KAAK,EACL,WAAW,EACX,QAAQ,EACR,SAAc,EACd,aAAoB,GACrB,EAAE,QAAQ,CAAC;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,CAAC,+BAaD"}
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ /** Keeps one portal phase mounted while allowing its content to be collapsed. */
5
+ export function CollapsiblePortalSection({ title, description, children, className = '', initiallyOpen = true, }) {
6
+ const [open, setOpen] = useState(initiallyOpen);
7
+ function retainDisclosureState(event) {
8
+ setOpen(event.currentTarget.open);
9
+ }
10
+ return _jsxs("details", { className: `research-import-disclosure ${className}`.trim(), open: open, onToggle: retainDisclosureState, children: [_jsxs("summary", { children: [_jsx("strong", { children: title }), description ? _jsx("span", { children: description }) : null] }), _jsx("div", { className: "research-import-disclosure-content", children: children })] });
11
+ }
@@ -0,0 +1,9 @@
1
+ /** Renders one accessible status and elapsed counter for a long portal operation. */
2
+ export declare function ElapsedOperationStatus({ busy, status, fallback, tone, testId }: Readonly<{
3
+ busy: boolean;
4
+ status: string;
5
+ fallback: string;
6
+ tone?: 'info' | 'warning' | 'success' | 'error';
7
+ testId?: string;
8
+ }>): import("react").JSX.Element | null;
9
+ //# sourceMappingURL=ElapsedOperationStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ElapsedOperationStatus.d.ts","sourceRoot":"","sources":["../src/ElapsedOperationStatus.tsx"],"names":[],"mappings":"AAIA,qFAAqF;AACrF,wBAAgB,sBAAsB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAgB,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC;IACpG,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAC,sCAiBD"}
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+ import { jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ /** Renders one accessible status and elapsed counter for a long portal operation. */
5
+ export function ElapsedOperationStatus({ busy, status, fallback, tone = 'warning', testId }) {
6
+ const [elapsedSeconds, setElapsedSeconds] = useState(0);
7
+ useEffect(() => {
8
+ if (!busy) {
9
+ setElapsedSeconds(0);
10
+ return;
11
+ }
12
+ const startedAt = Date.now();
13
+ setElapsedSeconds(0);
14
+ const timer = window.setInterval(() => setElapsedSeconds(Math.floor((Date.now() - startedAt) / 1000)), 1000);
15
+ return () => window.clearInterval(timer);
16
+ }, [busy]);
17
+ const text = status || (busy ? fallback : '');
18
+ if (!text)
19
+ return null;
20
+ return _jsxs("p", { role: tone === 'error' ? 'alert' : 'status', "data-testid": testId, className: `notice notice-${tone}`, children: [text, busy ? ` (${elapsedSeconds}s)` : ''] });
21
+ }
@@ -1,4 +1,5 @@
1
1
  import { type PortalBrand, type PortalAccountOption, type PortalLanguageOption, type PortalMenuAction, type PortalShellCopy } from './contracts.js';
2
+ import { type PortalCurrentSubjectRegistration } from './PortalCurrentSubject.js';
2
3
  export type PortalHeaderProps = Readonly<{
3
4
  brand: PortalBrand;
4
5
  copy: PortalShellCopy;
@@ -11,6 +12,7 @@ export type PortalHeaderProps = Readonly<{
11
12
  menuActions?: readonly PortalMenuAction[];
12
13
  onSignOut?: () => void | Promise<void>;
13
14
  storageNamespace?: string;
15
+ currentSubject?: PortalCurrentSubjectRegistration;
14
16
  }>;
15
17
  /**
16
18
  * Shared account/preferences header.
@@ -18,5 +20,5 @@ export type PortalHeaderProps = Readonly<{
18
20
  * Authentication is deliberately supplied as data and callbacks. The
19
21
  * component never imports Firebase and never treats browser state as a role.
20
22
  */
21
- export declare function PortalHeader({ brand, copy, accountIdentifier, languageHref, languageOptions, guestActionHref, accountOptions, onSelectAccount, menuActions, onSignOut, storageNamespace, }: PortalHeaderProps): import("react").JSX.Element;
23
+ export declare function PortalHeader({ brand, copy, accountIdentifier, languageHref, languageOptions, guestActionHref, accountOptions, onSelectAccount, menuActions, onSignOut, storageNamespace, currentSubject, }: PortalHeaderProps): import("react").JSX.Element;
22
24
  //# sourceMappingURL=PortalHeader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PortalHeader.d.ts","sourceRoot":"","sources":["../src/PortalHeader.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EAGrB,MAAM,gBAAgB,CAAA;AASvB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,KAAK,EAAE,WAAW,CAAA;IAClB,IAAI,EAAE,eAAe,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACjD,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAA;IAC/C,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,WAAW,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACzC,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAC,CAAA;AAEF;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,IAAI,EACJ,iBAAiB,EACjB,YAAY,EACZ,eAAoB,EACpB,eAAe,EACf,cAAmB,EACnB,eAAe,EACf,WAAgB,EAChB,SAAS,EACT,gBAA+B,GAChC,EAAE,iBAAiB,+BAoKnB"}
1
+ {"version":3,"file":"PortalHeader.d.ts","sourceRoot":"","sources":["../src/PortalHeader.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EAGrB,MAAM,gBAAgB,CAAA;AAOvB,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,2BAA2B,CAAA;AAElC,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,KAAK,EAAE,WAAW,CAAA;IAClB,IAAI,EAAE,eAAe,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACjD,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAA;IAC/C,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,WAAW,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACzC,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,cAAc,CAAC,EAAE,gCAAgC,CAAA;CAClD,CAAC,CAAA;AAEF;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,IAAI,EACJ,iBAAiB,EACjB,YAAY,EACZ,eAAoB,EACpB,eAAe,EACf,cAAmB,EACnB,eAAe,EACf,WAAgB,EAChB,SAAS,EACT,gBAA+B,EAC/B,cAAc,GACf,EAAE,iBAAiB,+BAyKnB"}
@@ -4,14 +4,14 @@ import { useEffect, useRef, useState } from 'react';
4
4
  import { portalUiDensities, portalUiThemes, } from './contracts.js';
5
5
  import { resolvePortalUiDensity, resolvePortalUiTheme, resolveRenderedPortalTheme, } from './preferences.js';
6
6
  import { PortalBrandArtwork } from './PortalBrandArtwork.js';
7
- import { PortalCurrentSubjectSlot } from './PortalCurrentSubject.js';
7
+ import { PortalCurrentSubjectControl, PortalCurrentSubjectSlot, } from './PortalCurrentSubject.js';
8
8
  /**
9
9
  * Shared account/preferences header.
10
10
  *
11
11
  * Authentication is deliberately supplied as data and callbacks. The
12
12
  * component never imports Firebase and never treats browser state as a role.
13
13
  */
14
- export function PortalHeader({ brand, copy, accountIdentifier, languageHref, languageOptions = [], guestActionHref, accountOptions = [], onSelectAccount, menuActions = [], onSignOut, storageNamespace = 'uhc-portal', }) {
14
+ export function PortalHeader({ brand, copy, accountIdentifier, languageHref, languageOptions = [], guestActionHref, accountOptions = [], onSelectAccount, menuActions = [], onSignOut, storageNamespace = 'uhc-portal', currentSubject, }) {
15
15
  const [open, setOpen] = useState(false);
16
16
  const [density, setDensity] = useState('medium');
17
17
  const [theme, setTheme] = useState('system');
@@ -61,7 +61,9 @@ export function PortalHeader({ brand, copy, accountIdentifier, languageHref, lan
61
61
  setTheme(value);
62
62
  localStorage.setItem(themeKey, value);
63
63
  }
64
- return (_jsxs("header", { className: "uhcp-header", style: brand.variables, children: [_jsxs("a", { className: "uhcp-brand", href: brand.homeHref, "aria-label": copy.home, children: [_jsx(PortalBrandArtwork, { artwork: brand.mark, className: "uhcp-brand-mark" }), _jsxs("span", { children: [_jsx("strong", { children: brand.productName }), _jsx("small", { children: brand.organizationName })] })] }), _jsx(PortalCurrentSubjectSlot, {}), _jsxs("div", { className: "uhcp-account-menu", ref: ref, children: [_jsxs("button", { type: "button", className: "uhcp-trigger", onClick: () => setOpen((value) => !value), "aria-label": copy.accountAndPreferences, "aria-expanded": open, children: [_jsx(PersonIcon, {}), accountIdentifier && _jsx("span", { className: "uhcp-account-id", children: accountIdentifier }), _jsx("span", { "aria-hidden": "true", children: "\u2304" })] }), open && (_jsxs("section", { className: "uhcp-menu", "aria-label": copy.accountAndPreferences, children: [_jsxs("div", { className: "uhcp-account-state", children: [_jsx("strong", { children: copy.accountAndPreferences }), _jsx("small", { children: accountIdentifier || copy.notSignedIn })] }), accountOptions.length > 0 && (_jsx("div", { className: "uhcp-account-options", children: accountOptions.map((option) => (_jsxs("button", { type: "button", disabled: option.disabled, "aria-pressed": Boolean(option.selected), onClick: () => {
64
+ return (_jsxs("header", { className: "uhcp-header", style: brand.variables, children: [_jsxs("a", { className: "uhcp-brand", href: brand.homeHref, "aria-label": copy.home, children: [_jsx(PortalBrandArtwork, { artwork: brand.mark, className: "uhcp-brand-mark" }), _jsxs("span", { children: [_jsx("strong", { children: brand.productName }), _jsx("small", { className: accountIdentifier ? 'uhcp-brand-account' : undefined, children: accountIdentifier || brand.organizationName })] })] }), currentSubject
65
+ ? _jsx(PortalCurrentSubjectControl, { ...currentSubject })
66
+ : _jsx(PortalCurrentSubjectSlot, {}), _jsxs("div", { className: "uhcp-account-menu", ref: ref, children: [_jsx("button", { type: "button", className: "uhcp-trigger", onClick: () => setOpen((value) => !value), "aria-label": copy.accountAndPreferences, "aria-expanded": open, children: _jsx(PersonIcon, {}) }), open && (_jsxs("section", { className: "uhcp-menu", "aria-label": copy.accountAndPreferences, children: [_jsxs("div", { className: "uhcp-account-state", children: [_jsx("strong", { children: copy.accountAndPreferences }), _jsx("small", { children: accountIdentifier || copy.notSignedIn })] }), accountOptions.length > 0 && (_jsx("div", { className: "uhcp-account-options", children: accountOptions.map((option) => (_jsxs("button", { type: "button", disabled: option.disabled, "aria-pressed": Boolean(option.selected), onClick: () => {
65
67
  onSelectAccount?.(option.id);
66
68
  setOpen(false);
67
69
  }, children: [_jsx("span", { children: option.label }), option.description && _jsx("small", { children: option.description })] }, option.id))) })), _jsx(PreferenceGroup, { label: copy.appearance, children: portalUiThemes.map((value) => (_jsxs("button", { type: "button", "aria-pressed": theme === value, onClick: () => changeTheme(value), children: [_jsx("span", { "aria-hidden": "true", children: value === 'light' ? '☀' : value === 'dark' ? '☾' : '◫' }), copy.themes[value]] }, value))) }), _jsx(PreferenceGroup, { label: copy.textSize, children: portalUiDensities.map((value) => (_jsx("button", { type: "button", "aria-pressed": density === value, onClick: () => changeDensity(value), children: value === 'small' ? 'S' : value === 'medium' ? 'M' : 'L' }, value))) }), _jsxs("div", { className: "uhcp-menu-actions", children: [languageOptions.length > 0
@@ -0,0 +1,29 @@
1
+ export type ProfileRecoveryMessages = Readonly<{
2
+ open: string;
3
+ title: string;
4
+ explanation: string;
5
+ recoveryCode: string;
6
+ recoveryCodeHelp: string;
7
+ newPin: string;
8
+ confirmPin: string;
9
+ submit: string;
10
+ cancel: string;
11
+ mismatch: string;
12
+ completed: string;
13
+ replacementCode: string;
14
+ replacementCodeHelp: string;
15
+ notificationQueued: string;
16
+ }>;
17
+ /** Collects recovery proof and a replacement PIN without receiving wallet keys. */
18
+ export declare function ProfileRecoveryPanel({ profileId, messages, disabled, onRecover, onClose }: Readonly<{
19
+ profileId: string;
20
+ messages: ProfileRecoveryMessages;
21
+ disabled?: boolean;
22
+ onRecover: (input: {
23
+ profileId: string;
24
+ recoveryCode: string;
25
+ newPin: string;
26
+ }) => Promise<string>;
27
+ onClose: () => void;
28
+ }>): import("react").JSX.Element;
29
+ //# sourceMappingURL=ProfileRecoveryPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProfileRecoveryPanel.d.ts","sourceRoot":"","sources":["../src/ProfileRecoveryPanel.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,gBAAgB,EAAE,MAAM,CAAA;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,kBAAkB,EAAE,MAAM,CAAA;CAC3B,CAAC,CAAA;AAEF,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAgB,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,QAAQ,CAAC;IAC3G,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,uBAAuB,CAAA;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAClG,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAC,+BAwCD"}
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ /** Collects recovery proof and a replacement PIN without receiving wallet keys. */
5
+ export function ProfileRecoveryPanel({ profileId, messages, disabled = false, onRecover, onClose }) {
6
+ const [busy, setBusy] = useState(false);
7
+ const [error, setError] = useState('');
8
+ const [replacementCode, setReplacementCode] = useState('');
9
+ async function submit(event) {
10
+ event.preventDefault();
11
+ setError('');
12
+ const form = new FormData(event.currentTarget);
13
+ const newPin = String(form.get('newPin') || '');
14
+ if (newPin !== String(form.get('confirmPin') || '')) {
15
+ setError(messages.mismatch);
16
+ return;
17
+ }
18
+ setBusy(true);
19
+ try {
20
+ setReplacementCode(await onRecover({ profileId, recoveryCode: String(form.get('recoveryCode') || ''), newPin }));
21
+ }
22
+ catch (reason) {
23
+ setError(reason instanceof Error ? reason.message : 'profile_recovery_failed');
24
+ }
25
+ finally {
26
+ setBusy(false);
27
+ }
28
+ }
29
+ if (replacementCode)
30
+ return _jsxs("section", { className: "panel profile-recovery-result", role: "status", children: [_jsx("strong", { children: messages.completed }), _jsxs("label", { children: [messages.replacementCode, _jsx("textarea", { readOnly: true, rows: 3, value: replacementCode, "aria-label": messages.replacementCode })] }), _jsx("p", { children: messages.replacementCodeHelp }), _jsx("p", { children: messages.notificationQueued }), _jsx("button", { type: "button", "data-portal-action": "close-profile-recovery", onClick: onClose, children: messages.cancel })] });
31
+ return _jsxs("form", { className: "panel form profile-recovery-form", onSubmit: submit, children: [_jsxs("div", { children: [_jsx("h3", { children: messages.title }), _jsx("p", { className: "muted", children: messages.explanation })] }), _jsxs("label", { children: [messages.recoveryCode, _jsx("textarea", { name: "recoveryCode", rows: 3, required: true, autoComplete: "off", "aria-describedby": "profile-recovery-help" })] }), _jsx("p", { id: "profile-recovery-help", className: "technical-note", children: messages.recoveryCodeHelp }), _jsxs("label", { children: [messages.newPin, _jsx("input", { name: "newPin", type: "password", minLength: 6, required: true, autoComplete: "new-password" })] }), _jsxs("label", { children: [messages.confirmPin, _jsx("input", { name: "confirmPin", type: "password", minLength: 6, required: true, autoComplete: "new-password" })] }), error ? _jsx("p", { role: "alert", children: error }) : null, _jsxs("div", { className: "inline-actions", children: [_jsx("button", { "data-portal-action": "submit-profile-recovery", disabled: busy, children: messages.submit }), _jsx("button", { type: "button", "data-portal-action": "cancel-profile-recovery", className: "secondary", disabled: busy || disabled, onClick: onClose, children: messages.cancel })] })] });
32
+ }
package/dist/index.d.ts CHANGED
@@ -50,4 +50,9 @@ export * from './notification-bff-client.js';
50
50
  export * from './CommunicationsWorkspace.js';
51
51
  export * from './SelectedSubjectWorkspace.js';
52
52
  export * from './SubjectWorkspace.js';
53
+ export * from './CardPreview.js';
54
+ export * from './CollapsiblePortalSection.js';
55
+ export * from './ElapsedOperationStatus.js';
56
+ export * from './ProfileRecoveryPanel.js';
57
+ export * from './passkey-policy.js';
53
58
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,mBAAmB,CAAA;AACjC,cAAc,2BAA2B,CAAA;AACzC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,wCAAwC,CAAA;AACtD,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,mCAAmC,CAAA;AACjD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,qCAAqC,CAAA;AACnD,cAAc,mCAAmC,CAAA;AACjD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,wBAAwB,CAAA;AACtC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,0CAA0C,CAAA;AACxD,cAAc,kCAAkC,CAAA;AAChD,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,oCAAoC,CAAA;AAClD,cAAc,sCAAsC,CAAA;AACpD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uCAAuC,CAAA;AACrD,cAAc,uBAAuB,CAAA;AACrC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,mBAAmB,CAAA;AACjC,cAAc,2BAA2B,CAAA;AACzC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,wCAAwC,CAAA;AACtD,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,mCAAmC,CAAA;AACjD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,qCAAqC,CAAA;AACnD,cAAc,mCAAmC,CAAA;AACjD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,wBAAwB,CAAA;AACtC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,0CAA0C,CAAA;AACxD,cAAc,kCAAkC,CAAA;AAChD,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,oCAAoC,CAAA;AAClD,cAAc,sCAAsC,CAAA;AACpD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uCAAuC,CAAA;AACrD,cAAc,uBAAuB,CAAA;AACrC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,qBAAqB,CAAA"}
package/dist/index.js CHANGED
@@ -50,3 +50,8 @@ export * from './notification-bff-client.js';
50
50
  export * from './CommunicationsWorkspace.js';
51
51
  export * from './SelectedSubjectWorkspace.js';
52
52
  export * from './SubjectWorkspace.js';
53
+ export * from './CardPreview.js';
54
+ export * from './CollapsiblePortalSection.js';
55
+ export * from './ElapsedOperationStatus.js';
56
+ export * from './ProfileRecoveryPanel.js';
57
+ export * from './passkey-policy.js';
@@ -0,0 +1,9 @@
1
+ /** Reports only a positive browser claim for a user-verifying platform authenticator. */
2
+ export declare function isUserVerifyingPlatformPasskeyAvailable(): Promise<boolean>;
3
+ /** Requires WebAuthn enrollment to finish before issuing a usable wallet session. */
4
+ export declare function openWalletAfterRequiredPasskey<T>(input: Readonly<{
5
+ passkeyRequired: boolean;
6
+ enroll(): Promise<void>;
7
+ open(): Promise<T>;
8
+ }>): Promise<T>;
9
+ //# sourceMappingURL=passkey-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey-policy.d.ts","sourceRoot":"","sources":["../src/passkey-policy.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,wBAAsB,uCAAuC,IAAI,OAAO,CAAC,OAAO,CAAC,CAQhF;AAED,qFAAqF;AACrF,wBAAsB,8BAA8B,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC;IACtE,eAAe,EAAE,OAAO,CAAA;IACxB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACvB,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,CAAA;CACnB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAGd"}
@@ -0,0 +1,18 @@
1
+ /** Reports only a positive browser claim for a user-verifying platform authenticator. */
2
+ export async function isUserVerifyingPlatformPasskeyAvailable() {
3
+ if (typeof PublicKeyCredential === 'undefined'
4
+ || typeof PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable !== 'function')
5
+ return false;
6
+ try {
7
+ return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ /** Requires WebAuthn enrollment to finish before issuing a usable wallet session. */
14
+ export async function openWalletAfterRequiredPasskey(input) {
15
+ if (input.passkeyRequired)
16
+ await input.enroll();
17
+ return input.open();
18
+ }
package/dist/styles.css CHANGED
@@ -51,7 +51,7 @@ html[data-theme='dark'] {
51
51
  }
52
52
  .uhcp-brand { align-items: center; color: var(--uhcp-text); display: flex; gap: .75rem; text-decoration: none; }
53
53
  .uhcp-brand > span:last-child { display: grid; line-height: 1.15; }
54
- .uhcp-brand small { color: var(--uhcp-muted); }
54
+ .uhcp-brand small { color: var(--uhcp-muted); max-width: min(22rem, 42vw); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
55
55
  .uhcp-brand-mark {
56
56
  align-items: center;
57
57
  display: inline-flex;
@@ -80,9 +80,10 @@ html[data-theme='dark'] .uhcp-brand-artwork-dark { display: block !important; }
80
80
  color: var(--uhcp-text);
81
81
  cursor: pointer;
82
82
  display: flex;
83
- gap: .5rem;
84
- max-width: min(19rem, 48vw);
85
- padding: .7rem .85rem;
83
+ justify-content: center;
84
+ min-height: 2.75rem;
85
+ min-width: 2.75rem;
86
+ padding: .7rem;
86
87
  }
87
88
  .uhcp-person-icon { height: 1.25rem; min-width: 1.25rem; width: 1.25rem; }
88
89
  .uhcp-account-id { font-size: .78rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -709,7 +710,7 @@ html[data-theme='dark'] .uhcp-brand-artwork-dark { display: block !important; }
709
710
  .uhcp-floating-back { border-radius: 50%; bottom: 1.25rem; height: 3.5rem; left: 1.25rem; width: 3.5rem; }
710
711
  .uhcp-floating-assistant { border-radius: 999px; bottom: 1.25rem; gap: .5rem; padding: 1rem 1.25rem; right: 1.25rem; }
711
712
  @media (max-width: 620px) {
712
- .uhcp-account-id, .uhcp-brand small { display: none; }
713
+ .uhcp-brand small { max-width: 48vw; }
713
714
  .uhcp-entry-hero { grid-template-columns: 1fr; }
714
715
  .uhcp-access-grid { grid-template-columns: 1fr; }
715
716
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "uhc-portal-components-react",
3
- "version": "0.2.20",
4
- "description": "Brand-neutral React portal shell, account preferences and protected-profile UI for UHC and VetChain portals.",
3
+ "version": "0.2.22",
4
+ "description": "Brand-neutral React portal screens, account preferences and protected-profile UI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "files": [