@alfadocs/ui-kit-debug 0.9.1 → 0.10.0
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/dist/_chunks/{patient-shell-BS2V6V1b.js → patient-shell-CL20JnVJ.js} +2 -2
- package/dist/_chunks/{patient-shell-BS2V6V1b.js.map → patient-shell-CL20JnVJ.js.map} +1 -1
- package/dist/_chunks/rating-BRD7O74e.js +171 -0
- package/dist/_chunks/rating-BRD7O74e.js.map +1 -0
- package/dist/_chunks/{sidebar-CoLHtVrP.js → sidebar-D8Lq065m.js} +107 -121
- package/dist/_chunks/{sidebar-CoLHtVrP.js.map → sidebar-D8Lq065m.js.map} +1 -1
- package/dist/_chunks/star-vav0SJ9e.js +20 -0
- package/dist/_chunks/star-vav0SJ9e.js.map +1 -0
- package/dist/agent-catalog.json +15 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/rating/index.d.ts +3 -0
- package/dist/components/rating/index.d.ts.map +1 -0
- package/dist/components/rating/index.js +5 -0
- package/dist/components/rating/index.js.map +1 -0
- package/dist/components/rating/rating.d.ts +29 -0
- package/dist/components/rating/rating.d.ts.map +1 -0
- package/dist/components/sidebar/index.js +1 -1
- package/dist/i18n/config.js +18 -0
- package/dist/i18n/config.js.map +1 -1
- package/dist/i18n/resources.d.ts +15 -0
- package/dist/i18n/resources.d.ts.map +1 -1
- package/dist/index.js +321 -319
- package/dist/index.js.map +1 -1
- package/dist/locales/de.json +5 -0
- package/dist/locales/en.json +5 -0
- package/dist/locales/it.json +5 -0
- package/dist/patterns/patient-shell/index.js +1 -1
- package/dist/tokens.css +1 -1
- package/package.json +5 -1
|
@@ -6,7 +6,7 @@ import { I } from "./icon-button-C4CGcYuz.js";
|
|
|
6
6
|
import { D as n } from "./dropdown-menu-dyV7gHh_.js";
|
|
7
7
|
import { H as L, g as A, f as H, e as x, a as C, c as M, d as B } from "./header-D0ULgQl3.js";
|
|
8
8
|
import { L as N } from "./logo-_Z-jLq80.js";
|
|
9
|
-
import { S as T, a as _, h as O, j as P, k as W, i as j } from "./sidebar-
|
|
9
|
+
import { S as T, a as _, h as O, j as P, k as W, i as j } from "./sidebar-D8Lq065m.js";
|
|
10
10
|
import { T as z } from "./theme-root-CSKD5ZRm.js";
|
|
11
11
|
import { a as R } from "./tooltip-DHik5yRI.js";
|
|
12
12
|
import { u as D } from "./use-theme-B1cwAXJR.js";
|
|
@@ -185,4 +185,4 @@ V.displayName = "PatientShell";
|
|
|
185
185
|
export {
|
|
186
186
|
V as P
|
|
187
187
|
};
|
|
188
|
-
//# sourceMappingURL=patient-shell-
|
|
188
|
+
//# sourceMappingURL=patient-shell-CL20JnVJ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patient-shell-BS2V6V1b.js","sources":["../../node_modules/lucide-react/dist/esm/icons/maximize-2.js","../../node_modules/lucide-react/dist/esm/icons/settings.js","../../src/patterns/patient-shell/patient-shell.tsx"],"sourcesContent":["/**\n * @license lucide-react v1.8.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"M15 3h6v6\", key: \"1q9fwt\" }],\n [\"path\", { d: \"m21 3-7 7\", key: \"1l2asr\" }],\n [\"path\", { d: \"m3 21 7-7\", key: \"tjx5ai\" }],\n [\"path\", { d: \"M9 21H3v-6\", key: \"wtvkvv\" }]\n];\nconst Maximize2 = createLucideIcon(\"maximize-2\", __iconNode);\n\nexport { __iconNode, Maximize2 as default };\n//# sourceMappingURL=maximize-2.js.map\n","/**\n * @license lucide-react v1.8.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\",\n key: \"1i5ecw\"\n }\n ],\n [\"circle\", { cx: \"12\", cy: \"12\", r: \"3\", key: \"1v7zrd\" }]\n];\nconst Settings = createLucideIcon(\"settings\", __iconNode);\n\nexport { __iconNode, Settings as default };\n//# sourceMappingURL=settings.js.map\n","/**\n * Patient Shell — the chrome for AlfaDocs's patient-facing users\n * (patients managing their own appointments and records).\n *\n * Deliberately flatter than the clinician `AppFrame` composition: no\n * command palette, no favourites, no practice/chain affordances, no\n * Leo, no privacy lock. Mirrors the legacy `_header.html.twig`\n * `app.user.type == 'patient'` branch and the `knp_menu_render('pcp')`\n * flat nav menu.\n *\n * Unlike most patterns in this folder, PatientShell exports a runtime\n * component because the consuming patient-portal app mounts it in\n * production — the exports are surfaced via `src/patterns/index.ts`\n * and included in the library bundle.\n */\nimport { forwardRef, useState, type ReactNode } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { LogOut, Maximize2, Settings as SettingsIcon } from 'lucide-react';\n\nimport { AppFrame } from '../../components/app-frame';\nimport { IconButton } from '../../components/button';\nimport { DropdownMenu } from '../../components/dropdown-menu';\nimport {\n Header,\n HeaderBrand,\n HeaderCenter,\n HeaderEnd,\n HeaderMenuButton,\n HeaderSkipLink,\n HeaderStart,\n} from '../../components/header';\nimport { Logo } from '../../components/logo';\nimport {\n Sidebar,\n SidebarBody,\n SidebarItem,\n SidebarItemBadge,\n SidebarItemIcon,\n SidebarItemLabel,\n type SidebarMode,\n} from '../../components/sidebar';\nimport { ThemeRoot } from '../../components/theme-root';\nimport { TooltipProvider } from '../../components/tooltip';\nimport { useTheme } from '../../hooks/use-theme';\n\n/* ------------------------------------------------------------------ */\n/* Public API */\n/* ------------------------------------------------------------------ */\n\nexport interface PatientNavItem {\n id: string;\n /** Pre-translated label — patients' nav labels are emitted already\n * translated by the consuming app's backend (legacy `knp_menu`). */\n label: string;\n href: string;\n /** Optional leading icon. Pass a lucide-react or equivalent element. */\n icon?: ReactNode;\n /** Optional unseen-item count rendered as a pill badge. */\n badgeCount?: number;\n /** Mark the item as the current page (`aria-current='page'`). */\n isActive?: boolean;\n}\n\nexport interface PatientShellProps {\n /** Patient's display name. Used as the avatar fallback by consumers\n * that read the name through a slot; not rendered inside this shell\n * today because the legacy patient chrome carries no profile pill. */\n userName: string;\n /** Optional avatar URL. Reserved for future profile-pill surfaces;\n * no visible effect today. */\n userAvatarSrc?: string;\n /** Flat nav items rendered in the sidebar, in order. */\n menuItems: PatientNavItem[];\n /** URL the \"Logout\" action navigates to — typically `/logout`. */\n logoutHref: string;\n /** Fires when the user toggles fullscreen from the settings menu. */\n onToggleFullscreen?: () => void;\n /** Optional brand logo. Defaults to\n * `<Logo variant=\"monochrome\" size=\"md\" decorative />`. */\n brandLogo?: ReactNode;\n /** Link target for the brand — typically the patient dashboard. */\n brandHref?: string;\n /** Override the accessible name of the `<main>` landmark. */\n mainAriaLabel?: string;\n /**\n * Force the sidebar into a specific mode. Consuming apps typically\n * flip to `'overlay'` on narrow viewports and leave this undefined\n * elsewhere (the sidebar then defaults to `'expanded'`).\n */\n sidebarState?: SidebarMode;\n /** Children render inside the main content region. */\n children: ReactNode;\n}\n\n/* ------------------------------------------------------------------ */\n/* Internals */\n/* ------------------------------------------------------------------ */\n\nfunction formatBadgeCount(count: number): string {\n return count > 99 ? '99+' : String(count);\n}\n\n/**\n * Guard against `javascript:` and other non-navigation schemes before\n * handing the URL to `window.location.assign`. A consuming app that lets\n * attacker-controlled input reach `logoutHref` would otherwise execute\n * script. Allow only relative paths, hash fragments, and `http(s)` URLs.\n */\nfunction isSafeLogoutHref(href: string): boolean {\n if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {\n return true;\n }\n try {\n const url = new URL(href, window.location.origin);\n return url.protocol === 'http:' || url.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\nfunction SettingsMenu({\n logoutHref,\n onToggleFullscreen,\n}: {\n logoutHref: string;\n onToggleFullscreen?: () => void;\n}) {\n const { t } = useTranslation();\n return (\n <DropdownMenu.Root>\n <DropdownMenu.Trigger asChild>\n <IconButton\n icon={<SettingsIcon aria-hidden=\"true\" />}\n aria-label={t('patientShell.settings.triggerAria')}\n tooltip={t('patientShell.settings.triggerAria')}\n data-testid=\"patient-shell-settings-trigger\"\n />\n </DropdownMenu.Trigger>\n <DropdownMenu.Content align=\"end\">\n <DropdownMenu.Item\n startIcon={<Maximize2 aria-hidden=\"true\" />}\n onSelect={onToggleFullscreen}\n data-testid=\"patient-shell-fullscreen\"\n >\n {t('patientShell.settings.fullscreen')}\n </DropdownMenu.Item>\n <DropdownMenu.Separator />\n <DropdownMenu.Item\n startIcon={<LogOut aria-hidden=\"true\" />}\n onSelect={() => {\n if (typeof window === 'undefined') return;\n if (!isSafeLogoutHref(logoutHref)) return;\n window.location.assign(logoutHref);\n }}\n data-testid=\"patient-shell-logout\"\n >\n {t('patientShell.settings.logout')}\n </DropdownMenu.Item>\n </DropdownMenu.Content>\n </DropdownMenu.Root>\n );\n}\n\nfunction PatientSidebarNav({ menuItems }: { menuItems: PatientNavItem[] }) {\n return (\n <SidebarBody>\n {menuItems.map((item) => {\n const badge =\n typeof item.badgeCount === 'number' && item.badgeCount > 0\n ? item.badgeCount\n : undefined;\n return (\n <SidebarItem\n key={item.id}\n href={item.href}\n aria-label={item.label}\n isActive={item.isActive}\n >\n {item.icon ? <SidebarItemIcon>{item.icon}</SidebarItemIcon> : null}\n <SidebarItemLabel>{item.label}</SidebarItemLabel>\n {badge !== undefined ? (\n <SidebarItemBadge>{formatBadgeCount(badge)}</SidebarItemBadge>\n ) : null}\n </SidebarItem>\n );\n })}\n </SidebarBody>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* Root */\n/* ------------------------------------------------------------------ */\n\nexport const PatientShell = forwardRef<HTMLDivElement, PatientShellProps>(\n (\n {\n menuItems,\n logoutHref,\n onToggleFullscreen,\n brandLogo,\n brandHref = '/',\n mainAriaLabel,\n sidebarState,\n children,\n // `userName` and `userAvatarSrc` are declared on the public API\n // for forward-compatibility (future profile-pill work) — they are\n // intentionally unused today because the legacy patient chrome\n // carries no profile pill.\n userName: _userName,\n userAvatarSrc: _userAvatarSrc,\n },\n ref,\n ) => {\n const { t } = useTranslation();\n const { resolvedTheme } = useTheme();\n const themeBase = resolvedTheme.startsWith('dark') ? 'dark' : 'light';\n const themeAccessible = resolvedTheme.endsWith('-accessible');\n\n // Narrow-viewport overlay: `HeaderMenuButton` only renders below the\n // `md` breakpoint, and toggling opens the Sidebar's overlay Dialog.\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const resolvedLogo = brandLogo ?? (\n <Logo variant=\"monochrome\" size=\"md\" decorative />\n );\n\n return (\n <ThemeRoot\n ref={ref}\n theme={themeBase}\n accessible={themeAccessible}\n className=\"ds:contents\"\n >\n <TooltipProvider>\n <AppFrame\n mainAriaLabel={mainAriaLabel}\n header={\n <Header>\n <HeaderStart>\n <HeaderSkipLink href=\"#main-content\" />\n <HeaderMenuButton onMenuOpen={() => setSidebarOpen(true)} />\n <HeaderBrand\n logo={resolvedLogo}\n href={brandHref}\n aria-label={t('patientShell.brand.homeAria')}\n />\n </HeaderStart>\n <HeaderCenter />\n <HeaderEnd>\n <SettingsMenu\n logoutHref={logoutHref}\n onToggleFullscreen={onToggleFullscreen}\n />\n </HeaderEnd>\n </Header>\n }\n sidebar={\n <Sidebar\n {...(sidebarState !== undefined\n ? { state: sidebarState }\n : { defaultState: 'expanded' as const })}\n open={sidebarOpen}\n onOpenChange={setSidebarOpen}\n aria-label={t('patientShell.sidebar.panelLabel')}\n data-testid=\"patient-shell-sidebar\"\n >\n <PatientSidebarNav menuItems={menuItems} />\n </Sidebar>\n }\n >\n {children}\n </AppFrame>\n </TooltipProvider>\n </ThemeRoot>\n );\n },\n);\n\nPatientShell.displayName = 'PatientShell';\n"],"names":["__iconNode","Maximize2","createLucideIcon","Settings","formatBadgeCount","count","isSafeLogoutHref","href","url","SettingsMenu","logoutHref","onToggleFullscreen","t","useTranslation","jsxs","DropdownMenu","jsx","IconButton","SettingsIcon","LogOut","PatientSidebarNav","menuItems","SidebarBody","item","badge","SidebarItem","SidebarItemIcon","SidebarItemLabel","SidebarItemBadge","PatientShell","forwardRef","brandLogo","brandHref","mainAriaLabel","sidebarState","children","_userName","_userAvatarSrc","ref","resolvedTheme","useTheme","themeBase","themeAccessible","sidebarOpen","setSidebarOpen","useState","ThemeRoot","TooltipProvider","AppFrame","Header","HeaderStart","HeaderSkipLink","HeaderMenuButton","HeaderBrand","Logo","HeaderCenter","HeaderEnd","Sidebar"],"mappings":"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","x_google_ignoreList":[0,1]}
|
|
1
|
+
{"version":3,"file":"patient-shell-CL20JnVJ.js","sources":["../../node_modules/lucide-react/dist/esm/icons/maximize-2.js","../../node_modules/lucide-react/dist/esm/icons/settings.js","../../src/patterns/patient-shell/patient-shell.tsx"],"sourcesContent":["/**\n * @license lucide-react v1.8.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"M15 3h6v6\", key: \"1q9fwt\" }],\n [\"path\", { d: \"m21 3-7 7\", key: \"1l2asr\" }],\n [\"path\", { d: \"m3 21 7-7\", key: \"tjx5ai\" }],\n [\"path\", { d: \"M9 21H3v-6\", key: \"wtvkvv\" }]\n];\nconst Maximize2 = createLucideIcon(\"maximize-2\", __iconNode);\n\nexport { __iconNode, Maximize2 as default };\n//# sourceMappingURL=maximize-2.js.map\n","/**\n * @license lucide-react v1.8.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\",\n key: \"1i5ecw\"\n }\n ],\n [\"circle\", { cx: \"12\", cy: \"12\", r: \"3\", key: \"1v7zrd\" }]\n];\nconst Settings = createLucideIcon(\"settings\", __iconNode);\n\nexport { __iconNode, Settings as default };\n//# sourceMappingURL=settings.js.map\n","/**\n * Patient Shell — the chrome for AlfaDocs's patient-facing users\n * (patients managing their own appointments and records).\n *\n * Deliberately flatter than the clinician `AppFrame` composition: no\n * command palette, no favourites, no practice/chain affordances, no\n * Leo, no privacy lock. Mirrors the legacy `_header.html.twig`\n * `app.user.type == 'patient'` branch and the `knp_menu_render('pcp')`\n * flat nav menu.\n *\n * Unlike most patterns in this folder, PatientShell exports a runtime\n * component because the consuming patient-portal app mounts it in\n * production — the exports are surfaced via `src/patterns/index.ts`\n * and included in the library bundle.\n */\nimport { forwardRef, useState, type ReactNode } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { LogOut, Maximize2, Settings as SettingsIcon } from 'lucide-react';\n\nimport { AppFrame } from '../../components/app-frame';\nimport { IconButton } from '../../components/button';\nimport { DropdownMenu } from '../../components/dropdown-menu';\nimport {\n Header,\n HeaderBrand,\n HeaderCenter,\n HeaderEnd,\n HeaderMenuButton,\n HeaderSkipLink,\n HeaderStart,\n} from '../../components/header';\nimport { Logo } from '../../components/logo';\nimport {\n Sidebar,\n SidebarBody,\n SidebarItem,\n SidebarItemBadge,\n SidebarItemIcon,\n SidebarItemLabel,\n type SidebarMode,\n} from '../../components/sidebar';\nimport { ThemeRoot } from '../../components/theme-root';\nimport { TooltipProvider } from '../../components/tooltip';\nimport { useTheme } from '../../hooks/use-theme';\n\n/* ------------------------------------------------------------------ */\n/* Public API */\n/* ------------------------------------------------------------------ */\n\nexport interface PatientNavItem {\n id: string;\n /** Pre-translated label — patients' nav labels are emitted already\n * translated by the consuming app's backend (legacy `knp_menu`). */\n label: string;\n href: string;\n /** Optional leading icon. Pass a lucide-react or equivalent element. */\n icon?: ReactNode;\n /** Optional unseen-item count rendered as a pill badge. */\n badgeCount?: number;\n /** Mark the item as the current page (`aria-current='page'`). */\n isActive?: boolean;\n}\n\nexport interface PatientShellProps {\n /** Patient's display name. Used as the avatar fallback by consumers\n * that read the name through a slot; not rendered inside this shell\n * today because the legacy patient chrome carries no profile pill. */\n userName: string;\n /** Optional avatar URL. Reserved for future profile-pill surfaces;\n * no visible effect today. */\n userAvatarSrc?: string;\n /** Flat nav items rendered in the sidebar, in order. */\n menuItems: PatientNavItem[];\n /** URL the \"Logout\" action navigates to — typically `/logout`. */\n logoutHref: string;\n /** Fires when the user toggles fullscreen from the settings menu. */\n onToggleFullscreen?: () => void;\n /** Optional brand logo. Defaults to\n * `<Logo variant=\"monochrome\" size=\"md\" decorative />`. */\n brandLogo?: ReactNode;\n /** Link target for the brand — typically the patient dashboard. */\n brandHref?: string;\n /** Override the accessible name of the `<main>` landmark. */\n mainAriaLabel?: string;\n /**\n * Force the sidebar into a specific mode. Consuming apps typically\n * flip to `'overlay'` on narrow viewports and leave this undefined\n * elsewhere (the sidebar then defaults to `'expanded'`).\n */\n sidebarState?: SidebarMode;\n /** Children render inside the main content region. */\n children: ReactNode;\n}\n\n/* ------------------------------------------------------------------ */\n/* Internals */\n/* ------------------------------------------------------------------ */\n\nfunction formatBadgeCount(count: number): string {\n return count > 99 ? '99+' : String(count);\n}\n\n/**\n * Guard against `javascript:` and other non-navigation schemes before\n * handing the URL to `window.location.assign`. A consuming app that lets\n * attacker-controlled input reach `logoutHref` would otherwise execute\n * script. Allow only relative paths, hash fragments, and `http(s)` URLs.\n */\nfunction isSafeLogoutHref(href: string): boolean {\n if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {\n return true;\n }\n try {\n const url = new URL(href, window.location.origin);\n return url.protocol === 'http:' || url.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\nfunction SettingsMenu({\n logoutHref,\n onToggleFullscreen,\n}: {\n logoutHref: string;\n onToggleFullscreen?: () => void;\n}) {\n const { t } = useTranslation();\n return (\n <DropdownMenu.Root>\n <DropdownMenu.Trigger asChild>\n <IconButton\n icon={<SettingsIcon aria-hidden=\"true\" />}\n aria-label={t('patientShell.settings.triggerAria')}\n tooltip={t('patientShell.settings.triggerAria')}\n data-testid=\"patient-shell-settings-trigger\"\n />\n </DropdownMenu.Trigger>\n <DropdownMenu.Content align=\"end\">\n <DropdownMenu.Item\n startIcon={<Maximize2 aria-hidden=\"true\" />}\n onSelect={onToggleFullscreen}\n data-testid=\"patient-shell-fullscreen\"\n >\n {t('patientShell.settings.fullscreen')}\n </DropdownMenu.Item>\n <DropdownMenu.Separator />\n <DropdownMenu.Item\n startIcon={<LogOut aria-hidden=\"true\" />}\n onSelect={() => {\n if (typeof window === 'undefined') return;\n if (!isSafeLogoutHref(logoutHref)) return;\n window.location.assign(logoutHref);\n }}\n data-testid=\"patient-shell-logout\"\n >\n {t('patientShell.settings.logout')}\n </DropdownMenu.Item>\n </DropdownMenu.Content>\n </DropdownMenu.Root>\n );\n}\n\nfunction PatientSidebarNav({ menuItems }: { menuItems: PatientNavItem[] }) {\n return (\n <SidebarBody>\n {menuItems.map((item) => {\n const badge =\n typeof item.badgeCount === 'number' && item.badgeCount > 0\n ? item.badgeCount\n : undefined;\n return (\n <SidebarItem\n key={item.id}\n href={item.href}\n aria-label={item.label}\n isActive={item.isActive}\n >\n {item.icon ? <SidebarItemIcon>{item.icon}</SidebarItemIcon> : null}\n <SidebarItemLabel>{item.label}</SidebarItemLabel>\n {badge !== undefined ? (\n <SidebarItemBadge>{formatBadgeCount(badge)}</SidebarItemBadge>\n ) : null}\n </SidebarItem>\n );\n })}\n </SidebarBody>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* Root */\n/* ------------------------------------------------------------------ */\n\nexport const PatientShell = forwardRef<HTMLDivElement, PatientShellProps>(\n (\n {\n menuItems,\n logoutHref,\n onToggleFullscreen,\n brandLogo,\n brandHref = '/',\n mainAriaLabel,\n sidebarState,\n children,\n // `userName` and `userAvatarSrc` are declared on the public API\n // for forward-compatibility (future profile-pill work) — they are\n // intentionally unused today because the legacy patient chrome\n // carries no profile pill.\n userName: _userName,\n userAvatarSrc: _userAvatarSrc,\n },\n ref,\n ) => {\n const { t } = useTranslation();\n const { resolvedTheme } = useTheme();\n const themeBase = resolvedTheme.startsWith('dark') ? 'dark' : 'light';\n const themeAccessible = resolvedTheme.endsWith('-accessible');\n\n // Narrow-viewport overlay: `HeaderMenuButton` only renders below the\n // `md` breakpoint, and toggling opens the Sidebar's overlay Dialog.\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const resolvedLogo = brandLogo ?? (\n <Logo variant=\"monochrome\" size=\"md\" decorative />\n );\n\n return (\n <ThemeRoot\n ref={ref}\n theme={themeBase}\n accessible={themeAccessible}\n className=\"ds:contents\"\n >\n <TooltipProvider>\n <AppFrame\n mainAriaLabel={mainAriaLabel}\n header={\n <Header>\n <HeaderStart>\n <HeaderSkipLink href=\"#main-content\" />\n <HeaderMenuButton onMenuOpen={() => setSidebarOpen(true)} />\n <HeaderBrand\n logo={resolvedLogo}\n href={brandHref}\n aria-label={t('patientShell.brand.homeAria')}\n />\n </HeaderStart>\n <HeaderCenter />\n <HeaderEnd>\n <SettingsMenu\n logoutHref={logoutHref}\n onToggleFullscreen={onToggleFullscreen}\n />\n </HeaderEnd>\n </Header>\n }\n sidebar={\n <Sidebar\n {...(sidebarState !== undefined\n ? { state: sidebarState }\n : { defaultState: 'expanded' as const })}\n open={sidebarOpen}\n onOpenChange={setSidebarOpen}\n aria-label={t('patientShell.sidebar.panelLabel')}\n data-testid=\"patient-shell-sidebar\"\n >\n <PatientSidebarNav menuItems={menuItems} />\n </Sidebar>\n }\n >\n {children}\n </AppFrame>\n </TooltipProvider>\n </ThemeRoot>\n );\n },\n);\n\nPatientShell.displayName = 'PatientShell';\n"],"names":["__iconNode","Maximize2","createLucideIcon","Settings","formatBadgeCount","count","isSafeLogoutHref","href","url","SettingsMenu","logoutHref","onToggleFullscreen","t","useTranslation","jsxs","DropdownMenu","jsx","IconButton","SettingsIcon","LogOut","PatientSidebarNav","menuItems","SidebarBody","item","badge","SidebarItem","SidebarItemIcon","SidebarItemLabel","SidebarItemBadge","PatientShell","forwardRef","brandLogo","brandHref","mainAriaLabel","sidebarState","children","_userName","_userAvatarSrc","ref","resolvedTheme","useTheme","themeBase","themeAccessible","sidebarOpen","setSidebarOpen","useState","ThemeRoot","TooltipProvider","AppFrame","Header","HeaderStart","HeaderSkipLink","HeaderMenuButton","HeaderBrand","Logo","HeaderCenter","HeaderEnd","Sidebar"],"mappings":"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","x_google_ignoreList":[0,1]}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { jsxs as x, jsx as l } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef as v, useMemo as o } from "react";
|
|
3
|
+
import { c as u } from "./index-D2ZczOXr.js";
|
|
4
|
+
import { useTranslation as M } from "react-i18next";
|
|
5
|
+
import { S as b } from "./star-vav0SJ9e.js";
|
|
6
|
+
const R = u(
|
|
7
|
+
[
|
|
8
|
+
"ds:inline-flex ds:items-center ds:align-middle",
|
|
9
|
+
"ds:gap-[var(--spacing-xs)]"
|
|
10
|
+
].join(" "),
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
size: {
|
|
14
|
+
sm: "",
|
|
15
|
+
md: "",
|
|
16
|
+
lg: ""
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: { size: "md" }
|
|
20
|
+
}
|
|
21
|
+
), S = u(
|
|
22
|
+
[
|
|
23
|
+
"ds:inline-flex ds:items-center ds:leading-none",
|
|
24
|
+
"ds:gap-[var(--spacing-xs)]"
|
|
25
|
+
].join(" ")
|
|
26
|
+
), F = u(
|
|
27
|
+
"ds:relative ds:inline-block ds:shrink-0 ds:leading-none",
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
size: {
|
|
31
|
+
sm: "ds:size-3.5",
|
|
32
|
+
md: "ds:size-4",
|
|
33
|
+
lg: "ds:size-5"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: { size: "md" }
|
|
37
|
+
}
|
|
38
|
+
), $ = u(
|
|
39
|
+
[
|
|
40
|
+
"ds:pointer-events-none ds:absolute ds:start-0 ds:[inset-block:0]",
|
|
41
|
+
"ds:overflow-hidden ds:text-warning"
|
|
42
|
+
].join(" "),
|
|
43
|
+
{
|
|
44
|
+
variants: {
|
|
45
|
+
fill: {
|
|
46
|
+
none: "ds:hidden",
|
|
47
|
+
half: "ds:[inline-size:50%]",
|
|
48
|
+
full: "ds:[inline-size:100%]"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
defaultVariants: { fill: "none" }
|
|
52
|
+
}
|
|
53
|
+
), I = u("ds:text-muted-foreground ds:leading-none", {
|
|
54
|
+
variants: {
|
|
55
|
+
size: {
|
|
56
|
+
sm: "type-meta",
|
|
57
|
+
md: "type-body-sm",
|
|
58
|
+
lg: "type-body"
|
|
59
|
+
},
|
|
60
|
+
underlined: {
|
|
61
|
+
true: "ds:underline ds:underline-offset-2",
|
|
62
|
+
false: ""
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
defaultVariants: { size: "md", underlined: !1 }
|
|
66
|
+
});
|
|
67
|
+
function T(t, s) {
|
|
68
|
+
const r = Math.max(0, Math.min(t, s)), n = Math.round(r * 2) / 2, e = [];
|
|
69
|
+
let a = n;
|
|
70
|
+
for (let m = 0; m < s; m += 1)
|
|
71
|
+
a >= 1 ? (e.push("full"), a -= 1) : a >= 0.5 ? (e.push("half"), a -= 0.5) : e.push("none");
|
|
72
|
+
return e;
|
|
73
|
+
}
|
|
74
|
+
const W = v(
|
|
75
|
+
({
|
|
76
|
+
value: t,
|
|
77
|
+
max: s = 5,
|
|
78
|
+
size: r = "md",
|
|
79
|
+
reviews: n,
|
|
80
|
+
underlined: e = !1,
|
|
81
|
+
decorative: a = !1,
|
|
82
|
+
className: m,
|
|
83
|
+
"aria-label": c,
|
|
84
|
+
...z
|
|
85
|
+
}, y) => {
|
|
86
|
+
const { t: d, i18n: f } = M(), i = o(
|
|
87
|
+
() => new Intl.NumberFormat(f.language, {
|
|
88
|
+
maximumFractionDigits: 1
|
|
89
|
+
}).format(t),
|
|
90
|
+
[t, f.language]
|
|
91
|
+
), g = o(
|
|
92
|
+
() => new Intl.NumberFormat(f.language).format(s),
|
|
93
|
+
[s, f.language]
|
|
94
|
+
), N = o(() => {
|
|
95
|
+
if (!a)
|
|
96
|
+
return c || (n ? d("rating.reviews", {
|
|
97
|
+
count: n.count,
|
|
98
|
+
value: i
|
|
99
|
+
}) : d("rating.label", {
|
|
100
|
+
value: i,
|
|
101
|
+
max: g
|
|
102
|
+
}));
|
|
103
|
+
}, [a, c, n, d, i, g]), V = o(() => T(t, s), [t, s]), p = o(() => {
|
|
104
|
+
if (n)
|
|
105
|
+
return n.label ? `${i} · ${n.count} ${n.label}` : d("rating.reviews", {
|
|
106
|
+
count: n.count,
|
|
107
|
+
value: i
|
|
108
|
+
});
|
|
109
|
+
}, [n, i, d]), j = a ? { "aria-hidden": !0 } : { role: "img", "aria-label": N };
|
|
110
|
+
return /* @__PURE__ */ x(
|
|
111
|
+
"span",
|
|
112
|
+
{
|
|
113
|
+
ref: y,
|
|
114
|
+
"data-component": "rating",
|
|
115
|
+
className: R({ size: r, className: m }),
|
|
116
|
+
...j,
|
|
117
|
+
...z,
|
|
118
|
+
children: [
|
|
119
|
+
/* @__PURE__ */ l(
|
|
120
|
+
"span",
|
|
121
|
+
{
|
|
122
|
+
"aria-hidden": "true",
|
|
123
|
+
className: S(),
|
|
124
|
+
"data-testid": "rating-stars",
|
|
125
|
+
children: V.map((h, k) => /* @__PURE__ */ x(
|
|
126
|
+
"span",
|
|
127
|
+
{
|
|
128
|
+
className: F({ size: r }),
|
|
129
|
+
"data-fill": h,
|
|
130
|
+
children: [
|
|
131
|
+
/* @__PURE__ */ l(
|
|
132
|
+
b,
|
|
133
|
+
{
|
|
134
|
+
"aria-hidden": "true",
|
|
135
|
+
className: "ds:size-full ds:text-muted-foreground/30",
|
|
136
|
+
strokeWidth: 1.5
|
|
137
|
+
}
|
|
138
|
+
),
|
|
139
|
+
/* @__PURE__ */ l("span", { className: $({ fill: h }), children: /* @__PURE__ */ l(
|
|
140
|
+
b,
|
|
141
|
+
{
|
|
142
|
+
"aria-hidden": "true",
|
|
143
|
+
className: "ds:size-full ds:text-warning ds:fill-current",
|
|
144
|
+
strokeWidth: 1.5
|
|
145
|
+
}
|
|
146
|
+
) })
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
k
|
|
150
|
+
))
|
|
151
|
+
}
|
|
152
|
+
),
|
|
153
|
+
p ? /* @__PURE__ */ l(
|
|
154
|
+
"span",
|
|
155
|
+
{
|
|
156
|
+
"aria-hidden": "true",
|
|
157
|
+
className: I({ size: r, underlined: e }),
|
|
158
|
+
"data-testid": "rating-summary",
|
|
159
|
+
children: p
|
|
160
|
+
}
|
|
161
|
+
) : null
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
W.displayName = "Rating";
|
|
168
|
+
export {
|
|
169
|
+
W as R
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=rating-BRD7O74e.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rating-BRD7O74e.js","sources":["../../src/components/rating/rating.tsx"],"sourcesContent":["import { forwardRef, useMemo, type HTMLAttributes } from 'react';\nimport { Star } from 'lucide-react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { useTranslation } from 'react-i18next';\n\n/* ------------------------------------------------------------------ */\n/* CVA — Rating root */\n/* */\n/* Display-only widget. No hover/focus/active states — the row is a */\n/* static average score, not a picker. Sizes shrink the star + gap + */\n/* summary text together so the row stays visually balanced. */\n/* ------------------------------------------------------------------ */\n\n// `size` is a CVA variant on `starSlotVariants` (icon dimension) and\n// `summaryVariants` (text scale) — the row gaps and the outer gap stay\n// constant at `--spacing-xs` across all sizes, since visual rhythm\n// already shifts via the star size itself.\nconst ratingVariants = cva(\n [\n 'ds:inline-flex ds:items-center ds:align-middle',\n 'ds:gap-[var(--spacing-xs)]',\n ].join(' '),\n {\n variants: {\n size: {\n sm: '',\n md: '',\n lg: '',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\nconst starRowVariants = cva(\n [\n 'ds:inline-flex ds:items-center ds:leading-none',\n 'ds:gap-[var(--spacing-xs)]',\n ].join(' '),\n);\n\nconst starSlotVariants = cva(\n 'ds:relative ds:inline-block ds:shrink-0 ds:leading-none',\n {\n variants: {\n size: {\n sm: 'ds:size-3.5',\n md: 'ds:size-4',\n lg: 'ds:size-5',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\n// Clipped overlay rule. `start-0` anchors the clip at the logical\n// start edge so half-stars fill from the leading side regardless of\n// document direction — RTL flows mirror the row, NOT the fill. The\n// three values are the only ones quantiseHalfStep can produce.\nconst fillOverlayVariants = cva(\n [\n 'ds:pointer-events-none ds:absolute ds:start-0 ds:[inset-block:0]',\n 'ds:overflow-hidden ds:text-warning',\n ].join(' '),\n {\n variants: {\n fill: {\n none: 'ds:hidden',\n half: 'ds:[inline-size:50%]',\n full: 'ds:[inline-size:100%]',\n },\n },\n defaultVariants: { fill: 'none' },\n },\n);\n\nconst summaryVariants = cva('ds:text-muted-foreground ds:leading-none', {\n variants: {\n size: {\n sm: 'type-meta',\n md: 'type-body-sm',\n lg: 'type-body',\n },\n underlined: {\n true: 'ds:underline ds:underline-offset-2',\n false: '',\n },\n },\n defaultVariants: { size: 'md', underlined: false },\n});\n\n/* ------------------------------------------------------------------ */\n/* RatingProps */\n/* ------------------------------------------------------------------ */\n\ninterface RatingProps\n extends\n Omit<HTMLAttributes<HTMLSpanElement>, 'aria-label'>,\n VariantProps<typeof ratingVariants> {\n /** Average score, 0..max. Fractional values render as half-step fills. */\n value: number;\n /** Total number of stars. Defaults to 5. */\n max?: number;\n /** Optional inline review-count summary. Plural-aware via i18next. */\n reviews?: { count: number; label?: string };\n /** Underline the reviews summary text — useful when it's a link. */\n underlined?: boolean;\n /**\n * When `true`, the rating is decorative — `aria-hidden`, no role, no\n * computed label. Use when the surrounding context already announces\n * the score (e.g. a card whose accessible name includes the value).\n */\n decorative?: boolean;\n /** Override the default i18n-derived `aria-label`. */\n 'aria-label'?: string;\n}\n\n/* ------------------------------------------------------------------ */\n/* Per-star fill calculation */\n/* */\n/* Quantise to half-steps so a 4.3 average renders as 4 + half + empty */\n/* rather than a jagged 30%-of-fifth-star tail. This matches the */\n/* visual idiom AlfaDocs users already see on Google and TripAdvisor. */\n/* ------------------------------------------------------------------ */\n\ntype Fill = 'none' | 'half' | 'full';\n\nfunction quantiseHalfStep(value: number, max: number): Fill[] {\n const clamped = Math.max(0, Math.min(value, max));\n const rounded = Math.round(clamped * 2) / 2;\n const fills: Fill[] = [];\n let remainder = rounded;\n for (let i = 0; i < max; i += 1) {\n if (remainder >= 1) {\n fills.push('full');\n remainder -= 1;\n } else if (remainder >= 0.5) {\n fills.push('half');\n remainder -= 0.5;\n } else {\n fills.push('none');\n }\n }\n return fills;\n}\n\n/* ------------------------------------------------------------------ */\n/* Rating */\n/* ------------------------------------------------------------------ */\n\nexport const Rating = forwardRef<HTMLSpanElement, RatingProps>(\n (\n {\n value,\n max = 5,\n size = 'md',\n reviews,\n underlined = false,\n decorative = false,\n className,\n 'aria-label': ariaLabelProp,\n ...props\n },\n ref,\n ) => {\n const { t, i18n } = useTranslation();\n\n // Locale-aware number formatting. Italian users see \"4,3\"; English\n // sees \"4.3\". `maximumFractionDigits: 1` matches the half-step\n // visual — no point showing 4.27 when the rendering is quantised.\n const formattedValue = useMemo(\n () =>\n new Intl.NumberFormat(i18n.language, {\n maximumFractionDigits: 1,\n }).format(value),\n [value, i18n.language],\n );\n\n const formattedMax = useMemo(\n () => new Intl.NumberFormat(i18n.language).format(max),\n [max, i18n.language],\n );\n\n // SR announcement. Three shapes:\n // 1. decorative → no label, aria-hidden\n // 2. caller override → use as-is\n // 3. reviews provided → \"{value} stars · N reviews\" (plural)\n // 4. plain → \"{value} out of {max} stars\"\n const computedLabel = useMemo(() => {\n if (decorative) return undefined;\n if (ariaLabelProp) return ariaLabelProp;\n if (reviews) {\n return t('rating.reviews', {\n count: reviews.count,\n value: formattedValue,\n });\n }\n return t('rating.label', {\n value: formattedValue,\n max: formattedMax,\n });\n }, [decorative, ariaLabelProp, reviews, t, formattedValue, formattedMax]);\n\n const fills = useMemo(() => quantiseHalfStep(value, max), [value, max]);\n\n // Visible summary text — rendered alongside the star row. When the\n // caller passes a custom `reviews.label` (e.g. localised \"opinioni\"),\n // we render it verbatim alongside the formatted value. The brief's\n // \"all visible strings via useTranslation()\" rule has a narrow\n // carve-out here: `reviews.label` is itself a localised string the\n // caller has already resolved (the booking-website ships its own\n // app-namespace strings — see src/docs/06-i18n.mdx §\"Translation\n // Key Namespacing\"). Without a custom `label`, the plural-aware\n // i18n string supplies the wording from the `ui` namespace.\n const summaryText = useMemo(() => {\n if (!reviews) return undefined;\n if (reviews.label) {\n return `${formattedValue} · ${reviews.count} ${reviews.label}`;\n }\n return t('rating.reviews', {\n count: reviews.count,\n value: formattedValue,\n });\n }, [reviews, formattedValue, t]);\n\n const rootProps = decorative\n ? { 'aria-hidden': true as const }\n : { role: 'img', 'aria-label': computedLabel };\n\n return (\n <span\n ref={ref}\n data-component=\"rating\"\n className={ratingVariants({ size, className })}\n {...rootProps}\n {...props}\n >\n {/* Star row — visual only. The accessible name lives on the */}\n {/* root span so a screen reader reads one phrase, not five */}\n {/* \"star\" calls. */}\n <span\n aria-hidden=\"true\"\n className={starRowVariants()}\n data-testid=\"rating-stars\"\n >\n {fills.map((fill, i) => (\n <span\n key={i}\n className={starSlotVariants({ size })}\n data-fill={fill}\n >\n {/* Empty layer — always present, low-opacity neutral. */}\n <Star\n aria-hidden=\"true\"\n className=\"ds:size-full ds:text-muted-foreground/30\"\n strokeWidth={1.5}\n />\n {/* Filled overlay — clipped to 50% / 100% of the star's */}\n {/* logical inline width. Three CVA values cover the */}\n {/* universe of `quantiseHalfStep` outputs. */}\n <span className={fillOverlayVariants({ fill })}>\n <Star\n aria-hidden=\"true\"\n className=\"ds:size-full ds:text-warning ds:fill-current\"\n strokeWidth={1.5}\n />\n </span>\n </span>\n ))}\n </span>\n {summaryText ? (\n <span\n aria-hidden=\"true\"\n className={summaryVariants({ size, underlined })}\n data-testid=\"rating-summary\"\n >\n {summaryText}\n </span>\n ) : null}\n </span>\n );\n },\n);\nRating.displayName = 'Rating';\n\nexport type { RatingProps };\n"],"names":["ratingVariants","cva","starRowVariants","starSlotVariants","fillOverlayVariants","summaryVariants","quantiseHalfStep","value","max","clamped","rounded","fills","remainder","i","Rating","forwardRef","size","reviews","underlined","decorative","className","ariaLabelProp","props","ref","t","i18n","useTranslation","formattedValue","useMemo","formattedMax","computedLabel","summaryText","rootProps","jsxs","jsx","fill","Star"],"mappings":"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"}
|