@alfadocs/ui-kit-debug 0.9.0 → 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/{agenda-card-DU13wUTA.js → agenda-card-CsCm7hQv.js} +2 -2
- package/dist/_chunks/{agenda-card-DU13wUTA.js.map → agenda-card-CsCm7hQv.js.map} +1 -1
- package/dist/_chunks/{agenda-tray-gQUXklO4.js → agenda-tray-B8QZ3wEp.js} +2 -2
- package/dist/_chunks/agenda-tray-B8QZ3wEp.js.map +1 -0
- package/dist/_chunks/audio-recorder-BHBonrFf.js.map +1 -1
- package/dist/_chunks/checkbox-group-Qkm3Rg1S.js.map +1 -1
- package/dist/_chunks/{combobox-DUpWoPPk.js → combobox-BHhnR3qm.js} +4 -8
- package/dist/_chunks/{combobox-DUpWoPPk.js.map → combobox-BHhnR3qm.js.map} +1 -1
- package/dist/_chunks/{file-upload-BVbfiANR.js → file-upload-DIecAfC-.js} +4 -8
- package/dist/_chunks/file-upload-DIecAfC-.js.map +1 -0
- package/dist/_chunks/freemium-paywall-BTEiVkes.js.map +1 -1
- package/dist/_chunks/{notification-card-CRIE2Cuk.js → notification-card-ejOw5g6g.js} +2 -5
- package/dist/_chunks/{notification-card-CRIE2Cuk.js.map → notification-card-ejOw5g6g.js.map} +1 -1
- package/dist/_chunks/{notification-tray-CfXNYrxv.js → notification-tray-C3dYdLAF.js} +3 -6
- package/dist/_chunks/notification-tray-C3dYdLAF.js.map +1 -0
- 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/{sparkline-D4Np8ikf.js → sparkline-B5Ms55PZ.js} +2 -2
- package/dist/_chunks/sparkline-B5Ms55PZ.js.map +1 -0
- package/dist/_chunks/star-vav0SJ9e.js +20 -0
- package/dist/_chunks/star-vav0SJ9e.js.map +1 -0
- package/dist/_chunks/{timeline-BaSULpSJ.js → timeline-rmls7dIh.js} +9 -9
- package/dist/_chunks/timeline-rmls7dIh.js.map +1 -0
- package/dist/_chunks/{transaction-chip-eVbolm0B.js → transaction-chip-z9ENE50O.js} +2 -2
- package/dist/_chunks/{transaction-chip-eVbolm0B.js.map → transaction-chip-z9ENE50O.js.map} +1 -1
- package/dist/_chunks/{use-password-requirements-B1M8lE2t.js → use-password-requirements-DbPZMfV9.js} +2 -2
- package/dist/_chunks/use-password-requirements-DbPZMfV9.js.map +1 -0
- package/dist/_chunks/warning-stack-CeRihME9.js.map +1 -1
- package/dist/agent-catalog.json +15 -1
- package/dist/components/agenda-card/index.js +1 -1
- package/dist/components/agenda-tray/agenda-tray.d.ts.map +1 -1
- package/dist/components/agenda-tray/index.js +1 -1
- package/dist/components/audio-recorder/audio-recorder.d.ts.map +1 -1
- package/dist/components/checkbox-group/checkbox-group.d.ts.map +1 -1
- package/dist/components/combobox/combobox.d.ts.map +1 -1
- package/dist/components/combobox/index.js +1 -1
- package/dist/components/file-upload/file-upload.d.ts.map +1 -1
- package/dist/components/file-upload/index.js +1 -1
- package/dist/components/freemium-paywall/freemium-paywall.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/notification-card/index.js +1 -1
- package/dist/components/notification-card/notification-card.d.ts.map +1 -1
- package/dist/components/notification-tray/index.js +1 -1
- package/dist/components/notification-tray/notification-tray.d.ts.map +1 -1
- package/dist/components/password-input/index.js +1 -1
- package/dist/components/password-input/password-input.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/components/sparkline/index.js +1 -1
- package/dist/components/timeline/index.js +1 -1
- package/dist/components/transaction-chip/index.js +1 -1
- package/dist/components/warning-stack/warning-stack.d.ts.map +1 -1
- package/dist/i18n/config.d.ts +1 -1
- package/dist/i18n/config.d.ts.map +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 +324 -322
- 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
- package/dist/_chunks/agenda-tray-gQUXklO4.js.map +0 -1
- package/dist/_chunks/file-upload-BVbfiANR.js.map +0 -1
- package/dist/_chunks/notification-tray-CfXNYrxv.js.map +0 -1
- package/dist/_chunks/sparkline-D4Np8ikf.js.map +0 -1
- package/dist/_chunks/timeline-BaSULpSJ.js.map +0 -1
- package/dist/_chunks/use-password-requirements-B1M8lE2t.js.map +0 -1
package/dist/_chunks/{notification-card-CRIE2Cuk.js.map → notification-card-ejOw5g6g.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-card-CRIE2Cuk.js","sources":["../../src/components/notification-card/notification-card.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useMemo,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { useTranslation } from 'react-i18next';\nimport { X } from 'lucide-react';\nimport { IconButton } from '../button/icon-button';\nimport { Button } from '../button/button';\nimport { Timestamp } from '../timestamp';\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\nexport interface NotificationItem {\n /** Unique identifier for the notification. */\n id: string;\n /** Notification title — rendered on the first line. */\n title: string;\n /** Notification body — rendered as plain text. */\n description?: string;\n /** Optional leading glyph — a React node (usually a Lucide icon). */\n icon?: ReactNode;\n /** Optional action URL — when present the card title is an anchor. */\n url?: string;\n /** ISO-8601 timestamp of when the notification was created. */\n createdAt: string;\n /** Whether the notification has been seen. Unread items get accent styling. */\n read?: boolean;\n}\n\nexport interface NotificationCardProps\n extends\n Omit<HTMLAttributes<HTMLDivElement>, 'onClick' | 'role' | 'title'>,\n VariantProps<typeof cardVariants> {\n /** The notification to render. */\n item: NotificationItem;\n /**\n * Layout shape.\n * - `compact` — single-line row with the whole card clickable (used by\n * NotificationTray).\n * - `dashboard` — alert-style block with explicit CTA and mark-as-read\n * controls (used by dashboards / sidebars).\n */\n variant?: 'compact' | 'dashboard';\n /** Visual density. */\n size?: 'sm' | 'md';\n /** Fires when the card title / body is activated. */\n onActivate?: (item: NotificationItem) => void;\n /** Fires when the dismiss control is activated. */\n onDismiss?: (item: NotificationItem) => void;\n /**\n * Label for the dashboard-variant CTA. Only rendered when `variant` is\n * `'dashboard'` and the item has a `url`.\n */\n ctaLabel?: string;\n /**\n * Label for the dashboard-variant mark-as-read link. Only rendered when\n * `variant` is `'dashboard'` and `onDismiss` is supplied.\n */\n dismissLabel?: string;\n /**\n * External exit-animation control. When supplied the card plays its\n * fade-out when this turns `true` and the consumer owns the subsequent\n * unmount (see NotificationTray). If omitted, the card self-manages the\n * exit on internal dismiss.\n */\n leaving?: boolean;\n}\n\n/* ------------------------------------------------------------------ */\n/* CVA */\n/* ------------------------------------------------------------------ */\n\nconst cardVariants = cva(\n [\n 'ds:relative ds:flex ds:gap-[var(--spacing-sm)]',\n 'ds:rounded-[var(--radius-sm)]',\n 'ds:transition-colors',\n // Mount: fade + slide-down so a fresh notification reads as arriving.\n // `data-leaving=true` plays the matching exit, driven by the owning\n // tray's AnimatePresence loop.\n 'ds:motion-safe:animate-in ds:motion-safe:fade-in-0 ds:motion-safe:slide-in-from-top-2 ds:motion-safe:duration-[var(--animation-duration)]',\n 'ds:data-[leaving=true]:motion-safe:animate-out ds:data-[leaving=true]:motion-safe:fade-out-0 ds:data-[leaving=true]:motion-safe:slide-out-to-top-2',\n // Pin the exit's end-state until React unmounts. Without `fill-mode-forwards`\n // tw-animate-css leaves the element at opacity:1 for one paint frame after\n // the animation ends — the \"flicker before it animates away\" bug.\n 'ds:data-[leaving=true]:motion-safe:fill-mode-forwards',\n 'ds:data-[leaving=true]:pointer-events-none',\n 'ds:motion-reduce:transition-none',\n 'ds:text-start',\n 'ds:forced-colors:border ds:forced-colors:border-[CanvasText]',\n ].join(' '),\n {\n variants: {\n variant: {\n compact: 'ds:items-start',\n dashboard:\n 'ds:flex-col ds:sm:flex-row ds:items-stretch ds:sm:items-start ds:shadow-[var(--shadow-md)] ds:bg-[color:var(--card)]',\n },\n size: {\n sm: 'ds:p-[var(--spacing-sm)]',\n md: 'ds:p-[var(--spacing-md)]',\n },\n state: {\n unread: '',\n read: 'ds:bg-transparent',\n },\n },\n compoundVariants: [\n {\n variant: 'compact',\n state: 'unread',\n class: 'ds:bg-[color:var(--accent)]/5',\n },\n {\n variant: 'compact',\n state: 'read',\n class: 'ds:hover:bg-[color:var(--muted)]/40',\n },\n {\n variant: 'dashboard',\n state: 'unread',\n class: 'ds:bg-[color:var(--accent)]/5',\n },\n ],\n defaultVariants: {\n variant: 'compact',\n size: 'sm',\n state: 'read',\n },\n },\n);\n\nconst stretchedLinkClass = [\n 'ds:focus-visible:outline-none',\n \"ds:after:content-[''] ds:after:absolute ds:after:inset-0 ds:after:rounded-[var(--radius-sm)]\",\n 'ds:after:pointer-events-auto',\n 'ds:focus-visible:after:outline-[length:var(--focus-ring-width)]',\n 'ds:focus-visible:after:outline-solid',\n 'ds:focus-visible:after:outline-[color:var(--ring)]',\n 'ds:focus-visible:after:outline-offset-[length:var(--focus-ring-offset)]',\n 'ds:forced-colors:focus-visible:after:outline-[CanvasText]',\n].join(' ');\n\n/* ------------------------------------------------------------------ */\n/* Helpers */\n/* ------------------------------------------------------------------ */\n\nexport function isSafeNotificationUrl(url: string): boolean {\n // Reject protocol-relative (//host), javascript:, data:, mailto: etc.\n // Only http(s), same-origin absolute paths (/), and hash (#) anchors pass.\n return /^(https?:\\/\\/(?!\\/)|\\/(?!\\/)|#)/.test(url);\n}\n\n/* ------------------------------------------------------------------ */\n/* Component */\n/* ------------------------------------------------------------------ */\n\nexport const NotificationCard = forwardRef<\n HTMLDivElement,\n NotificationCardProps\n>(\n (\n {\n item,\n variant = 'compact',\n size = 'sm',\n onActivate,\n onDismiss,\n ctaLabel,\n dismissLabel,\n leaving,\n className,\n ...rest\n },\n ref,\n ) => {\n const { t, i18n } = useTranslation();\n // Screen-reader time context — absolute & unambiguous rather than the\n // visible relative phrase, which drifts and reads less clearly aloud.\n const accessibleTime = useMemo(() => {\n const date = new Date(item.createdAt);\n if (Number.isNaN(date.getTime())) return '';\n return new Intl.DateTimeFormat(i18n.language, {\n dateStyle: 'medium',\n timeStyle: 'short',\n }).format(date);\n }, [item.createdAt, i18n.language]);\n\n /* Exit animation.\n * - When `leaving` is undefined (uncontrolled): the card owns the\n * fade-out — dismiss click flips `isLeaving`, waits for\n * --animation-duration, then fires onDismiss.\n * - When `leaving` is boolean (controlled, e.g. NotificationTray): the\n * parent owns the lifecycle — dismiss click fires onDismiss\n * immediately and the parent schedules unmount after the animation. */\n const isControlledLeaving = leaving !== undefined;\n const [isLeaving, setIsLeaving] = useState(false);\n const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const effectiveLeaving = isControlledLeaving ? leaving : isLeaving;\n\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const handleDismiss = useCallback(() => {\n if (!onDismiss) return;\n if (isControlledLeaving || prefersReducedMotion) {\n onDismiss(item);\n return;\n }\n setIsLeaving(true);\n if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);\n const raw =\n (typeof window !== 'undefined' &&\n window\n .getComputedStyle(document.documentElement)\n .getPropertyValue('--animation-duration')) ||\n '200ms';\n const ms = raw.trim().endsWith('ms')\n ? parseFloat(raw)\n : parseFloat(raw) * 1000;\n dismissTimerRef.current = setTimeout(\n () => onDismiss(item),\n Number.isFinite(ms) ? ms : 200,\n );\n }, [onDismiss, item, isControlledLeaving, prefersReducedMotion]);\n\n const state = item.read ? 'read' : 'unread';\n\n const unreadSuffix = item.read\n ? ''\n : t('notificationCard.unreadSuffix', ', unread');\n const ariaLabel = t('notificationCard.itemLabel', {\n title: item.title,\n time: accessibleTime,\n unreadSuffix,\n defaultValue: '{{title}}, {{time}}{{unreadSuffix}}',\n });\n\n const hasSafeUrl = !!item.url && isSafeNotificationUrl(item.url);\n\n /* ------- Title rendering (stretched link in compact mode) ------- */\n\n const titleText = (\n <span\n className={[\n 'ds:flex-1 ds:min-w-0 ds:truncate',\n item.read ? 'ds:font-normal' : 'ds:font-semibold',\n ].join(' ')}\n >\n {item.title}\n </span>\n );\n\n let titleNode: ReactNode;\n if (variant === 'compact' && hasSafeUrl && onActivate) {\n titleNode = (\n <a\n href={item.url}\n aria-label={ariaLabel}\n className={stretchedLinkClass + ' ds:contents'}\n onClick={(event) => {\n if (\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey\n )\n return;\n event.preventDefault();\n onActivate(item);\n }}\n >\n {titleText}\n </a>\n );\n } else if (variant === 'compact' && onActivate) {\n titleNode = (\n <button\n type=\"button\"\n aria-label={ariaLabel}\n className={\n stretchedLinkClass +\n ' ds:contents ds:bg-transparent ds:border-0 ds:p-0'\n }\n onClick={() => onActivate(item)}\n >\n {titleText}\n </button>\n );\n } else if (variant === 'dashboard' && hasSafeUrl && onActivate) {\n titleNode = (\n <a\n href={item.url}\n className=\"ds:text-[color:var(--foreground)] ds:no-underline ds:hover:underline ds:focus-visible:outline-[length:var(--focus-ring-width)] ds:focus-visible:outline-solid ds:focus-visible:outline-[color:var(--ring)] ds:focus-visible:outline-offset-[length:var(--focus-ring-offset)]\"\n onClick={(event) => {\n if (\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey\n )\n return;\n event.preventDefault();\n onActivate(item);\n }}\n >\n {titleText}\n </a>\n );\n } else {\n titleNode = titleText;\n }\n\n return (\n <div\n ref={ref}\n role=\"listitem\"\n data-component=\"notification-card\"\n data-component-id={item.id}\n data-read={item.read ? 'true' : 'false'}\n data-leaving={effectiveLeaving ? 'true' : undefined}\n aria-hidden={effectiveLeaving || undefined}\n className={cardVariants({ variant, size, state, className })}\n {...rest}\n >\n {item.icon ? (\n <span\n aria-hidden=\"true\"\n className=\"ds:inline-flex ds:shrink-0 ds:items-center ds:justify-center ds:size-8 ds:rounded-[var(--radius-full)] ds:bg-[color:var(--accent)]/10 ds:text-[color:var(--accent)] ds:[&>svg]:size-4\"\n >\n {item.icon}\n </span>\n ) : null}\n\n <div className=\"ds:flex-1 ds:min-w-0 ds:flex ds:flex-col ds:gap-[var(--spacing-sm)]\">\n <div className=\"ds:flex ds:items-center ds:gap-[var(--spacing-xs)]\">\n {!item.read ? (\n <span\n aria-hidden=\"true\"\n className=\"ds:inline-block ds:size-1.5 ds:shrink-0 ds:rounded-[var(--radius-full)] ds:bg-[color:var(--primary)] ds:forced-colors:bg-[Highlight]\"\n />\n ) : null}\n {variant === 'dashboard' ? (\n // `h3` keeps the dashboard heading ladder continuous:\n // h1 (page) → h2 (WarningStack + NotificationsPanel) → h3 (notification title).\n <h3 className=\"ds:m-0 ds:flex-1 ds:min-w-0 type-title-item\">\n {titleNode}\n </h3>\n ) : (\n <span className=\"ds:flex-1 ds:min-w-0 type-title-item ds:text-[color:var(--foreground)]\">\n {titleNode}\n </span>\n )}\n </div>\n\n {item.description ? (\n <p className=\"ds:m-0 type-body-sm ds:text-[color:var(--muted-foreground)] ds:[overflow:hidden] ds:[display:-webkit-box] ds:[-webkit-box-orient:vertical] ds:[-webkit-line-clamp:2] ds:break-words\">\n {item.description}\n </p>\n ) : null}\n\n <Timestamp\n value={item.createdAt}\n shape=\"chip\"\n size=\"sm\"\n relativeWindow={12 * 60 * 60 * 1000}\n />\n\n {variant === 'dashboard' && (ctaLabel || dismissLabel) ? (\n <div className=\"ds:flex ds:flex-wrap ds:items-center ds:gap-[var(--spacing-sm)]\">\n {ctaLabel && hasSafeUrl ? (\n <Button\n intent=\"outline\"\n size=\"sm\"\n asChild\n className=\"ds:relative ds:z-[1]\"\n >\n <a\n href={item.url}\n onClick={(event) => {\n if (\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey\n )\n return;\n event.preventDefault();\n onActivate?.(item);\n }}\n >\n {ctaLabel}\n </a>\n </Button>\n ) : null}\n {dismissLabel && onDismiss ? (\n <Button\n intent=\"link\"\n size=\"sm\"\n onClick={handleDismiss}\n className=\"ds:relative ds:z-[1]\"\n >\n {dismissLabel}\n </Button>\n ) : null}\n </div>\n ) : null}\n </div>\n\n {variant === 'compact' && onDismiss ? (\n <span className=\"ds:relative ds:z-[1] ds:opacity-0 ds:group-hover:opacity-100 ds:group-focus-within:opacity-100 ds:transition-opacity ds:motion-reduce:transition-none\">\n <IconButton\n icon={<X />}\n intent=\"ghost\"\n size=\"sm\"\n aria-label={t(\n 'notificationCard.dismiss',\n 'Dismiss notification',\n )}\n onClick={handleDismiss}\n />\n </span>\n ) : null}\n </div>\n );\n },\n);\n\nNotificationCard.displayName = 'NotificationCard';\n"],"names":["cardVariants","cva","stretchedLinkClass","isSafeNotificationUrl","url","NotificationCard","forwardRef","item","variant","size","onActivate","onDismiss","ctaLabel","dismissLabel","leaving","className","rest","ref","t","i18n","useTranslation","accessibleTime","useMemo","date","isControlledLeaving","isLeaving","setIsLeaving","useState","dismissTimerRef","useRef","effectiveLeaving","prefersReducedMotion","handleDismiss","useCallback","raw","ms","state","unreadSuffix","ariaLabel","hasSafeUrl","titleText","jsx","titleNode","event","jsxs","Timestamp","Button","IconButton","X"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"notification-card-ejOw5g6g.js","sources":["../../src/components/notification-card/notification-card.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useMemo,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { useTranslation } from 'react-i18next';\nimport { X } from 'lucide-react';\nimport { IconButton } from '../button/icon-button';\nimport { Button } from '../button/button';\nimport { Timestamp } from '../timestamp';\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\nexport interface NotificationItem {\n /** Unique identifier for the notification. */\n id: string;\n /** Notification title — rendered on the first line. */\n title: string;\n /** Notification body — rendered as plain text. */\n description?: string;\n /** Optional leading glyph — a React node (usually a Lucide icon). */\n icon?: ReactNode;\n /** Optional action URL — when present the card title is an anchor. */\n url?: string;\n /** ISO-8601 timestamp of when the notification was created. */\n createdAt: string;\n /** Whether the notification has been seen. Unread items get accent styling. */\n read?: boolean;\n}\n\nexport interface NotificationCardProps\n extends\n Omit<HTMLAttributes<HTMLDivElement>, 'onClick' | 'role' | 'title'>,\n VariantProps<typeof cardVariants> {\n /** The notification to render. */\n item: NotificationItem;\n /**\n * Layout shape.\n * - `compact` — single-line row with the whole card clickable (used by\n * NotificationTray).\n * - `dashboard` — alert-style block with explicit CTA and mark-as-read\n * controls (used by dashboards / sidebars).\n */\n variant?: 'compact' | 'dashboard';\n /** Visual density. */\n size?: 'sm' | 'md';\n /** Fires when the card title / body is activated. */\n onActivate?: (item: NotificationItem) => void;\n /** Fires when the dismiss control is activated. */\n onDismiss?: (item: NotificationItem) => void;\n /**\n * Label for the dashboard-variant CTA. Only rendered when `variant` is\n * `'dashboard'` and the item has a `url`.\n */\n ctaLabel?: string;\n /**\n * Label for the dashboard-variant mark-as-read link. Only rendered when\n * `variant` is `'dashboard'` and `onDismiss` is supplied.\n */\n dismissLabel?: string;\n /**\n * External exit-animation control. When supplied the card plays its\n * fade-out when this turns `true` and the consumer owns the subsequent\n * unmount (see NotificationTray). If omitted, the card self-manages the\n * exit on internal dismiss.\n */\n leaving?: boolean;\n}\n\n/* ------------------------------------------------------------------ */\n/* CVA */\n/* ------------------------------------------------------------------ */\n\nconst cardVariants = cva(\n [\n 'ds:relative ds:flex ds:gap-[var(--spacing-sm)]',\n 'ds:rounded-[var(--radius-sm)]',\n 'ds:transition-colors',\n // Mount: fade + slide-down so a fresh notification reads as arriving.\n // `data-leaving=true` plays the matching exit, driven by the owning\n // tray's AnimatePresence loop.\n 'ds:motion-safe:animate-in ds:motion-safe:fade-in-0 ds:motion-safe:slide-in-from-top-2 ds:motion-safe:duration-[var(--animation-duration)]',\n 'ds:data-[leaving=true]:motion-safe:animate-out ds:data-[leaving=true]:motion-safe:fade-out-0 ds:data-[leaving=true]:motion-safe:slide-out-to-top-2',\n // Pin the exit's end-state until React unmounts. Without `fill-mode-forwards`\n // tw-animate-css leaves the element at opacity:1 for one paint frame after\n // the animation ends — the \"flicker before it animates away\" bug.\n 'ds:data-[leaving=true]:motion-safe:fill-mode-forwards',\n 'ds:data-[leaving=true]:pointer-events-none',\n 'ds:motion-reduce:transition-none',\n 'ds:text-start',\n 'ds:forced-colors:border ds:forced-colors:border-[CanvasText]',\n ].join(' '),\n {\n variants: {\n variant: {\n compact: 'ds:items-start',\n dashboard:\n 'ds:flex-col ds:sm:flex-row ds:items-stretch ds:sm:items-start ds:shadow-[var(--shadow-md)] ds:bg-[color:var(--card)]',\n },\n size: {\n sm: 'ds:p-[var(--spacing-sm)]',\n md: 'ds:p-[var(--spacing-md)]',\n },\n state: {\n unread: '',\n read: 'ds:bg-transparent',\n },\n },\n compoundVariants: [\n {\n variant: 'compact',\n state: 'unread',\n class: 'ds:bg-[color:var(--accent)]/5',\n },\n {\n variant: 'compact',\n state: 'read',\n class: 'ds:hover:bg-[color:var(--muted)]/40',\n },\n {\n variant: 'dashboard',\n state: 'unread',\n class: 'ds:bg-[color:var(--accent)]/5',\n },\n ],\n defaultVariants: {\n variant: 'compact',\n size: 'sm',\n state: 'read',\n },\n },\n);\n\nconst stretchedLinkClass = [\n 'ds:focus-visible:outline-none',\n \"ds:after:content-[''] ds:after:absolute ds:after:inset-0 ds:after:rounded-[var(--radius-sm)]\",\n 'ds:after:pointer-events-auto',\n 'ds:focus-visible:after:outline-[length:var(--focus-ring-width)]',\n 'ds:focus-visible:after:outline-solid',\n 'ds:focus-visible:after:outline-[color:var(--ring)]',\n 'ds:focus-visible:after:outline-offset-[length:var(--focus-ring-offset)]',\n 'ds:forced-colors:focus-visible:after:outline-[CanvasText]',\n].join(' ');\n\n/* ------------------------------------------------------------------ */\n/* Helpers */\n/* ------------------------------------------------------------------ */\n\nexport function isSafeNotificationUrl(url: string): boolean {\n // Reject protocol-relative (//host), javascript:, data:, mailto: etc.\n // Only http(s), same-origin absolute paths (/), and hash (#) anchors pass.\n return /^(https?:\\/\\/(?!\\/)|\\/(?!\\/)|#)/.test(url);\n}\n\n/* ------------------------------------------------------------------ */\n/* Component */\n/* ------------------------------------------------------------------ */\n\nexport const NotificationCard = forwardRef<\n HTMLDivElement,\n NotificationCardProps\n>(\n (\n {\n item,\n variant = 'compact',\n size = 'sm',\n onActivate,\n onDismiss,\n ctaLabel,\n dismissLabel,\n leaving,\n className,\n ...rest\n },\n ref,\n ) => {\n const { t, i18n } = useTranslation();\n // Screen-reader time context — absolute & unambiguous rather than the\n // visible relative phrase, which drifts and reads less clearly aloud.\n const accessibleTime = useMemo(() => {\n const date = new Date(item.createdAt);\n if (Number.isNaN(date.getTime())) return '';\n return new Intl.DateTimeFormat(i18n.language, {\n dateStyle: 'medium',\n timeStyle: 'short',\n }).format(date);\n }, [item.createdAt, i18n.language]);\n\n /* Exit animation.\n * - When `leaving` is undefined (uncontrolled): the card owns the\n * fade-out — dismiss click flips `isLeaving`, waits for\n * --animation-duration, then fires onDismiss.\n * - When `leaving` is boolean (controlled, e.g. NotificationTray): the\n * parent owns the lifecycle — dismiss click fires onDismiss\n * immediately and the parent schedules unmount after the animation. */\n const isControlledLeaving = leaving !== undefined;\n const [isLeaving, setIsLeaving] = useState(false);\n const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const effectiveLeaving = isControlledLeaving ? leaving : isLeaving;\n\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n const handleDismiss = useCallback(() => {\n if (!onDismiss) return;\n if (isControlledLeaving || prefersReducedMotion) {\n onDismiss(item);\n return;\n }\n setIsLeaving(true);\n if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);\n const raw =\n (typeof window !== 'undefined' &&\n window\n .getComputedStyle(document.documentElement)\n .getPropertyValue('--animation-duration')) ||\n '200ms';\n const ms = raw.trim().endsWith('ms')\n ? parseFloat(raw)\n : parseFloat(raw) * 1000;\n dismissTimerRef.current = setTimeout(\n () => onDismiss(item),\n Number.isFinite(ms) ? ms : 200,\n );\n }, [onDismiss, item, isControlledLeaving, prefersReducedMotion]);\n\n const state = item.read ? 'read' : 'unread';\n\n const unreadSuffix = item.read\n ? ''\n : t('notificationCard.unreadSuffix', ', unread');\n const ariaLabel = t('notificationCard.itemLabel', {\n title: item.title,\n time: accessibleTime,\n unreadSuffix,\n defaultValue: '{{title}}, {{time}}{{unreadSuffix}}',\n });\n\n const hasSafeUrl = !!item.url && isSafeNotificationUrl(item.url);\n\n /* ------- Title rendering (stretched link in compact mode) ------- */\n\n const titleText = (\n <span\n className={[\n 'ds:flex-1 ds:min-w-0 ds:truncate',\n item.read ? 'ds:font-normal' : 'ds:font-semibold',\n ].join(' ')}\n >\n {item.title}\n </span>\n );\n\n let titleNode: ReactNode;\n if (variant === 'compact' && hasSafeUrl && onActivate) {\n titleNode = (\n <a\n href={item.url}\n aria-label={ariaLabel}\n className={stretchedLinkClass + ' ds:contents'}\n onClick={(event) => {\n if (\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey\n )\n return;\n event.preventDefault();\n onActivate(item);\n }}\n >\n {titleText}\n </a>\n );\n } else if (variant === 'compact' && onActivate) {\n titleNode = (\n <button\n type=\"button\"\n aria-label={ariaLabel}\n className={\n stretchedLinkClass +\n ' ds:contents ds:bg-transparent ds:border-0 ds:p-0'\n }\n onClick={() => onActivate(item)}\n >\n {titleText}\n </button>\n );\n } else if (variant === 'dashboard' && hasSafeUrl && onActivate) {\n titleNode = (\n <a\n href={item.url}\n className=\"ds:text-[color:var(--foreground)] ds:no-underline ds:hover:underline ds:focus-visible:outline-[length:var(--focus-ring-width)] ds:focus-visible:outline-solid ds:focus-visible:outline-[color:var(--ring)] ds:focus-visible:outline-offset-[length:var(--focus-ring-offset)]\"\n onClick={(event) => {\n if (\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey\n )\n return;\n event.preventDefault();\n onActivate(item);\n }}\n >\n {titleText}\n </a>\n );\n } else {\n titleNode = titleText;\n }\n\n return (\n <div\n ref={ref}\n role=\"listitem\"\n data-component=\"notification-card\"\n data-component-id={item.id}\n data-read={item.read ? 'true' : 'false'}\n data-leaving={effectiveLeaving ? 'true' : undefined}\n aria-hidden={effectiveLeaving || undefined}\n className={cardVariants({ variant, size, state, className })}\n {...rest}\n >\n {item.icon ? (\n <span\n aria-hidden=\"true\"\n className=\"ds:inline-flex ds:shrink-0 ds:items-center ds:justify-center ds:size-8 ds:rounded-[var(--radius-full)] ds:bg-[color:var(--accent)]/10 ds:text-[color:var(--accent)] ds:[&>svg]:size-4\"\n >\n {item.icon}\n </span>\n ) : null}\n\n <div className=\"ds:flex-1 ds:min-w-0 ds:flex ds:flex-col ds:gap-[var(--spacing-sm)]\">\n <div className=\"ds:flex ds:items-center ds:gap-[var(--spacing-xs)]\">\n {!item.read ? (\n <span\n aria-hidden=\"true\"\n className=\"ds:inline-block ds:size-1.5 ds:shrink-0 ds:rounded-[var(--radius-full)] ds:bg-[color:var(--primary)] ds:forced-colors:bg-[Highlight]\"\n />\n ) : null}\n {variant === 'dashboard' ? (\n // `h3` keeps the dashboard heading ladder continuous:\n // h1 (page) → h2 (WarningStack + NotificationsPanel) → h3 (notification title).\n <h3 className=\"ds:m-0 ds:flex-1 ds:min-w-0 type-title-item\">\n {titleNode}\n </h3>\n ) : (\n <span className=\"ds:flex-1 ds:min-w-0 type-title-item ds:text-[color:var(--foreground)]\">\n {titleNode}\n </span>\n )}\n </div>\n\n {item.description ? (\n <p className=\"ds:m-0 type-body-sm ds:text-[color:var(--muted-foreground)] ds:[overflow:hidden] ds:[display:-webkit-box] ds:[-webkit-box-orient:vertical] ds:[-webkit-line-clamp:2] ds:break-words\">\n {item.description}\n </p>\n ) : null}\n\n <Timestamp\n value={item.createdAt}\n shape=\"chip\"\n size=\"sm\"\n relativeWindow={12 * 60 * 60 * 1000}\n />\n\n {variant === 'dashboard' && (ctaLabel || dismissLabel) ? (\n <div className=\"ds:flex ds:flex-wrap ds:items-center ds:gap-[var(--spacing-sm)]\">\n {ctaLabel && hasSafeUrl ? (\n <Button\n intent=\"outline\"\n size=\"sm\"\n asChild\n className=\"ds:relative ds:z-[1]\"\n >\n <a\n href={item.url}\n onClick={(event) => {\n if (\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey\n )\n return;\n event.preventDefault();\n onActivate?.(item);\n }}\n >\n {ctaLabel}\n </a>\n </Button>\n ) : null}\n {dismissLabel && onDismiss ? (\n <Button\n intent=\"link\"\n size=\"sm\"\n onClick={handleDismiss}\n className=\"ds:relative ds:z-[1]\"\n >\n {dismissLabel}\n </Button>\n ) : null}\n </div>\n ) : null}\n </div>\n\n {variant === 'compact' && onDismiss ? (\n <span className=\"ds:relative ds:z-[1] ds:opacity-0 ds:group-hover:opacity-100 ds:group-focus-within:opacity-100 ds:transition-opacity ds:motion-reduce:transition-none\">\n <IconButton\n icon={<X />}\n intent=\"ghost\"\n size=\"sm\"\n aria-label={t('notificationCard.dismiss', 'Dismiss notification')}\n onClick={handleDismiss}\n />\n </span>\n ) : null}\n </div>\n );\n },\n);\n\nNotificationCard.displayName = 'NotificationCard';\n"],"names":["cardVariants","cva","stretchedLinkClass","isSafeNotificationUrl","url","NotificationCard","forwardRef","item","variant","size","onActivate","onDismiss","ctaLabel","dismissLabel","leaving","className","rest","ref","t","i18n","useTranslation","accessibleTime","useMemo","date","isControlledLeaving","isLeaving","setIsLeaving","useState","dismissTimerRef","useRef","effectiveLeaving","prefersReducedMotion","handleDismiss","useCallback","raw","ms","state","unreadSuffix","ariaLabel","hasSafeUrl","titleText","jsx","titleNode","event","jsxs","Timestamp","Button","IconButton","X"],"mappings":"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"}
|
|
@@ -7,7 +7,7 @@ import { I as pe } from "./icon-button-C4CGcYuz.js";
|
|
|
7
7
|
import { B as W } from "./button-DD_0Xdmr.js";
|
|
8
8
|
import { S as Y } from "./separator-CYU_bGFn.js";
|
|
9
9
|
import { S as N } from "./skeleton-CZbwyJAA.js";
|
|
10
|
-
import { N as ue } from "./notification-card-
|
|
10
|
+
import { N as ue } from "./notification-card-ejOw5g6g.js";
|
|
11
11
|
import { u as me } from "./registry-C9nwlNyL.js";
|
|
12
12
|
import { c as ge } from "./createLucideIcon-CrFbzy84.js";
|
|
13
13
|
/**
|
|
@@ -299,10 +299,7 @@ const we = de(
|
|
|
299
299
|
const h = b ?? 0, ae = h > 0 ? r("notificationTray.triggerLabel", {
|
|
300
300
|
count: h,
|
|
301
301
|
defaultValue: "Notifications, {{count}} unread"
|
|
302
|
-
}) : r(
|
|
303
|
-
"notificationTray.triggerLabelNone",
|
|
304
|
-
"Notifications, none unread"
|
|
305
|
-
), ne = be(
|
|
302
|
+
}) : r("notificationTray.triggerLabelNone", "Notifications, none unread"), ne = be(
|
|
306
303
|
h,
|
|
307
304
|
r("notificationTray.badgeCountOverflow", "99+")
|
|
308
305
|
), se = xe({ size: T }), l = f(a);
|
|
@@ -445,4 +442,4 @@ export {
|
|
|
445
442
|
we as N,
|
|
446
443
|
ye as n
|
|
447
444
|
};
|
|
448
|
-
//# sourceMappingURL=notification-tray-
|
|
445
|
+
//# sourceMappingURL=notification-tray-C3dYdLAF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-tray-C3dYdLAF.js","sources":["../../node_modules/lucide-react/dist/esm/icons/bell.js","../../src/components/notification-tray/notification-tray.agent.ts","../../src/components/notification-tray/notification-tray.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: \"M10.268 21a2 2 0 0 0 3.464 0\", key: \"vwvbt9\" }],\n [\n \"path\",\n {\n d: \"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\",\n key: \"11g9vi\"\n }\n ]\n];\nconst Bell = createLucideIcon(\"bell\", __iconNode);\n\nexport { __iconNode, Bell as default };\n//# sourceMappingURL=bell.js.map\n","/* -------------------------------------------------------------------- */\n/* Agent adapter — NotificationTray. */\n/* */\n/* Tray-level surface: an agent can open / close the panel, read the */\n/* current items (id + read flag only — never PHI), and route per-item */\n/* operations (dismiss, mark_read, select) through the curated handle. */\n/* All actions are routed through the imperative handle; the adapter */\n/* never touches the DOM directly. */\n/* -------------------------------------------------------------------- */\n\nimport type { AgentAdapter } from '../../agent/types';\nimport type { NotificationTrayHandle } from './notification-tray';\n\nexport const notificationTrayAgent: AgentAdapter<NotificationTrayHandle> = {\n id: 'notification-tray',\n capabilities: ['open', 'close', 'select_single', 'dismiss'],\n state: {\n items: {\n type: 'Array<{ id: string; read: boolean }>',\n descriptionKey: 'ui.agent.notificationTray.state.items',\n description:\n 'Currently-displayed notifications. Ids + read flag only — no PHI.',\n read: (handle) => handle.getItems(),\n },\n unreadCount: {\n type: 'number',\n descriptionKey: 'ui.agent.notificationTray.state.unreadCount',\n description: 'Badge unread count surfaced by the host.',\n read: (handle) => handle.getUnreadCount(),\n },\n isOpen: {\n type: 'boolean',\n descriptionKey: 'ui.agent.notificationTray.state.isOpen',\n description: 'Whether the dropdown panel is currently open.',\n read: (handle) => handle.isOpen(),\n },\n },\n actions: {\n open: {\n safety: 'read',\n descriptionKey: 'ui.agent.notificationTray.actions.open',\n description: 'Open the notification panel.',\n invoke: (handle) => {\n handle.open();\n },\n },\n close: {\n safety: 'read',\n descriptionKey: 'ui.agent.notificationTray.actions.close',\n description: 'Close the notification panel.',\n invoke: (handle) => {\n handle.close();\n },\n },\n select_item: {\n safety: 'read',\n argsType: '{ id: string }',\n descriptionKey: 'ui.agent.notificationTray.actions.selectItem',\n description: 'Activate the notification with the given id.',\n invoke: (handle, args: { id: string }) => {\n handle.selectItem(args.id);\n },\n },\n mark_read: {\n safety: 'write',\n argsType: '{ id: string }',\n descriptionKey: 'ui.agent.notificationTray.actions.markRead',\n description: 'Request the host to mark a notification as read.',\n invoke: (handle, args: { id: string }) => {\n handle.markRead(args.id);\n },\n },\n dismiss: {\n safety: 'destructive',\n argsType: '{ id: string }',\n descriptionKey: 'ui.agent.notificationTray.actions.dismiss',\n description:\n 'Request the host to dismiss the notification with the given id.',\n invoke: (handle, args: { id: string }) => {\n handle.dismiss(args.id);\n },\n },\n },\n domHooks: {\n root: {\n attr: 'data-component',\n value: 'notification-tray',\n description: 'Marks the NotificationTray wrapper.',\n },\n instanceId: {\n attr: 'data-component-id',\n sourceProp: 'id',\n description:\n 'Sourced from the id prop. Required to address a specific tray from the agent.',\n },\n item: {\n attr: 'data-notification-id',\n description:\n 'Stable notification id emitted on each rendered NotificationCard inside the tray.',\n },\n },\n};\n","import {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type HTMLAttributes,\n} from 'react';\nimport * as RadixPopover from '@radix-ui/react-popover';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { useTranslation } from 'react-i18next';\nimport { Bell } from 'lucide-react';\nimport { IconButton } from '../button/icon-button';\nimport { Button } from '../button/button';\nimport { Separator } from '../separator/separator';\nimport { Skeleton } from '../skeleton/skeleton';\nimport {\n NotificationCard,\n type NotificationItem,\n} from '../notification-card/notification-card';\nimport { useAgentRegistration } from '../../agent/registry';\nimport { notificationTrayAgent } from './notification-tray.agent';\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\n/**\n * Curated imperative handle for NotificationTray. Exposed as the\n * forwardRef target so a future agent / MCP UI bridge can drive the\n * tray without touching the DOM. See `notification-tray.agent.ts`.\n */\nexport interface NotificationTrayHandle {\n open: () => void;\n close: () => void;\n isOpen: () => boolean;\n getItems: () => Array<{ id: string; read: boolean }>;\n getUnreadCount: () => number;\n selectItem: (id: string) => void;\n markRead: (id: string) => void;\n dismiss: (id: string) => void;\n}\n\nexport interface NotificationTrayProps\n extends\n Omit<\n HTMLAttributes<HTMLDivElement>,\n 'onClick' | 'children' | 'role' | 'id'\n >,\n VariantProps<typeof notificationTrayVariants> {\n /**\n * Stable instance id. Surfaced on the root as `data-component-id` so\n * an agent / MCP UI bridge can address this specific tray.\n */\n id?: string;\n /** Fires when the host should mark a single notification as read (used by agent integration). */\n onMarkRead?: (item: NotificationItem) => void;\n /** Notifications rendered in the dropdown panel. */\n items: NotificationItem[];\n /** Number of unseen notifications. Drives the badge count. */\n unseenCount?: number;\n /** Controlled open state. */\n open?: boolean;\n /** Fires when the open state changes. */\n onOpenChange?: (open: boolean) => void;\n /** Fires when the user activates an item (click or Enter/Space). */\n onItemClick?: (item: NotificationItem) => void;\n /** Fires when the user dismisses a notification. */\n onDismiss?: (item: NotificationItem) => void;\n /** Fires when the user clicks the header \"Mark all as read\" action. */\n onMarkAllRead?: () => void;\n /** When true the panel shows skeleton placeholders. */\n loading?: boolean;\n /** Advisory item count before the list scrolls. Consumers override the cap\n * via the `--notification-tray-max-block-size` custom property. */\n maxVisible?: number;\n /** When set renders a footer link that invokes `onViewAll`. */\n viewAllLabel?: string;\n /** Fires when the footer \"View all\" link is clicked. */\n onViewAll?: () => void;\n /** Panel alignment relative to the trigger. Default `'end'`. */\n align?: 'start' | 'center' | 'end';\n /** Side of the trigger to open on. Default `'bottom'`. */\n side?: 'top' | 'bottom';\n}\n\n/* ------------------------------------------------------------------ */\n/* CVA */\n/* ------------------------------------------------------------------ */\n\nconst notificationTrayVariants = cva(\n 'ds:relative ds:inline-flex ds:items-center',\n {\n variants: {\n size: {\n sm: '',\n md: '',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n },\n);\n\nconst panelVariants = cva(\n [\n 'ds:flex ds:flex-col',\n 'ds:bg-[var(--popover)] ds:text-[var(--popover-foreground)]',\n // Border kept (transparent) so the forced-colors override below can\n // repaint a visible edge in HCM. In normal modes the soft drop-shadow\n // carries the edge cue.\n 'ds:border ds:border-transparent',\n 'ds:rounded-[var(--radius-md)]',\n 'ds:shadow-[var(--shadow-lg)]',\n 'ds:z-[var(--z-popover)]',\n // Width: fluid below the design cap so narrow viewports keep a visible\n // gutter on *both* sides of the panel (shadow included). The legacy\n // `min-w` approach clamped at 20rem, which flush-mounted the panel\n // against the inline-start viewport edge on phones.\n 'ds:w-[min(24rem,calc(100vw-2*var(--spacing-md)))]',\n // Height: below the `sm` breakpoint the panel takes a near-full-screen\n // sheet shape so the user gets room to triage messages / notifications\n // without scrolling a tiny popover. `6rem` reserves space for the\n // Header (≈4rem) plus the sideOffset + collisionPadding (≈2rem).\n // Above `sm` it returns to `h-auto` so the list's own\n // `--*-tray-max-block-size` cap governs the height.\n 'ds:h-[calc(100dvh-6rem)] ds:sm:h-auto',\n 'ds:data-[state=open]:animate-in ds:data-[state=closed]:animate-out',\n 'ds:data-[state=closed]:fade-out-0 ds:data-[state=open]:fade-in-0',\n 'ds:data-[state=closed]:zoom-out-95 ds:data-[state=open]:zoom-in-95',\n 'ds:motion-reduce:transition-none ds:motion-reduce:animate-none',\n 'ds:focus-visible:outline-none',\n 'ds:forced-colors:border-[CanvasText]',\n ].join(' '),\n {\n variants: {\n size: {\n sm: 'ds:p-[var(--spacing-xs)]',\n md: 'ds:p-[var(--spacing-sm)]',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n },\n);\n\nconst badgeVariants = cva(\n [\n // Anchored to the top-end corner of the trigger, nudged outward so\n // the bell icon isn't covered.\n 'ds:absolute ds:-top-[var(--spacing-xs)] ds:-end-[var(--spacing-xs)]',\n 'ds:inline-flex ds:items-center ds:justify-center',\n // Circle at 1-2 digits; widens into a pill only for \"99+\".\n 'ds:min-w-[calc(var(--spacing-md)+var(--spacing-xs))] ds:h-[calc(var(--spacing-md)+var(--spacing-xs))] ds:ps-[calc(var(--spacing-xs)/1.5)] ds:pe-[calc(var(--spacing-xs)/1.5)]',\n 'ds:rounded-[var(--radius-full)]',\n 'ds:bg-[color:var(--destructive)] ds:text-[color:var(--destructive-foreground)]',\n 'ds:text-[length:var(--font-size-xs)] ds:font-semibold ds:leading-none',\n 'ds:pointer-events-none ds:select-none',\n 'ds:forced-colors:outline ds:forced-colors:outline-1 ds:forced-colors:outline-[CanvasText]',\n ].join(' '),\n);\n\n/* ------------------------------------------------------------------ */\n/* Helpers */\n/* ------------------------------------------------------------------ */\n\nfunction formatBadgeCount(count: number, overflowLabel: string): string {\n return count > 99 ? overflowLabel : String(count);\n}\n\nfunction SkeletonRow() {\n return (\n <div\n aria-hidden=\"true\"\n className=\"ds:flex ds:items-start ds:gap-[var(--spacing-sm)] ds:p-[var(--spacing-sm)]\"\n >\n <Skeleton variant=\"circular\" size=\"sm\" />\n <div className=\"ds:flex-1 ds:flex ds:flex-col ds:gap-[var(--spacing-xs)]\">\n <Skeleton variant=\"text\" size=\"md\" width=\"70%\" />\n <Skeleton variant=\"text\" size=\"sm\" width=\"90%\" />\n <Skeleton variant=\"text\" size=\"sm\" width=\"30%\" />\n </div>\n </div>\n );\n}\n\nfunction EmptyPanel() {\n const { t } = useTranslation();\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n className=\"ds:flex ds:flex-col ds:items-center ds:justify-center ds:gap-[var(--spacing-xs)] ds:p-[var(--spacing-lg)] ds:text-center\"\n >\n <span\n aria-hidden=\"true\"\n className=\"ds:inline-flex ds:items-center ds:justify-center ds:size-10 ds:rounded-[var(--radius-full)] ds:bg-[color:var(--muted)] ds:text-[color:var(--muted-foreground)] ds:[&>svg]:size-5\"\n >\n <Bell />\n </span>\n <p className=\"type-title-item ds:text-[color:var(--foreground)]\">\n {t('notificationTray.noNotifications', 'No notifications')}\n </p>\n <p className=\"type-meta ds:text-[color:var(--muted-foreground)]\">\n {t(\n 'notificationTray.noNotificationsDescription',\n \"You're all caught up.\",\n )}\n </p>\n </div>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* Root */\n/* ------------------------------------------------------------------ */\n\nexport const NotificationTray = forwardRef<\n HTMLDivElement,\n NotificationTrayProps\n>(\n (\n {\n id,\n items,\n unseenCount,\n open,\n onOpenChange,\n onItemClick,\n onDismiss,\n onMarkRead,\n onMarkAllRead,\n loading = false,\n maxVisible,\n viewAllLabel,\n onViewAll,\n align = 'end',\n side = 'bottom',\n size = 'md',\n className,\n ...rest\n },\n ref,\n ) => {\n const { t } = useTranslation();\n\n const isControlled = open !== undefined;\n const [internalOpen, setInternalOpen] = useState(false);\n const isOpen = isControlled ? open : internalOpen;\n\n const handleOpenChange = useCallback(\n (next: boolean) => {\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [isControlled, onOpenChange],\n );\n\n /* Exit-animation list (AnimatePresence-style).\n * `displayed` holds an entry per card that's currently mounted, even\n * briefly after the consumer removes an item from `items`. When the\n * consumer drops an id from `items`, we flip that entry to\n * `leaving: true` for one animation cycle, then prune it. */\n type Displayed = { item: NotificationItem; leaving: boolean };\n const [displayed, setDisplayed] = useState<Displayed[]>(() =>\n items.map((item) => ({ item, leaving: false })),\n );\n\n const animationDurationMs = useCallback((): number => {\n if (typeof window === 'undefined') return 200;\n const raw = window\n .getComputedStyle(document.documentElement)\n .getPropertyValue('--animation-duration');\n if (!raw) return 200;\n const trimmed = raw.trim();\n const n = parseFloat(trimmed);\n if (!Number.isFinite(n)) return 200;\n return trimmed.endsWith('ms') ? n : n * 1000;\n }, []);\n\n // `useLayoutEffect` so the `leaving=true` flip applies in the same paint\n // as the consumer's `items` update. With `useEffect` the browser paints\n // once with the dismissed card still at `leaving=false` (no animation)\n // before the flag flips — a visible lag before the exit animation starts.\n useLayoutEffect(() => {\n setDisplayed((prev) => {\n const nextIds = new Set(items.map((i) => i.id));\n const prevIds = new Set(prev.map((d) => d.item.id));\n\n const merged: Displayed[] = prev.map((d) => {\n if (nextIds.has(d.item.id)) {\n const fresh = items.find((i) => i.id === d.item.id);\n return fresh\n ? { item: fresh, leaving: false }\n : { ...d, leaving: true };\n }\n return { ...d, leaving: true };\n });\n\n for (const i of items) {\n if (!prevIds.has(i.id)) merged.push({ item: i, leaving: false });\n }\n return merged;\n });\n }, [items]);\n\n useEffect(() => {\n if (!displayed.some((d) => d.leaving)) return;\n const ms = animationDurationMs();\n const timer = setTimeout(() => {\n setDisplayed((prev) => prev.filter((d) => !d.leaving));\n }, ms);\n return () => clearTimeout(timer);\n }, [displayed, animationDurationMs]);\n\n /* Polite live region — announces when a new unread notification\n floats to the top of the list, even with the panel closed. */\n const liveRegionRef = useRef<HTMLDivElement | null>(null);\n const lastTopIdRef = useRef<string | undefined>(items[0]?.id);\n\n useEffect(() => {\n const topId = items[0]?.id;\n const prev = lastTopIdRef.current;\n if (prev && topId && prev !== topId && liveRegionRef.current) {\n const newest = items[0];\n if (newest && !newest.read) {\n liveRegionRef.current.textContent = t(\n 'notificationTray.newNotification',\n {\n title: newest.title,\n defaultValue: 'New notification: {{title}}',\n },\n );\n }\n }\n lastTopIdRef.current = topId;\n }, [items, t]);\n\n const displayCount = unseenCount ?? 0;\n const triggerAriaLabel =\n displayCount > 0\n ? t('notificationTray.triggerLabel', {\n count: displayCount,\n defaultValue: 'Notifications, {{count}} unread',\n })\n : t('notificationTray.triggerLabelNone', 'Notifications, none unread');\n\n const badgeLabel = formatBadgeCount(\n displayCount,\n t('notificationTray.badgeCountOverflow', '99+'),\n );\n\n const panelClass = panelVariants({ size });\n\n /* Curated imperative handle for agent integration. Pure look-ups +\n * delegations into the consumer-supplied callbacks; never touches the\n * DOM. See notification-tray.agent.ts. */\n const itemsRef = useRef<NotificationItem[]>(items);\n itemsRef.current = items;\n const unseenRef = useRef<number | undefined>(unseenCount);\n unseenRef.current = unseenCount;\n const isOpenRef = useRef<boolean>(isOpen);\n isOpenRef.current = isOpen;\n\n const handle = useMemo<NotificationTrayHandle>(\n () => ({\n open: () => handleOpenChange(true),\n close: () => handleOpenChange(false),\n isOpen: () => isOpenRef.current,\n getItems: () =>\n itemsRef.current.map((i) => ({ id: i.id, read: !!i.read })),\n getUnreadCount: () =>\n unseenRef.current ?? itemsRef.current.filter((i) => !i.read).length,\n selectItem: (targetId: string) => {\n const found = itemsRef.current.find((i) => i.id === targetId);\n if (found) onItemClick?.(found);\n },\n markRead: (targetId: string) => {\n const found = itemsRef.current.find((i) => i.id === targetId);\n if (found) onMarkRead?.(found);\n },\n dismiss: (targetId: string) => {\n const found = itemsRef.current.find((i) => i.id === targetId);\n if (found) onDismiss?.(found);\n },\n }),\n [handleOpenChange, onItemClick, onMarkRead, onDismiss],\n );\n\n const rootRef = useRef<HTMLDivElement>(null);\n useImperativeHandle(ref, () => rootRef.current as HTMLDivElement, []);\n useAgentRegistration(notificationTrayAgent, handle, id);\n\n return (\n <div\n ref={rootRef}\n data-component=\"notification-tray\"\n data-component-id={id}\n className={[notificationTrayVariants({ size }), className]\n .filter(Boolean)\n .join(' ')}\n {...rest}\n >\n <RadixPopover.Root open={isOpen} onOpenChange={handleOpenChange}>\n <RadixPopover.Trigger asChild>\n <IconButton\n icon={<Bell />}\n intent=\"outline\"\n size={size === 'sm' ? 'sm' : 'md'}\n aria-label={triggerAriaLabel}\n aria-haspopup=\"dialog\"\n />\n </RadixPopover.Trigger>\n {displayCount > 0 ? (\n <span\n aria-hidden=\"true\"\n data-testid=\"notification-tray-badge\"\n className={badgeVariants()}\n >\n {badgeLabel}\n </span>\n ) : null}\n {/* Live region sits outside the popover so it announces even\n while the panel is closed. */}\n <div\n ref={liveRegionRef}\n aria-live=\"polite\"\n aria-atomic=\"true\"\n className=\"ds:sr-only\"\n />\n <RadixPopover.Portal>\n <RadixPopover.Content\n role=\"dialog\"\n aria-label={t('notificationTray.panelTitle', 'Notifications')}\n side={side}\n align={align}\n sideOffset={8}\n // `spacing-md` (16px) keeps Radix's collision-avoidance from\n // flush-mounting the panel against the viewport edge when the\n // trigger is near the inline-end of a narrow screen.\n collisionPadding={16}\n className={panelClass}\n >\n <div className=\"ds:flex ds:items-center ds:justify-between ds:gap-[var(--spacing-sm)] ds:ps-[var(--spacing-sm)] ds:pe-[var(--spacing-sm)] ds:pb-[var(--spacing-xs)]\">\n <h3 className=\"ds:m-0 type-title-card ds:text-[color:var(--foreground)]\">\n {t('notificationTray.panelTitle', 'Notifications')}\n </h3>\n {onMarkAllRead && items.some((item) => !item.read) ? (\n <Button\n intent=\"link\"\n size=\"sm\"\n onClick={onMarkAllRead}\n className=\"ds:text-[length:var(--font-size-xs)]\"\n >\n {t('notificationTray.markAllRead', 'Mark all as read')}\n </Button>\n ) : null}\n </div>\n <Separator />\n {/* Cap at ~24rem so ~5 items fit before the list scrolls.\n Consumers override via the --notification-tray-max-block-size\n custom property on the root element. */}\n <div\n // `role=\"list\"` requires `listitem` children — applied only\n // when we render NotificationCards. The empty / loading\n // states render a `role=\"status\"` panel which would violate\n // aria-required-children if the list role were always on.\n role={loading || displayed.length === 0 ? undefined : 'list'}\n data-max-visible={maxVisible}\n // Below `sm` the panel is a near-full-screen sheet (see\n // `panelVariants`) and the list expands to fill the\n // remaining space with `flex-1`. Above `sm` the list\n // caps at the `--notification-tray-max-block-size` custom\n // property so the popover stays compact.\n className=\"ds:flex ds:flex-col ds:gap-[var(--spacing-xs)] ds:overflow-y-auto ds:pt-[var(--spacing-xs)] ds:flex-1 ds:sm:flex-none ds:sm:[max-block-size:var(--notification-tray-max-block-size,24rem)]\"\n >\n {loading ? (\n <>\n <SkeletonRow />\n <SkeletonRow />\n <SkeletonRow />\n </>\n ) : displayed.length === 0 ? (\n <EmptyPanel />\n ) : (\n displayed.map(({ item, leaving }) => (\n <NotificationCard\n key={item.id}\n item={item}\n variant=\"compact\"\n leaving={leaving}\n onActivate={onItemClick}\n onDismiss={onDismiss}\n data-notification-id={item.id}\n />\n ))\n )}\n </div>\n {viewAllLabel ? (\n <>\n <Separator />\n <div className=\"ds:pt-[var(--spacing-xs)]\">\n <Button\n intent=\"link\"\n size=\"sm\"\n onClick={onViewAll}\n className=\"ds:w-full ds:justify-center\"\n >\n {viewAllLabel}\n </Button>\n </div>\n </>\n ) : null}\n </RadixPopover.Content>\n </RadixPopover.Portal>\n </RadixPopover.Root>\n </div>\n );\n },\n);\n\nNotificationTray.displayName = 'NotificationTray';\n"],"names":["__iconNode","Bell","createLucideIcon","notificationTrayAgent","handle","args","notificationTrayVariants","cva","panelVariants","badgeVariants","formatBadgeCount","count","overflowLabel","SkeletonRow","jsxs","jsx","Skeleton","EmptyPanel","t","useTranslation","NotificationTray","forwardRef","id","items","unseenCount","open","onOpenChange","onItemClick","onDismiss","onMarkRead","onMarkAllRead","loading","maxVisible","viewAllLabel","onViewAll","align","side","size","className","rest","ref","isControlled","internalOpen","setInternalOpen","useState","isOpen","handleOpenChange","useCallback","next","displayed","setDisplayed","item","animationDurationMs","raw","trimmed","n","useLayoutEffect","prev","nextIds","i","prevIds","d","merged","fresh","useEffect","ms","timer","liveRegionRef","useRef","lastTopIdRef","_a","topId","newest","displayCount","triggerAriaLabel","badgeLabel","panelClass","itemsRef","unseenRef","isOpenRef","useMemo","targetId","found","rootRef","useImperativeHandle","useAgentRegistration","RadixPopover","IconButton","Button","Separator","Fragment","leaving","NotificationCard"],"mappings":"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","x_google_ignoreList":[0]}
|
|
@@ -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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"}
|