@zendir/ui 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +419 -406
- package/dist/react/3d/CesiumCaptureSource.js.map +1 -1
- package/dist/react/3d/ZenSpace3D.js.map +1 -1
- package/dist/react/3d/ZenSpace3DCesium.js.map +1 -1
- package/dist/react/3d/ZenSpace3DShaders.js.map +1 -1
- package/dist/react/3d/ZenSpace3DUtils.js.map +1 -1
- package/dist/react/3d/capturePngAnalysis.js.map +1 -1
- package/dist/react/3d/threeLoader.js.map +1 -1
- package/dist/react/astro/ClassificationBanner.js.map +1 -1
- package/dist/react/astro/GlobalStatusBar.js.map +1 -1
- package/dist/react/astro/MissionClock.js.map +1 -1
- package/dist/react/astro/MonitoringIcon.js.map +1 -1
- package/dist/react/astro/Notification.js.map +1 -1
- package/dist/react/astro/Progress.js.map +1 -1
- package/dist/react/astro/SimulationControls.js.map +1 -1
- package/dist/react/astro/StatusIndicator.js.map +1 -1
- package/dist/react/astro/UnifiedTimeline.js.map +1 -1
- package/dist/react/cards/AccessCard.js.map +1 -1
- package/dist/react/cards/OrbitCard.js.map +1 -1
- package/dist/react/cards/SpacecraftCard.js.map +1 -1
- package/dist/react/cards/TelemetryCard.js.map +1 -1
- package/dist/react/cards/TelemetryStreamCard.js.map +1 -1
- package/dist/react/charts/GroundTrackMap.js.map +1 -1
- package/dist/react/charts/GroundTrackMapLeaflet.js.map +1 -1
- package/dist/react/charts/groundTrackMapLeafletTiles.js.map +1 -1
- package/dist/react/charts/groundTrackMapLeafletUtils.js.map +1 -1
- package/dist/react/charts/unified/AstroChart.js.map +1 -1
- package/dist/react/charts/unified/PowerOverviewChart.js.map +1 -1
- package/dist/react/charts/unified/domain.js.map +1 -1
- package/dist/react/charts/unified/generators.js.map +1 -1
- package/dist/react/charts/unified/presets.js.map +1 -1
- package/dist/react/charts/unified/sync.js.map +1 -1
- package/dist/react/charts/unified/theme.js.map +1 -1
- package/dist/react/charts/unified/useChartStream.js.map +1 -1
- package/dist/react/chatgpt/index.js.map +1 -1
- package/dist/react/context/CategoryContext.js.map +1 -1
- package/dist/react/context/DisplaySettingsContext.js.map +1 -1
- package/dist/react/context/SpatialSelectionContext.js.map +1 -1
- package/dist/react/core/data/DataTable.js.map +1 -1
- package/dist/react/core/data/DataValue.js.map +1 -1
- package/dist/react/core/data/propertyConfig.js.map +1 -1
- package/dist/react/core/display/AstroIcon.js.map +1 -1
- package/dist/react/core/display/Badge.js.map +1 -1
- package/dist/react/core/display/CardHeader.js.map +1 -1
- package/dist/react/core/display/Container.js.map +1 -1
- package/dist/react/core/display/CopyButton.js.map +1 -1
- package/dist/react/core/display/GlassCard.js.map +1 -1
- package/dist/react/core/display/HeaderIconWithStatus.js.map +1 -1
- package/dist/react/core/display/Icon.js.map +1 -1
- package/dist/react/core/display/Typography.js.map +1 -1
- package/dist/react/core/feedback/ConfirmDialog.js.map +1 -1
- package/dist/react/core/feedback/Dialog.js.map +1 -1
- package/dist/react/core/feedback/Toast.js.map +1 -1
- package/dist/react/core/inputs/Button.js.map +1 -1
- package/dist/react/core/inputs/Checkbox.js.map +1 -1
- package/dist/react/core/inputs/Input.js.map +1 -1
- package/dist/react/core/inputs/LimitsBar.js.map +1 -1
- package/dist/react/core/inputs/NumberInput.js.map +1 -1
- package/dist/react/core/inputs/PinInput.js.map +1 -1
- package/dist/react/core/inputs/Select.js.map +1 -1
- package/dist/react/core/inputs/Toggle.js.map +1 -1
- package/dist/react/core/layout/Box.js.map +1 -1
- package/dist/react/core/layout/Center.js.map +1 -1
- package/dist/react/core/layout/Divider.js.map +1 -1
- package/dist/react/core/layout/Flex.js.map +1 -1
- package/dist/react/core/layout/Grid.js.map +1 -1
- package/dist/react/core/layout/Spacer.js.map +1 -1
- package/dist/react/core/layout/Stack.js.map +1 -1
- package/dist/react/core/layout/responsive.js.map +1 -1
- package/dist/react/core/layout/useBreakpoint.js.map +1 -1
- package/dist/react/core/navigation/AppBar.js.map +1 -1
- package/dist/react/core/navigation/Pagination.js.map +1 -1
- package/dist/react/core/navigation/SideNav.js.map +1 -1
- package/dist/react/core/navigation/Tabs.js.map +1 -1
- package/dist/react/core/overlays/Popover.js.map +1 -1
- package/dist/react/core/overlays/SidePanel.js.map +1 -1
- package/dist/react/core/overlays/Tooltip.js.map +1 -1
- package/dist/react/core/widgets/ActivityPlanner.js.map +1 -1
- package/dist/react/core/widgets/Capture.js.map +1 -1
- package/dist/react/core/widgets/ChatPanel.js.map +1 -1
- package/dist/react/core/widgets/ColorPickerPanel.js.map +1 -1
- package/dist/react/core/widgets/CommandBuilder.js.map +1 -1
- package/dist/react/core/widgets/ConnectionForm.js.map +1 -1
- package/dist/react/core/widgets/FileExplorer.js.map +1 -1
- package/dist/react/core/widgets/HexViewer.js.map +1 -1
- package/dist/react/core/widgets/ImageFrame.js.map +1 -1
- package/dist/react/core/widgets/ImageGallery.js.map +1 -1
- package/dist/react/core/widgets/ImagePanel.js.map +1 -1
- package/dist/react/core/widgets/LogViewer.js.map +1 -1
- package/dist/react/core/widgets/MessageStream.js.map +1 -1
- package/dist/react/core/widgets/MissionCalendar.js.map +1 -1
- package/dist/react/core/widgets/PacketViewer.js.map +1 -1
- package/dist/react/hooks/useCompactMode.js.map +1 -1
- package/dist/react/hooks/useSimulationScene.js.map +1 -1
- package/dist/react/panels/LayerControlPanel.js.map +1 -1
- package/dist/react/panels/ObjectInventoryPanel.js.map +1 -1
- package/dist/react/shared/ErrorBoundary.js.map +1 -1
- package/dist/react/shared/Skeleton.js.map +1 -1
- package/dist/react/theme/ThemeProvider.js +2 -1
- package/dist/react/theme/ThemeProvider.js.map +1 -1
- package/dist/react/theme/cardAccent.js.map +1 -1
- package/dist/react/theme/config.js.map +1 -1
- package/dist/react/theme/types.js.map +1 -1
- package/dist/react/types.js.map +1 -1
- package/dist/react/utils/categoryPalette.js.map +1 -1
- package/dist/react/utils/index.js.map +1 -1
- package/dist/style.css +216 -216
- package/dist/tokens/core.js.map +1 -1
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/palettes/astro.js.map +1 -1
- package/dist/tokens/palettes/index.js.map +1 -1
- package/dist/tokens/palettes/tailwind.js.map +1 -1
- package/dist/tokens/palettes/zendir.js.map +1 -1
- package/dist/tokens/themes.js.map +1 -1
- package/dist/tokens/tokens.css +533 -533
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toast.js","names":[],"sources":["../../../../src/react/core/feedback/Toast.tsx"],"sourcesContent":["/**\n * @zendir/ui - Toast / Notification System\n *\n * Contextual toast notifications with queue management, auto-dismiss,\n * and Astro UX status colors. Essential for operator dashboards surfacing\n * connection events, command results, and system alerts.\n *\n * Astro UX Compliance:\n * - 6-level status colors (off, standby, normal, caution, serious, critical)\n * - Positioned top-right (operator dashboard convention)\n * - Reduced motion support\n * - ARIA live region for screen readers\n *\n * @example\n * ```tsx\n * // Wrap app in ToastProvider\n * <ToastProvider>\n * <App />\n * </ToastProvider>\n *\n * // Use in any component\n * const toast = useToast();\n * toast({ status: 'normal', title: 'Connected', message: 'MQTT link established' });\n * toast({ status: 'caution', title: 'SAFE Mode', message: 'Spacecraft entered SAFE mode', duration: 0 });\n * toast({ status: 'critical', title: 'Link Lost', message: 'Downlink signal below threshold' });\n * ```\n */\n\nimport React, {\n memo,\n createContext,\n useContext,\n useState,\n useCallback,\n useRef,\n useEffect,\n} from \"react\";\nimport { useTheme } from \"../../theme\";\nimport { safeAccentText } from \"../../utils\";\nimport { useBreakpoint } from \"../layout/useBreakpoint\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport type ToastStatus =\n | \"normal\"\n | \"standby\"\n | \"caution\"\n | \"serious\"\n | \"critical\"\n | \"off\";\nexport type ToastPosition =\n | \"top-right\"\n | \"top-left\"\n | \"bottom-right\"\n | \"bottom-left\"\n | \"top-center\"\n | \"bottom-center\";\n\nexport interface ToastOptions {\n /** Status level (determines color) */\n status?: ToastStatus;\n /** Title text (bold) */\n title: string;\n /** Description text */\n message?: string;\n /** Auto-dismiss duration in ms (0 = persistent, default 5000) */\n duration?: number;\n /** Dismissible by user click (default true) */\n dismissible?: boolean;\n /** Icon override */\n icon?: React.ReactNode;\n /** Action button */\n action?: {\n label: string;\n onClick: () => void;\n };\n}\n\ninterface ToastInstance extends ToastOptions {\n id: string;\n createdAt: number;\n}\n\n// ─── Context ─────────────────────────────────────────────────────────────────\n\ntype ToastFn = (options: ToastOptions) => string;\n\ninterface ToastContextValue {\n toast: ToastFn;\n dismiss: (id: string) => void;\n dismissAll: () => void;\n}\n\nconst ToastContext = createContext<ToastContextValue | null>(null);\n\n// ─── Hook ────────────────────────────────────────────────────────────────────\n\n/**\n * Hook to show toast notifications.\n * Must be used within a `<ToastProvider>`.\n *\n * @example\n * ```tsx\n * const toast = useToast();\n * toast({ status: 'normal', title: 'Success', message: 'Command sent' });\n * ```\n */\nexport function useToast(): ToastFn {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used within <ToastProvider>\");\n return ctx.toast;\n}\n\n/**\n * Hook to access full toast API (toast, dismiss, dismissAll).\n */\nexport function useToastManager(): ToastContextValue {\n const ctx = useContext(ToastContext);\n if (!ctx)\n throw new Error(\"useToastManager must be used within <ToastProvider>\");\n return ctx;\n}\n\n// ─── Toast Item ──────────────────────────────────────────────────────────────\n\nconst ToastItem = memo(function ToastItem({\n toast: t,\n onDismiss,\n}: {\n toast: ToastInstance;\n onDismiss: (id: string) => void;\n}): React.ReactElement {\n const { tokens } = useTheme();\n const [exiting, setExiting] = useState(false);\n const dismissTimeoutRef = useRef<ReturnType<typeof setTimeout>>();\n\n // Cleanup dismiss timeout on unmount\n useEffect(() => {\n return () => {\n clearTimeout(dismissTimeoutRef.current);\n };\n }, []);\n\n const statusColors: Record<string, string> = {\n normal: tokens.colors.status.normal,\n standby: tokens.colors.status.standby,\n caution: tokens.colors.status.caution,\n serious: tokens.colors.status.serious,\n critical: tokens.colors.status.critical,\n off: tokens.colors.status.off,\n };\n\n const color = statusColors[t.status || \"off\"] || tokens.colors.status.off;\n\n const handleDismiss = useCallback(() => {\n setExiting(true);\n dismissTimeoutRef.current = setTimeout(() => onDismiss(t.id), 200);\n }, [t.id, onDismiss]);\n\n // Auto-dismiss\n useEffect(() => {\n const dur = t.duration ?? 5000;\n if (dur <= 0) return;\n const timer = setTimeout(handleDismiss, dur);\n return () => clearTimeout(timer);\n }, [t.duration, handleDismiss]);\n\n // Status icon — Astro UX dual-coded shapes\n const statusIcon = (() => {\n const s = 10;\n const h = s / 2;\n const status = t.status || \"off\";\n const sharedStyle: React.CSSProperties = {\n flexShrink: 0,\n marginTop: \"3px\",\n };\n switch (status) {\n case \"critical\":\n return (\n <svg\n width={s}\n height={s}\n viewBox={`0 0 ${s} ${s}`}\n style={sharedStyle}\n >\n <polygon points={`${h},${s} 0,0 ${s},0`} fill={color} />\n </svg>\n );\n case \"serious\":\n return (\n <svg\n width={s}\n height={s}\n viewBox={`0 0 ${s} ${s}`}\n style={sharedStyle}\n >\n <polygon\n points={`${h},0 ${s},${h} ${h},${s} 0,${h}`}\n fill={color}\n />\n </svg>\n );\n case \"caution\":\n return (\n <svg\n width={s}\n height={s}\n viewBox={`0 0 ${s} ${s}`}\n style={sharedStyle}\n >\n <rect width={s} height={s} fill={color} />\n </svg>\n );\n case \"standby\":\n return (\n <svg width={s} height={s} viewBox=\"0 0 12 12\" style={sharedStyle}>\n <circle\n cx=\"6\"\n cy=\"6\"\n r=\"3.5\"\n fill=\"none\"\n stroke={color}\n strokeWidth=\"2\"\n />\n </svg>\n );\n case \"off\":\n return (\n <svg width={s} height={s} viewBox=\"0 0 12 12\" style={sharedStyle}>\n <circle cx=\"6\" cy=\"6\" r=\"3\" fill={color} />\n </svg>\n );\n default: // normal\n return (\n <svg\n width={s}\n height={s}\n viewBox={`0 0 ${s} ${s}`}\n style={sharedStyle}\n >\n <circle cx={h} cy={h} r={h} fill={color} />\n </svg>\n );\n }\n })();\n\n return (\n <div\n role=\"alert\"\n style={{\n display: \"flex\",\n gap: tokens.spacing.sm,\n padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,\n backgroundColor: tokens.colors.background.elevated,\n border: `1px solid ${color}30`,\n borderLeft: `3px solid ${color}`,\n borderRadius: tokens.borderRadius.md,\n boxShadow: `${tokens.shadows.lg}, 0 0 20px ${color}15`,\n maxWidth: \"min(400px, calc(100vw - 32px))\",\n minWidth: \"min(280px, calc(100vw - 32px))\",\n fontFamily: tokens.typography.fontFamily.primary,\n animation: exiting\n ? `zendir-toast-exit 200ms ${tokens.animation.easing.default} forwards`\n : `zendir-toast-enter 300ms ${tokens.animation.easing.default}`,\n transition: tokens.animation.normal,\n }}\n >\n {t.icon || statusIcon}\n\n <div style={{ flex: 1, minWidth: 0 }}>\n <div\n style={{\n fontSize: tokens.typography.fontSize.sm,\n fontWeight: tokens.typography.fontWeight.semibold,\n color: tokens.colors.text.primary,\n lineHeight: tokens.typography.lineHeight.tight,\n }}\n >\n {t.title}\n </div>\n {t.message && (\n <div\n style={{\n fontSize: tokens.typography.fontSize.xs,\n color: tokens.colors.text.secondary,\n marginTop: \"2px\",\n lineHeight: tokens.typography.lineHeight.normal,\n }}\n >\n {t.message}\n </div>\n )}\n {t.action && (\n <button\n onClick={t.action.onClick}\n style={{\n marginTop: tokens.spacing.xs,\n fontSize: tokens.typography.fontSize.xs,\n fontWeight: tokens.typography.fontWeight.semibold,\n color: safeAccentText(tokens.colors.accent.primary),\n background: \"none\",\n border: \"none\",\n padding: 0,\n cursor: \"pointer\",\n fontFamily: tokens.typography.fontFamily.primary,\n }}\n >\n {t.action.label}\n </button>\n )}\n </div>\n\n {t.dismissible !== false && (\n <button\n aria-label=\"Dismiss notification\"\n onClick={handleDismiss}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 44,\n height: 44,\n border: \"none\",\n background: \"none\",\n color: tokens.colors.text.tertiary,\n cursor: \"pointer\",\n borderRadius: tokens.borderRadius.sm,\n padding: tokens.spacing.xs,\n flexShrink: 0,\n transition: tokens.animation.fast,\n }}\n >\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n )}\n </div>\n );\n});\n\n// ─── Provider ────────────────────────────────────────────────────────────────\n\nexport interface ToastProviderProps {\n /** Toast position (default: top-right) */\n position?: ToastPosition;\n /** Maximum visible toasts (default: 5) */\n maxVisible?: number;\n /** Default duration in ms (default: 5000) */\n defaultDuration?: number;\n /** Children */\n children: React.ReactNode;\n}\n\nexport const ToastProvider = memo(function ToastProvider({\n position = \"top-right\",\n maxVisible = 5,\n defaultDuration = 5000,\n children,\n}: ToastProviderProps): React.ReactElement {\n const { tokens } = useTheme();\n const { isMobile } = useBreakpoint();\n const [toasts, setToasts] = useState<ToastInstance[]>([]);\n const idCounter = useRef(0);\n\n const toast = useCallback(\n (options: ToastOptions): string => {\n const id = `toast-${++idCounter.current}-${Date.now()}`;\n const instance: ToastInstance = {\n ...options,\n id,\n createdAt: Date.now(),\n duration: options.duration ?? defaultDuration,\n };\n setToasts((prev) => [...prev, instance].slice(-maxVisible * 2)); // Keep buffer\n return id;\n },\n [defaultDuration, maxVisible],\n );\n\n const dismiss = useCallback((id: string) => {\n setToasts((prev) => prev.filter((t) => t.id !== id));\n }, []);\n\n const dismissAll = useCallback(() => {\n setToasts([]);\n }, []);\n\n // Position styles\n const inset = isMobile ? tokens.spacing.sm : tokens.spacing.md;\n const positionStyles: Record<ToastPosition, React.CSSProperties> = {\n \"top-right\": { top: inset, right: inset },\n \"top-left\": { top: inset, left: inset },\n \"bottom-right\": { bottom: inset, right: inset },\n \"bottom-left\": { bottom: inset, left: inset },\n \"top-center\": { top: inset, left: \"50%\", transform: \"translateX(-50%)\" },\n \"bottom-center\": {\n bottom: inset,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n },\n };\n\n const isBottom = position.startsWith(\"bottom\");\n const visibleToasts = toasts.slice(-maxVisible);\n\n return (\n <ToastContext.Provider value={{ toast, dismiss, dismissAll }}>\n {children}\n\n {/* Toast container */}\n {visibleToasts.length > 0 && (\n <div\n aria-live={\n visibleToasts.some(\n (t) => t.status === \"critical\" || t.status === \"serious\",\n )\n ? \"assertive\"\n : \"polite\"\n }\n aria-atomic=\"false\"\n style={{\n position: \"fixed\",\n zIndex: 9999,\n display: \"flex\",\n flexDirection: isBottom ? \"column-reverse\" : \"column\",\n gap: tokens.spacing.sm,\n pointerEvents: \"none\",\n ...positionStyles[position],\n }}\n >\n {visibleToasts.map((t) => (\n <div key={t.id} style={{ pointerEvents: \"auto\" }}>\n <ToastItem toast={t} onDismiss={dismiss} />\n </div>\n ))}\n </div>\n )}\n\n <style>{`\n @keyframes zendir-toast-enter {\n from { opacity: 0; transform: translateX(20px); }\n to { opacity: 1; transform: translateX(0); }\n }\n @keyframes zendir-toast-exit {\n from { opacity: 1; transform: translateX(0); }\n to { opacity: 0; transform: translateX(20px); }\n }\n @media (prefers-reduced-motion: reduce) {\n @keyframes zendir-toast-enter { from { opacity: 0; } to { opacity: 1; } }\n @keyframes zendir-toast-exit { from { opacity: 1; } to { opacity: 0; } }\n }\n `}</style>\n </ToastContext.Provider>\n );\n});\n\nexport default ToastProvider;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,IAAM,eAAe,cAAwC,IAAI;;;;;;;;;;;AAcjE,SAAgB,WAAoB;CAClC,MAAM,MAAM,WAAW,YAAY;CACnC,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,8CAA8C;CACxE,OAAO,IAAI;AACb;;;;AAKA,SAAgB,kBAAqC;CACnD,MAAM,MAAM,WAAW,YAAY;CACnC,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,qDAAqD;CACvE,OAAO;AACT;AAIA,IAAM,YAAY,KAAK,SAAS,UAAU,EACxC,OAAO,GACP,aAIqB;CACrB,MAAM,EAAE,WAAW,SAAS;CAC5B,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,oBAAoB,OAAsC;CAGhE,gBAAgB;EACd,aAAa;GACX,aAAa,kBAAkB,OAAO;EACxC;CACF,GAAG,CAAC,CAAC;CAWL,MAAM,QAAQ;EARZ,QAAQ,OAAO,OAAO,OAAO;EAC7B,SAAS,OAAO,OAAO,OAAO;EAC9B,SAAS,OAAO,OAAO,OAAO;EAC9B,SAAS,OAAO,OAAO,OAAO;EAC9B,UAAU,OAAO,OAAO,OAAO;EAC/B,KAAK,OAAO,OAAO,OAAO;CAGd,EAAa,EAAE,UAAU,UAAU,OAAO,OAAO,OAAO;CAEtE,MAAM,gBAAgB,kBAAkB;EACtC,WAAW,IAAI;EACf,kBAAkB,UAAU,iBAAiB,UAAU,EAAE,EAAE,GAAG,GAAG;CACnE,GAAG,CAAC,EAAE,IAAI,SAAS,CAAC;CAGpB,gBAAgB;EACd,MAAM,MAAM,EAAE,YAAY;EAC1B,IAAI,OAAO,GAAG;EACd,MAAM,QAAQ,WAAW,eAAe,GAAG;EAC3C,aAAa,aAAa,KAAK;CACjC,GAAG,CAAC,EAAE,UAAU,aAAa,CAAC;CAG9B,MAAM,oBAAoB;EACxB,MAAM,IAAI;EACV,MAAM,IAAI,IAAI;EACd,MAAM,SAAS,EAAE,UAAU;EAC3B,MAAM,cAAmC;GACvC,YAAY;GACZ,WAAW;EACb;EACA,QAAQ,QAAR;GACE,KAAK,YACH,OACE,oBAAC,OAAD;IACE,OAAO;IACP,QAAQ;IACR,SAAS,OAAO,EAAE,GAAG;IACrB,OAAO;cAEP,oBAAC,WAAD;KAAS,QAAQ,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE;KAAK,MAAM;IAAQ,CAAA;GACpD,CAAA;GAET,KAAK,WACH,OACE,oBAAC,OAAD;IACE,OAAO;IACP,QAAQ;IACR,SAAS,OAAO,EAAE,GAAG;IACrB,OAAO;cAEP,oBAAC,WAAD;KACE,QAAQ,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK;KACxC,MAAM;IACP,CAAA;GACE,CAAA;GAET,KAAK,WACH,OACE,oBAAC,OAAD;IACE,OAAO;IACP,QAAQ;IACR,SAAS,OAAO,EAAE,GAAG;IACrB,OAAO;cAEP,oBAAC,QAAD;KAAM,OAAO;KAAG,QAAQ;KAAG,MAAM;IAAQ,CAAA;GACtC,CAAA;GAET,KAAK,WACH,OACE,oBAAC,OAAD;IAAK,OAAO;IAAG,QAAQ;IAAG,SAAQ;IAAY,OAAO;cACnD,oBAAC,UAAD;KACE,IAAG;KACH,IAAG;KACH,GAAE;KACF,MAAK;KACL,QAAQ;KACR,aAAY;IACb,CAAA;GACE,CAAA;GAET,KAAK,OACH,OACE,oBAAC,OAAD;IAAK,OAAO;IAAG,QAAQ;IAAG,SAAQ;IAAY,OAAO;cACnD,oBAAC,UAAD;KAAQ,IAAG;KAAI,IAAG;KAAI,GAAE;KAAI,MAAM;IAAQ,CAAA;GACvC,CAAA;GAET,SACE,OACE,oBAAC,OAAD;IACE,OAAO;IACP,QAAQ;IACR,SAAS,OAAO,EAAE,GAAG;IACrB,OAAO;cAEP,oBAAC,UAAD;KAAQ,IAAI;KAAG,IAAI;KAAG,GAAG;KAAG,MAAM;IAAQ,CAAA;GACvC,CAAA;EAEX;CACF,EAAA,CAAG;CAEH,OACE,qBAAC,OAAD;EACE,MAAK;EACL,OAAO;GACL,SAAS;GACT,KAAK,OAAO,QAAQ;GACpB,SAAS,GAAG,OAAO,QAAQ,GAAG,GAAG,OAAO,QAAQ;GAChD,iBAAiB,OAAO,OAAO,WAAW;GAC1C,QAAQ,aAAa,MAAM;GAC3B,YAAY,aAAa;GACzB,cAAc,OAAO,aAAa;GAClC,WAAW,GAAG,OAAO,QAAQ,GAAG,aAAa,MAAM;GACnD,UAAU;GACV,UAAU;GACV,YAAY,OAAO,WAAW,WAAW;GACzC,WAAW,UACP,2BAA2B,OAAO,UAAU,OAAO,QAAQ,aAC3D,4BAA4B,OAAO,UAAU,OAAO;GACxD,YAAY,OAAO,UAAU;EAC/B;YAlBF;GAoBG,EAAE,QAAQ;GAEX,qBAAC,OAAD;IAAK,OAAO;KAAE,MAAM;KAAG,UAAU;IAAE;cAAnC;KACE,oBAAC,OAAD;MACE,OAAO;OACL,UAAU,OAAO,WAAW,SAAS;OACrC,YAAY,OAAO,WAAW,WAAW;OACzC,OAAO,OAAO,OAAO,KAAK;OAC1B,YAAY,OAAO,WAAW,WAAW;MAC3C;gBAEC,EAAE;KACA,CAAA;KACJ,EAAE,WACD,oBAAC,OAAD;MACE,OAAO;OACL,UAAU,OAAO,WAAW,SAAS;OACrC,OAAO,OAAO,OAAO,KAAK;OAC1B,WAAW;OACX,YAAY,OAAO,WAAW,WAAW;MAC3C;gBAEC,EAAE;KACA,CAAA;KAEN,EAAE,UACD,oBAAC,UAAD;MACE,SAAS,EAAE,OAAO;MAClB,OAAO;OACL,WAAW,OAAO,QAAQ;OAC1B,UAAU,OAAO,WAAW,SAAS;OACrC,YAAY,OAAO,WAAW,WAAW;OACzC,OAAO,eAAe,OAAO,OAAO,OAAO,OAAO;OAClD,YAAY;OACZ,QAAQ;OACR,SAAS;OACT,QAAQ;OACR,YAAY,OAAO,WAAW,WAAW;MAC3C;gBAEC,EAAE,OAAO;KACJ,CAAA;IAEP;;GAEJ,EAAE,gBAAgB,SACjB,oBAAC,UAAD;IACE,cAAW;IACX,SAAS;IACT,OAAO;KACL,SAAS;KACT,YAAY;KACZ,gBAAgB;KAChB,OAAO;KACP,QAAQ;KACR,QAAQ;KACR,YAAY;KACZ,OAAO,OAAO,OAAO,KAAK;KAC1B,QAAQ;KACR,cAAc,OAAO,aAAa;KAClC,SAAS,OAAO,QAAQ;KACxB,YAAY;KACZ,YAAY,OAAO,UAAU;IAC/B;cAEA,qBAAC,OAAD;KACE,OAAM;KACN,QAAO;KACP,SAAQ;KACR,MAAK;KACL,QAAO;KACP,aAAY;KACZ,eAAc;eAPhB,CASE,oBAAC,QAAD;MAAM,IAAG;MAAK,IAAG;MAAI,IAAG;MAAI,IAAG;KAAM,CAAA,GACrC,oBAAC,QAAD;MAAM,IAAG;MAAI,IAAG;MAAI,IAAG;MAAK,IAAG;KAAM,CAAA,CAClC;;GACC,CAAA;EAEP;;AAET,CAAC;AAeD,IAAa,gBAAgB,KAAK,SAAS,cAAc,EACvD,WAAW,aACX,aAAa,GACb,kBAAkB,KAClB,YACyC;CACzC,MAAM,EAAE,WAAW,SAAS;CAC5B,MAAM,EAAE,aAAa,cAAc;CACnC,MAAM,CAAC,QAAQ,aAAa,SAA0B,CAAC,CAAC;CACxD,MAAM,YAAY,OAAO,CAAC;CAE1B,MAAM,QAAQ,aACX,YAAkC;EACjC,MAAM,KAAK,SAAS,EAAE,UAAU,QAAQ,GAAG,KAAK,IAAI;EACpD,MAAM,WAA0B;GAC9B,GAAG;GACH;GACA,WAAW,KAAK,IAAI;GACpB,UAAU,QAAQ,YAAY;EAChC;EACA,WAAW,SAAS,CAAC,GAAG,MAAM,QAAQ,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;EAC9D,OAAO;CACT,GACA,CAAC,iBAAiB,UAAU,CAC9B;CAEA,MAAM,UAAU,aAAa,OAAe;EAC1C,WAAW,SAAS,KAAK,QAAQ,MAAM,EAAE,OAAO,EAAE,CAAC;CACrD,GAAG,CAAC,CAAC;CAEL,MAAM,aAAa,kBAAkB;EACnC,UAAU,CAAC,CAAC;CACd,GAAG,CAAC,CAAC;CAGL,MAAM,QAAQ,WAAW,OAAO,QAAQ,KAAK,OAAO,QAAQ;CAC5D,MAAM,iBAA6D;EACjE,aAAa;GAAE,KAAK;GAAO,OAAO;EAAM;EACxC,YAAY;GAAE,KAAK;GAAO,MAAM;EAAM;EACtC,gBAAgB;GAAE,QAAQ;GAAO,OAAO;EAAM;EAC9C,eAAe;GAAE,QAAQ;GAAO,MAAM;EAAM;EAC5C,cAAc;GAAE,KAAK;GAAO,MAAM;GAAO,WAAW;EAAmB;EACvE,iBAAiB;GACf,QAAQ;GACR,MAAM;GACN,WAAW;EACb;CACF;CAEA,MAAM,WAAW,SAAS,WAAW,QAAQ;CAC7C,MAAM,gBAAgB,OAAO,MAAM,CAAC,UAAU;CAE9C,OACE,qBAAC,aAAa,UAAd;EAAuB,OAAO;GAAE;GAAO;GAAS;EAAW;YAA3D;GACG;GAGA,cAAc,SAAS,KACtB,oBAAC,OAAD;IACE,aACE,cAAc,MACX,MAAM,EAAE,WAAW,cAAc,EAAE,WAAW,SACjD,IACI,cACA;IAEN,eAAY;IACZ,OAAO;KACL,UAAU;KACV,QAAQ;KACR,SAAS;KACT,eAAe,WAAW,mBAAmB;KAC7C,KAAK,OAAO,QAAQ;KACpB,eAAe;KACf,GAAG,eAAe;IACpB;cAEC,cAAc,KAAK,MAClB,oBAAC,OAAD;KAAgB,OAAO,EAAE,eAAe,OAAO;eAC7C,oBAAC,WAAD;MAAW,OAAO;MAAG,WAAW;KAAU,CAAA;IACvC,GAFK,EAAE,EAEP,CACN;GACE,CAAA;GAGP,oBAAC,SAAD,EAAA,UAAQ;;;;;;;;;;;;;QAaC,CAAA;EACY;;AAE3B,CAAC"}
|
|
1
|
+
{"version":3,"file":"Toast.js","names":[],"sources":["../../../../src/react/core/feedback/Toast.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - Toast / Notification System\r\n *\r\n * Contextual toast notifications with queue management, auto-dismiss,\r\n * and Astro UX status colors. Essential for operator dashboards surfacing\r\n * connection events, command results, and system alerts.\r\n *\r\n * Astro UX Compliance:\r\n * - 6-level status colors (off, standby, normal, caution, serious, critical)\r\n * - Positioned top-right (operator dashboard convention)\r\n * - Reduced motion support\r\n * - ARIA live region for screen readers\r\n *\r\n * @example\r\n * ```tsx\r\n * // Wrap app in ToastProvider\r\n * <ToastProvider>\r\n * <App />\r\n * </ToastProvider>\r\n *\r\n * // Use in any component\r\n * const toast = useToast();\r\n * toast({ status: 'normal', title: 'Connected', message: 'MQTT link established' });\r\n * toast({ status: 'caution', title: 'SAFE Mode', message: 'Spacecraft entered SAFE mode', duration: 0 });\r\n * toast({ status: 'critical', title: 'Link Lost', message: 'Downlink signal below threshold' });\r\n * ```\r\n */\r\n\r\nimport React, {\r\n memo,\r\n createContext,\r\n useContext,\r\n useState,\r\n useCallback,\r\n useRef,\r\n useEffect,\r\n} from \"react\";\r\nimport { useTheme } from \"../../theme\";\r\nimport { safeAccentText } from \"../../utils\";\r\nimport { useBreakpoint } from \"../layout/useBreakpoint\";\r\n\r\n// ─── Types ───────────────────────────────────────────────────────────────────\r\n\r\nexport type ToastStatus =\r\n | \"normal\"\r\n | \"standby\"\r\n | \"caution\"\r\n | \"serious\"\r\n | \"critical\"\r\n | \"off\";\r\nexport type ToastPosition =\r\n | \"top-right\"\r\n | \"top-left\"\r\n | \"bottom-right\"\r\n | \"bottom-left\"\r\n | \"top-center\"\r\n | \"bottom-center\";\r\n\r\nexport interface ToastOptions {\r\n /** Status level (determines color) */\r\n status?: ToastStatus;\r\n /** Title text (bold) */\r\n title: string;\r\n /** Description text */\r\n message?: string;\r\n /** Auto-dismiss duration in ms (0 = persistent, default 5000) */\r\n duration?: number;\r\n /** Dismissible by user click (default true) */\r\n dismissible?: boolean;\r\n /** Icon override */\r\n icon?: React.ReactNode;\r\n /** Action button */\r\n action?: {\r\n label: string;\r\n onClick: () => void;\r\n };\r\n}\r\n\r\ninterface ToastInstance extends ToastOptions {\r\n id: string;\r\n createdAt: number;\r\n}\r\n\r\n// ─── Context ─────────────────────────────────────────────────────────────────\r\n\r\ntype ToastFn = (options: ToastOptions) => string;\r\n\r\ninterface ToastContextValue {\r\n toast: ToastFn;\r\n dismiss: (id: string) => void;\r\n dismissAll: () => void;\r\n}\r\n\r\nconst ToastContext = createContext<ToastContextValue | null>(null);\r\n\r\n// ─── Hook ────────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook to show toast notifications.\r\n * Must be used within a `<ToastProvider>`.\r\n *\r\n * @example\r\n * ```tsx\r\n * const toast = useToast();\r\n * toast({ status: 'normal', title: 'Success', message: 'Command sent' });\r\n * ```\r\n */\r\nexport function useToast(): ToastFn {\r\n const ctx = useContext(ToastContext);\r\n if (!ctx) throw new Error(\"useToast must be used within <ToastProvider>\");\r\n return ctx.toast;\r\n}\r\n\r\n/**\r\n * Hook to access full toast API (toast, dismiss, dismissAll).\r\n */\r\nexport function useToastManager(): ToastContextValue {\r\n const ctx = useContext(ToastContext);\r\n if (!ctx)\r\n throw new Error(\"useToastManager must be used within <ToastProvider>\");\r\n return ctx;\r\n}\r\n\r\n// ─── Toast Item ──────────────────────────────────────────────────────────────\r\n\r\nconst ToastItem = memo(function ToastItem({\r\n toast: t,\r\n onDismiss,\r\n}: {\r\n toast: ToastInstance;\r\n onDismiss: (id: string) => void;\r\n}): React.ReactElement {\r\n const { tokens } = useTheme();\r\n const [exiting, setExiting] = useState(false);\r\n const dismissTimeoutRef = useRef<ReturnType<typeof setTimeout>>();\r\n\r\n // Cleanup dismiss timeout on unmount\r\n useEffect(() => {\r\n return () => {\r\n clearTimeout(dismissTimeoutRef.current);\r\n };\r\n }, []);\r\n\r\n const statusColors: Record<string, string> = {\r\n normal: tokens.colors.status.normal,\r\n standby: tokens.colors.status.standby,\r\n caution: tokens.colors.status.caution,\r\n serious: tokens.colors.status.serious,\r\n critical: tokens.colors.status.critical,\r\n off: tokens.colors.status.off,\r\n };\r\n\r\n const color = statusColors[t.status || \"off\"] || tokens.colors.status.off;\r\n\r\n const handleDismiss = useCallback(() => {\r\n setExiting(true);\r\n dismissTimeoutRef.current = setTimeout(() => onDismiss(t.id), 200);\r\n }, [t.id, onDismiss]);\r\n\r\n // Auto-dismiss\r\n useEffect(() => {\r\n const dur = t.duration ?? 5000;\r\n if (dur <= 0) return;\r\n const timer = setTimeout(handleDismiss, dur);\r\n return () => clearTimeout(timer);\r\n }, [t.duration, handleDismiss]);\r\n\r\n // Status icon — Astro UX dual-coded shapes\r\n const statusIcon = (() => {\r\n const s = 10;\r\n const h = s / 2;\r\n const status = t.status || \"off\";\r\n const sharedStyle: React.CSSProperties = {\r\n flexShrink: 0,\r\n marginTop: \"3px\",\r\n };\r\n switch (status) {\r\n case \"critical\":\r\n return (\r\n <svg\r\n width={s}\r\n height={s}\r\n viewBox={`0 0 ${s} ${s}`}\r\n style={sharedStyle}\r\n >\r\n <polygon points={`${h},${s} 0,0 ${s},0`} fill={color} />\r\n </svg>\r\n );\r\n case \"serious\":\r\n return (\r\n <svg\r\n width={s}\r\n height={s}\r\n viewBox={`0 0 ${s} ${s}`}\r\n style={sharedStyle}\r\n >\r\n <polygon\r\n points={`${h},0 ${s},${h} ${h},${s} 0,${h}`}\r\n fill={color}\r\n />\r\n </svg>\r\n );\r\n case \"caution\":\r\n return (\r\n <svg\r\n width={s}\r\n height={s}\r\n viewBox={`0 0 ${s} ${s}`}\r\n style={sharedStyle}\r\n >\r\n <rect width={s} height={s} fill={color} />\r\n </svg>\r\n );\r\n case \"standby\":\r\n return (\r\n <svg width={s} height={s} viewBox=\"0 0 12 12\" style={sharedStyle}>\r\n <circle\r\n cx=\"6\"\r\n cy=\"6\"\r\n r=\"3.5\"\r\n fill=\"none\"\r\n stroke={color}\r\n strokeWidth=\"2\"\r\n />\r\n </svg>\r\n );\r\n case \"off\":\r\n return (\r\n <svg width={s} height={s} viewBox=\"0 0 12 12\" style={sharedStyle}>\r\n <circle cx=\"6\" cy=\"6\" r=\"3\" fill={color} />\r\n </svg>\r\n );\r\n default: // normal\r\n return (\r\n <svg\r\n width={s}\r\n height={s}\r\n viewBox={`0 0 ${s} ${s}`}\r\n style={sharedStyle}\r\n >\r\n <circle cx={h} cy={h} r={h} fill={color} />\r\n </svg>\r\n );\r\n }\r\n })();\r\n\r\n return (\r\n <div\r\n role=\"alert\"\r\n style={{\r\n display: \"flex\",\r\n gap: tokens.spacing.sm,\r\n padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,\r\n backgroundColor: tokens.colors.background.elevated,\r\n border: `1px solid ${color}30`,\r\n borderLeft: `3px solid ${color}`,\r\n borderRadius: tokens.borderRadius.md,\r\n boxShadow: `${tokens.shadows.lg}, 0 0 20px ${color}15`,\r\n maxWidth: \"min(400px, calc(100vw - 32px))\",\r\n minWidth: \"min(280px, calc(100vw - 32px))\",\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n animation: exiting\r\n ? `zendir-toast-exit 200ms ${tokens.animation.easing.default} forwards`\r\n : `zendir-toast-enter 300ms ${tokens.animation.easing.default}`,\r\n transition: tokens.animation.normal,\r\n }}\r\n >\r\n {t.icon || statusIcon}\r\n\r\n <div style={{ flex: 1, minWidth: 0 }}>\r\n <div\r\n style={{\r\n fontSize: tokens.typography.fontSize.sm,\r\n fontWeight: tokens.typography.fontWeight.semibold,\r\n color: tokens.colors.text.primary,\r\n lineHeight: tokens.typography.lineHeight.tight,\r\n }}\r\n >\r\n {t.title}\r\n </div>\r\n {t.message && (\r\n <div\r\n style={{\r\n fontSize: tokens.typography.fontSize.xs,\r\n color: tokens.colors.text.secondary,\r\n marginTop: \"2px\",\r\n lineHeight: tokens.typography.lineHeight.normal,\r\n }}\r\n >\r\n {t.message}\r\n </div>\r\n )}\r\n {t.action && (\r\n <button\r\n onClick={t.action.onClick}\r\n style={{\r\n marginTop: tokens.spacing.xs,\r\n fontSize: tokens.typography.fontSize.xs,\r\n fontWeight: tokens.typography.fontWeight.semibold,\r\n color: safeAccentText(tokens.colors.accent.primary),\r\n background: \"none\",\r\n border: \"none\",\r\n padding: 0,\r\n cursor: \"pointer\",\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n }}\r\n >\r\n {t.action.label}\r\n </button>\r\n )}\r\n </div>\r\n\r\n {t.dismissible !== false && (\r\n <button\r\n aria-label=\"Dismiss notification\"\r\n onClick={handleDismiss}\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n width: 44,\r\n height: 44,\r\n border: \"none\",\r\n background: \"none\",\r\n color: tokens.colors.text.tertiary,\r\n cursor: \"pointer\",\r\n borderRadius: tokens.borderRadius.sm,\r\n padding: tokens.spacing.xs,\r\n flexShrink: 0,\r\n transition: tokens.animation.fast,\r\n }}\r\n >\r\n <svg\r\n width=\"14\"\r\n height=\"14\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n strokeWidth=\"2\"\r\n strokeLinecap=\"round\"\r\n >\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\r\n </svg>\r\n </button>\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\n// ─── Provider ────────────────────────────────────────────────────────────────\r\n\r\nexport interface ToastProviderProps {\r\n /** Toast position (default: top-right) */\r\n position?: ToastPosition;\r\n /** Maximum visible toasts (default: 5) */\r\n maxVisible?: number;\r\n /** Default duration in ms (default: 5000) */\r\n defaultDuration?: number;\r\n /** Children */\r\n children: React.ReactNode;\r\n}\r\n\r\nexport const ToastProvider = memo(function ToastProvider({\r\n position = \"top-right\",\r\n maxVisible = 5,\r\n defaultDuration = 5000,\r\n children,\r\n}: ToastProviderProps): React.ReactElement {\r\n const { tokens } = useTheme();\r\n const { isMobile } = useBreakpoint();\r\n const [toasts, setToasts] = useState<ToastInstance[]>([]);\r\n const idCounter = useRef(0);\r\n\r\n const toast = useCallback(\r\n (options: ToastOptions): string => {\r\n const id = `toast-${++idCounter.current}-${Date.now()}`;\r\n const instance: ToastInstance = {\r\n ...options,\r\n id,\r\n createdAt: Date.now(),\r\n duration: options.duration ?? defaultDuration,\r\n };\r\n setToasts((prev) => [...prev, instance].slice(-maxVisible * 2)); // Keep buffer\r\n return id;\r\n },\r\n [defaultDuration, maxVisible],\r\n );\r\n\r\n const dismiss = useCallback((id: string) => {\r\n setToasts((prev) => prev.filter((t) => t.id !== id));\r\n }, []);\r\n\r\n const dismissAll = useCallback(() => {\r\n setToasts([]);\r\n }, []);\r\n\r\n // Position styles\r\n const inset = isMobile ? tokens.spacing.sm : tokens.spacing.md;\r\n const positionStyles: Record<ToastPosition, React.CSSProperties> = {\r\n \"top-right\": { top: inset, right: inset },\r\n \"top-left\": { top: inset, left: inset },\r\n \"bottom-right\": { bottom: inset, right: inset },\r\n \"bottom-left\": { bottom: inset, left: inset },\r\n \"top-center\": { top: inset, left: \"50%\", transform: \"translateX(-50%)\" },\r\n \"bottom-center\": {\r\n bottom: inset,\r\n left: \"50%\",\r\n transform: \"translateX(-50%)\",\r\n },\r\n };\r\n\r\n const isBottom = position.startsWith(\"bottom\");\r\n const visibleToasts = toasts.slice(-maxVisible);\r\n\r\n return (\r\n <ToastContext.Provider value={{ toast, dismiss, dismissAll }}>\r\n {children}\r\n\r\n {/* Toast container */}\r\n {visibleToasts.length > 0 && (\r\n <div\r\n aria-live={\r\n visibleToasts.some(\r\n (t) => t.status === \"critical\" || t.status === \"serious\",\r\n )\r\n ? \"assertive\"\r\n : \"polite\"\r\n }\r\n aria-atomic=\"false\"\r\n style={{\r\n position: \"fixed\",\r\n zIndex: 9999,\r\n display: \"flex\",\r\n flexDirection: isBottom ? \"column-reverse\" : \"column\",\r\n gap: tokens.spacing.sm,\r\n pointerEvents: \"none\",\r\n ...positionStyles[position],\r\n }}\r\n >\r\n {visibleToasts.map((t) => (\r\n <div key={t.id} style={{ pointerEvents: \"auto\" }}>\r\n <ToastItem toast={t} onDismiss={dismiss} />\r\n </div>\r\n ))}\r\n </div>\r\n )}\r\n\r\n <style>{`\r\n @keyframes zendir-toast-enter {\r\n from { opacity: 0; transform: translateX(20px); }\r\n to { opacity: 1; transform: translateX(0); }\r\n }\r\n @keyframes zendir-toast-exit {\r\n from { opacity: 1; transform: translateX(0); }\r\n to { opacity: 0; transform: translateX(20px); }\r\n }\r\n @media (prefers-reduced-motion: reduce) {\r\n @keyframes zendir-toast-enter { from { opacity: 0; } to { opacity: 1; } }\r\n @keyframes zendir-toast-exit { from { opacity: 1; } to { opacity: 0; } }\r\n }\r\n `}</style>\r\n </ToastContext.Provider>\r\n );\r\n});\r\n\r\nexport default ToastProvider;\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.js","names":[],"sources":["../../../../src/react/core/inputs/Button.tsx"],"sourcesContent":["/**\n * @zendir/ui - Button Component\n *\n * Enterprise-grade button following Astro UX Design System with Zendir purple accents.\n *\n * Astro UX Compliance:\n * - Three visual variants: primary, secondary, borderless\n * - Size variants: small, medium, large\n * - Sentence case capitalization\n * - Focus ring for accessibility (WCAG 2.1 AA)\n * - Right-align in button groups (primary on right)\n *\n * Zendir Enhancements:\n * - Purple accent colors\n * - Smooth micro-interactions\n * - Subtle glow effects\n * - Modern glassmorphism hover states\n *\n * @example\n * ```tsx\n * <Button>Primary Action</Button>\n * <Button variant=\"secondary\">Cancel</Button>\n * <Button variant=\"borderless\" icon={<Icon name=\"settings\" />}>Settings</Button>\n * <Button loading>Saving...</Button>\n * ```\n */\n\nimport React, {\n memo,\n forwardRef,\n useState,\n useMemo,\n useRef,\n useLayoutEffect,\n useCallback,\n} from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { useTheme } from \"../../theme\";\nimport { classNames, safeAccentText } from \"../../utils\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"borderless\";\nexport type ButtonSize = \"small\" | \"medium\" | \"large\";\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Visual variant */\n variant?: ButtonVariant;\n /** Size variant */\n size?: ButtonSize;\n /** Icon to display before text */\n icon?: React.ReactNode;\n /** Icon to display after text */\n iconEnd?: React.ReactNode;\n /** Show loading spinner */\n loading?: boolean;\n /** Full width button */\n fullWidth?: boolean;\n /** Tooltip text (shows on hover) */\n tooltip?: string;\n /** Custom className */\n className?: string;\n /** Children */\n children?: React.ReactNode;\n}\n\nexport const Button = memo(\n forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n {\n variant = \"primary\",\n size = \"medium\",\n icon,\n iconEnd,\n loading = false,\n fullWidth = false,\n tooltip,\n disabled,\n className = \"\",\n children,\n style,\n onMouseEnter,\n onMouseLeave,\n onFocus,\n onBlur,\n ...rest\n },\n ref,\n ): React.ReactElement {\n const { tokens, theme } = useTheme();\n\n // Spin keyframe is used by loading spinner only (injected there)\n\n const isTransparentTheme =\n theme === \"transparent\" ||\n theme === \"transparent-bold\" ||\n theme === \"transparent-minimal\";\n const [isHovered, setIsHovered] = useState(false);\n const [isActive, setIsActive] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const [showTooltip, setShowTooltip] = useState(false);\n\n const isDisabled = disabled || loading;\n const transparentDefault = tokens.colors.interactive.transparentDefault;\n const transparentHover = tokens.colors.interactive.transparentHover;\n\n // WCAG AA: compute a button-safe background from accent.primary\n // If white text (#fff) on the accent bg doesn't meet 4.5:1, darken it proportionally.\n const safeAccentBg = useMemo(() => {\n const hex = tokens.colors.accent.primary.replace(\"#\", \"\");\n if (hex.length < 6) return tokens.colors.accent.primary;\n const toLinear = (c: number) =>\n c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n const r = parseInt(hex.slice(0, 2), 16) / 255;\n const g = parseInt(hex.slice(2, 4), 16) / 255;\n const b = parseInt(hex.slice(4, 6), 16) / 255;\n const L =\n 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n if (1.05 / (L + 0.05) >= 4.5) return tokens.colors.accent.primary;\n // Darken just enough: target L for ~4.7:1 contrast with white\n const factor = 0.88;\n const dr = Math.min(255, Math.round(r * 255 * factor));\n const dg = Math.min(255, Math.round(g * 255 * factor));\n const db = Math.min(255, Math.round(b * 255 * factor));\n return `#${dr.toString(16).padStart(2, \"0\")}${dg.toString(16).padStart(2, \"0\")}${db.toString(16).padStart(2, \"0\")}`;\n }, [tokens.colors.accent.primary]);\n\n // WCAG AA safe accent for use as foreground text on dark backgrounds\n const safeAccentFg = useMemo(\n () => safeAccentText(tokens.colors.accent.primary),\n [tokens.colors.accent.primary],\n );\n\n const sizeConfig = {\n small: {\n padding: `${tokens.spacing.xxs} ${tokens.spacing.smd}`,\n fontSize: tokens.typography.fontSize.xs,\n height: \"24px\",\n gap: tokens.spacing.xs,\n iconSize: 13,\n borderRadius: tokens.borderRadius.sm,\n },\n medium: {\n padding: `${tokens.spacing.xxs} ${tokens.spacing.md}`,\n fontSize: tokens.typography.fontSize.sm,\n height: \"28px\",\n gap: tokens.spacing.xs,\n iconSize: 15,\n borderRadius: tokens.borderRadius.md,\n },\n large: {\n padding: `${tokens.spacing.xs} ${tokens.spacing.lg}`,\n fontSize: tokens.typography.fontSize.base,\n height: \"34px\",\n gap: tokens.spacing.sm,\n iconSize: 17,\n borderRadius: tokens.borderRadius.md,\n },\n };\n\n const config = sizeConfig[size];\n\n // Variant base styles (transparent theme: purple-hue transparent bg, smooth fade on hover)\n const getVariantStyles = (): React.CSSProperties => {\n const baseTransition = `${tokens.animation.normal}, opacity ${tokens.animation.duration.normal}ms ${tokens.animation.easing.default}`;\n\n if (isTransparentTheme && transparentDefault && transparentHover) {\n switch (variant) {\n case \"primary\":\n return {\n backgroundColor:\n isHovered && !isDisabled\n ? transparentHover\n : transparentDefault,\n color: tokens.colors.text.primary,\n border: `1px solid ${isHovered && !isDisabled ? `${tokens.colors.accent.primary}80` : `${tokens.colors.accent.primary}40`}`,\n boxShadow:\n isHovered && !isDisabled\n ? `0 0 10px ${tokens.colors.accent.primary}26`\n : \"none\",\n transition: baseTransition,\n backdropFilter: \"blur(8px)\",\n WebkitBackdropFilter: \"blur(8px)\",\n };\n case \"secondary\":\n return {\n backgroundColor:\n isHovered && !isDisabled\n ? `${tokens.colors.accent.primary}15` // Use accent color with opacity instead of transparentHover\n : transparentDefault,\n color: safeAccentFg,\n border: `1px solid ${isHovered && !isDisabled ? `${tokens.colors.accent.primary}70` : `${tokens.colors.accent.primary}30`}`,\n boxShadow: \"none\",\n transition: baseTransition,\n backdropFilter: \"blur(8px)\",\n WebkitBackdropFilter: \"blur(8px)\",\n };\n case \"borderless\":\n return {\n backgroundColor:\n isHovered && !isDisabled\n ? transparentHover\n : transparentDefault,\n color: safeAccentFg,\n border: \"none\",\n boxShadow: \"none\",\n transition: baseTransition,\n backdropFilter: \"blur(6px)\",\n WebkitBackdropFilter: \"blur(6px)\",\n };\n }\n }\n\n switch (variant) {\n case \"primary\":\n return {\n backgroundColor:\n isHovered && !isDisabled\n ? tokens.colors.interactive.hover\n : safeAccentBg,\n color: \"#ffffff\",\n border: \"none\",\n boxShadow:\n isHovered && !isDisabled\n ? `0 2px 8px -2px ${tokens.colors.accent.primary}35`\n : \"none\",\n transition: baseTransition,\n };\n\n case \"secondary\":\n return {\n backgroundColor:\n isHovered && !isDisabled\n ? `${tokens.colors.accent.primary}12`\n : \"transparent\",\n color: isHovered && !isDisabled ? safeAccentFg : safeAccentFg,\n border: `${tokens.borders.width.medium} solid ${isHovered && !isDisabled ? tokens.colors.accent.primary : `${tokens.colors.accent.primary}80`}`,\n boxShadow: \"none\",\n transition: baseTransition,\n };\n\n case \"borderless\":\n return {\n backgroundColor:\n isHovered && !isDisabled\n ? `${tokens.colors.accent.primary}10`\n : \"transparent\",\n color: safeAccentFg,\n border: `${tokens.borders.width.medium} solid transparent`,\n boxShadow: \"none\",\n transition: baseTransition,\n };\n }\n };\n\n const focusStyles: React.CSSProperties = isFocused\n ? {\n outline: \"none\",\n boxShadow: tokens.borders.focusRing.button,\n }\n : {};\n\n const activeStyles: React.CSSProperties =\n isActive && !isDisabled\n ? {\n filter: \"brightness(0.85)\",\n transform: \"scale(0.98)\",\n boxShadow: \"none\",\n }\n : {};\n\n const baseStyle: React.CSSProperties = {\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: config.gap,\n padding: config.padding,\n height: config.height,\n fontSize: config.fontSize,\n fontWeight: 400,\n fontFamily: tokens.typography.fontFamily.primary,\n letterSpacing: \"0.02em\",\n borderRadius: config.borderRadius,\n cursor: isDisabled ? \"not-allowed\" : \"pointer\",\n opacity: isDisabled ? 0.5 : 1,\n outline: \"none\",\n textDecoration: \"none\",\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n width: fullWidth ? \"100%\" : \"auto\",\n position: \"relative\",\n ...getVariantStyles(),\n ...focusStyles,\n ...activeStyles,\n ...style,\n };\n\n // Loading spinner with smooth animation\n const spinner = (\n <svg\n width={config.iconSize}\n height={config.iconSize}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n style={{\n animation: \"zendir-spin 0.8s cubic-bezier(0.4, 0, 0.6, 1) infinite\",\n }}\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeDasharray=\"31.4 31.4\"\n strokeDashoffset=\"10\"\n opacity=\"0.9\"\n />\n <style>\n {`@keyframes zendir-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`}\n </style>\n </svg>\n );\n\n // Portal-based tooltip: position fixed relative to viewport, never clipped\n const internalRef = useRef<HTMLButtonElement>(null);\n const tooltipNodeRef = useRef<HTMLDivElement>(null);\n const [tooltipPos, setTooltipPos] = useState<{\n top: number;\n left: number;\n } | null>(null);\n\n const mergedRef = useCallback(\n (node: HTMLButtonElement | null) => {\n (\n internalRef as React.MutableRefObject<HTMLButtonElement | null>\n ).current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref)\n (ref as React.MutableRefObject<HTMLButtonElement | null>).current =\n node;\n },\n [ref],\n );\n\n useLayoutEffect(() => {\n if (!showTooltip || !tooltip || !internalRef.current) {\n setTooltipPos(null);\n return;\n }\n const btn = internalRef.current.getBoundingClientRect();\n const gap = 8;\n let top = btn.top - gap;\n let left = btn.left + btn.width / 2;\n // Adjust after tooltip renders so we can measure its width\n if (tooltipNodeRef.current) {\n const tt = tooltipNodeRef.current.getBoundingClientRect();\n top = btn.top - tt.height - gap;\n left = btn.left + (btn.width - tt.width) / 2;\n if (left < 8) left = 8;\n if (left + tt.width > window.innerWidth - 8)\n left = window.innerWidth - tt.width - 8;\n if (top < 8) top = btn.bottom + gap; // flip below if no room above\n }\n setTooltipPos({ top, left });\n }, [showTooltip, tooltip]);\n\n const tooltipPortal =\n tooltip &&\n showTooltip &&\n ReactDOM.createPortal(\n <div\n ref={tooltipNodeRef}\n role=\"tooltip\"\n style={{\n position: \"fixed\",\n top: tooltipPos ? tooltipPos.top : -9999,\n left: tooltipPos ? tooltipPos.left : -9999,\n padding: `${tokens.spacing.xs} ${tokens.spacing.smd}`,\n fontSize: \"0.75rem\",\n fontWeight: 500,\n color: tokens.colors.text.primary,\n backgroundColor: tokens.colors.background.elevated,\n border: tokens.borders.divider,\n borderRadius: tokens.borderRadius.md,\n boxShadow: tokens.shadows.md,\n whiteSpace: \"nowrap\",\n zIndex: 2147483100,\n pointerEvents: \"none\",\n opacity: tooltipPos ? 1 : 0,\n animation: tooltipPos\n ? `zendir-btn-tip ${tokens.animation.duration.fast}ms ${tokens.animation.easing.default} both`\n : \"none\",\n }}\n >\n {tooltip}\n <style>\n {`@keyframes zendir-btn-tip { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }`}\n </style>\n </div>,\n document.body,\n );\n\n return (\n <>\n <button\n ref={mergedRef}\n type=\"button\"\n disabled={isDisabled}\n className={classNames(\n \"zendir-button\",\n `zendir-button--${variant}`,\n `zendir-button--${size}`,\n className,\n )}\n style={baseStyle}\n title={tooltip && !showTooltip ? tooltip : undefined}\n onMouseDown={() => setIsActive(true)}\n onMouseUp={() => setIsActive(false)}\n onMouseEnter={(e) => {\n setIsHovered(true);\n setShowTooltip(true);\n onMouseEnter?.(e);\n }}\n onMouseLeave={(e) => {\n setIsHovered(false);\n setIsActive(false);\n setShowTooltip(false);\n onMouseLeave?.(e);\n }}\n onFocus={(e) => {\n setIsFocused(true);\n onFocus?.(e);\n }}\n onBlur={(e) => {\n setIsFocused(false);\n onBlur?.(e);\n }}\n {...rest}\n >\n {loading ? spinner : icon}\n {children && <span>{children}</span>}\n {iconEnd && !loading && iconEnd}\n </button>\n {tooltipPortal}\n </>\n );\n }),\n);\n\nexport default Button;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Button.js","names":[],"sources":["../../../../src/react/core/inputs/Button.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - Button Component\r\n *\r\n * Enterprise-grade button following Astro UX Design System with Zendir purple accents.\r\n *\r\n * Astro UX Compliance:\r\n * - Three visual variants: primary, secondary, borderless\r\n * - Size variants: small, medium, large\r\n * - Sentence case capitalization\r\n * - Focus ring for accessibility (WCAG 2.1 AA)\r\n * - Right-align in button groups (primary on right)\r\n *\r\n * Zendir Enhancements:\r\n * - Purple accent colors\r\n * - Smooth micro-interactions\r\n * - Subtle glow effects\r\n * - Modern glassmorphism hover states\r\n *\r\n * @example\r\n * ```tsx\r\n * <Button>Primary Action</Button>\r\n * <Button variant=\"secondary\">Cancel</Button>\r\n * <Button variant=\"borderless\" icon={<Icon name=\"settings\" />}>Settings</Button>\r\n * <Button loading>Saving...</Button>\r\n * ```\r\n */\r\n\r\nimport React, {\r\n memo,\r\n forwardRef,\r\n useState,\r\n useMemo,\r\n useRef,\r\n useLayoutEffect,\r\n useCallback,\r\n} from \"react\";\r\nimport ReactDOM from \"react-dom\";\r\nimport { useTheme } from \"../../theme\";\r\nimport { classNames, safeAccentText } from \"../../utils\";\r\n\r\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"borderless\";\r\nexport type ButtonSize = \"small\" | \"medium\" | \"large\";\r\n\r\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\r\n /** Visual variant */\r\n variant?: ButtonVariant;\r\n /** Size variant */\r\n size?: ButtonSize;\r\n /** Icon to display before text */\r\n icon?: React.ReactNode;\r\n /** Icon to display after text */\r\n iconEnd?: React.ReactNode;\r\n /** Show loading spinner */\r\n loading?: boolean;\r\n /** Full width button */\r\n fullWidth?: boolean;\r\n /** Tooltip text (shows on hover) */\r\n tooltip?: string;\r\n /** Custom className */\r\n className?: string;\r\n /** Children */\r\n children?: React.ReactNode;\r\n}\r\n\r\nexport const Button = memo(\r\n forwardRef<HTMLButtonElement, ButtonProps>(function Button(\r\n {\r\n variant = \"primary\",\r\n size = \"medium\",\r\n icon,\r\n iconEnd,\r\n loading = false,\r\n fullWidth = false,\r\n tooltip,\r\n disabled,\r\n className = \"\",\r\n children,\r\n style,\r\n onMouseEnter,\r\n onMouseLeave,\r\n onFocus,\r\n onBlur,\r\n ...rest\r\n },\r\n ref,\r\n ): React.ReactElement {\r\n const { tokens, theme } = useTheme();\r\n\r\n // Spin keyframe is used by loading spinner only (injected there)\r\n\r\n const isTransparentTheme =\r\n theme === \"transparent\" ||\r\n theme === \"transparent-bold\" ||\r\n theme === \"transparent-minimal\";\r\n const [isHovered, setIsHovered] = useState(false);\r\n const [isActive, setIsActive] = useState(false);\r\n const [isFocused, setIsFocused] = useState(false);\r\n const [showTooltip, setShowTooltip] = useState(false);\r\n\r\n const isDisabled = disabled || loading;\r\n const transparentDefault = tokens.colors.interactive.transparentDefault;\r\n const transparentHover = tokens.colors.interactive.transparentHover;\r\n\r\n // WCAG AA: compute a button-safe background from accent.primary\r\n // If white text (#fff) on the accent bg doesn't meet 4.5:1, darken it proportionally.\r\n const safeAccentBg = useMemo(() => {\r\n const hex = tokens.colors.accent.primary.replace(\"#\", \"\");\r\n if (hex.length < 6) return tokens.colors.accent.primary;\r\n const toLinear = (c: number) =>\r\n c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\r\n const r = parseInt(hex.slice(0, 2), 16) / 255;\r\n const g = parseInt(hex.slice(2, 4), 16) / 255;\r\n const b = parseInt(hex.slice(4, 6), 16) / 255;\r\n const L =\r\n 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\r\n if (1.05 / (L + 0.05) >= 4.5) return tokens.colors.accent.primary;\r\n // Darken just enough: target L for ~4.7:1 contrast with white\r\n const factor = 0.88;\r\n const dr = Math.min(255, Math.round(r * 255 * factor));\r\n const dg = Math.min(255, Math.round(g * 255 * factor));\r\n const db = Math.min(255, Math.round(b * 255 * factor));\r\n return `#${dr.toString(16).padStart(2, \"0\")}${dg.toString(16).padStart(2, \"0\")}${db.toString(16).padStart(2, \"0\")}`;\r\n }, [tokens.colors.accent.primary]);\r\n\r\n // WCAG AA safe accent for use as foreground text on dark backgrounds\r\n const safeAccentFg = useMemo(\r\n () => safeAccentText(tokens.colors.accent.primary),\r\n [tokens.colors.accent.primary],\r\n );\r\n\r\n const sizeConfig = {\r\n small: {\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.smd}`,\r\n fontSize: tokens.typography.fontSize.xs,\r\n height: \"24px\",\r\n gap: tokens.spacing.xs,\r\n iconSize: 13,\r\n borderRadius: tokens.borderRadius.sm,\r\n },\r\n medium: {\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.md}`,\r\n fontSize: tokens.typography.fontSize.sm,\r\n height: \"28px\",\r\n gap: tokens.spacing.xs,\r\n iconSize: 15,\r\n borderRadius: tokens.borderRadius.md,\r\n },\r\n large: {\r\n padding: `${tokens.spacing.xs} ${tokens.spacing.lg}`,\r\n fontSize: tokens.typography.fontSize.base,\r\n height: \"34px\",\r\n gap: tokens.spacing.sm,\r\n iconSize: 17,\r\n borderRadius: tokens.borderRadius.md,\r\n },\r\n };\r\n\r\n const config = sizeConfig[size];\r\n\r\n // Variant base styles (transparent theme: purple-hue transparent bg, smooth fade on hover)\r\n const getVariantStyles = (): React.CSSProperties => {\r\n const baseTransition = `${tokens.animation.normal}, opacity ${tokens.animation.duration.normal}ms ${tokens.animation.easing.default}`;\r\n\r\n if (isTransparentTheme && transparentDefault && transparentHover) {\r\n switch (variant) {\r\n case \"primary\":\r\n return {\r\n backgroundColor:\r\n isHovered && !isDisabled\r\n ? transparentHover\r\n : transparentDefault,\r\n color: tokens.colors.text.primary,\r\n border: `1px solid ${isHovered && !isDisabled ? `${tokens.colors.accent.primary}80` : `${tokens.colors.accent.primary}40`}`,\r\n boxShadow:\r\n isHovered && !isDisabled\r\n ? `0 0 10px ${tokens.colors.accent.primary}26`\r\n : \"none\",\r\n transition: baseTransition,\r\n backdropFilter: \"blur(8px)\",\r\n WebkitBackdropFilter: \"blur(8px)\",\r\n };\r\n case \"secondary\":\r\n return {\r\n backgroundColor:\r\n isHovered && !isDisabled\r\n ? `${tokens.colors.accent.primary}15` // Use accent color with opacity instead of transparentHover\r\n : transparentDefault,\r\n color: safeAccentFg,\r\n border: `1px solid ${isHovered && !isDisabled ? `${tokens.colors.accent.primary}70` : `${tokens.colors.accent.primary}30`}`,\r\n boxShadow: \"none\",\r\n transition: baseTransition,\r\n backdropFilter: \"blur(8px)\",\r\n WebkitBackdropFilter: \"blur(8px)\",\r\n };\r\n case \"borderless\":\r\n return {\r\n backgroundColor:\r\n isHovered && !isDisabled\r\n ? transparentHover\r\n : transparentDefault,\r\n color: safeAccentFg,\r\n border: \"none\",\r\n boxShadow: \"none\",\r\n transition: baseTransition,\r\n backdropFilter: \"blur(6px)\",\r\n WebkitBackdropFilter: \"blur(6px)\",\r\n };\r\n }\r\n }\r\n\r\n switch (variant) {\r\n case \"primary\":\r\n return {\r\n backgroundColor:\r\n isHovered && !isDisabled\r\n ? tokens.colors.interactive.hover\r\n : safeAccentBg,\r\n color: \"#ffffff\",\r\n border: \"none\",\r\n boxShadow:\r\n isHovered && !isDisabled\r\n ? `0 2px 8px -2px ${tokens.colors.accent.primary}35`\r\n : \"none\",\r\n transition: baseTransition,\r\n };\r\n\r\n case \"secondary\":\r\n return {\r\n backgroundColor:\r\n isHovered && !isDisabled\r\n ? `${tokens.colors.accent.primary}12`\r\n : \"transparent\",\r\n color: isHovered && !isDisabled ? safeAccentFg : safeAccentFg,\r\n border: `${tokens.borders.width.medium} solid ${isHovered && !isDisabled ? tokens.colors.accent.primary : `${tokens.colors.accent.primary}80`}`,\r\n boxShadow: \"none\",\r\n transition: baseTransition,\r\n };\r\n\r\n case \"borderless\":\r\n return {\r\n backgroundColor:\r\n isHovered && !isDisabled\r\n ? `${tokens.colors.accent.primary}10`\r\n : \"transparent\",\r\n color: safeAccentFg,\r\n border: `${tokens.borders.width.medium} solid transparent`,\r\n boxShadow: \"none\",\r\n transition: baseTransition,\r\n };\r\n }\r\n };\r\n\r\n const focusStyles: React.CSSProperties = isFocused\r\n ? {\r\n outline: \"none\",\r\n boxShadow: tokens.borders.focusRing.button,\r\n }\r\n : {};\r\n\r\n const activeStyles: React.CSSProperties =\r\n isActive && !isDisabled\r\n ? {\r\n filter: \"brightness(0.85)\",\r\n transform: \"scale(0.98)\",\r\n boxShadow: \"none\",\r\n }\r\n : {};\r\n\r\n const baseStyle: React.CSSProperties = {\r\n display: \"inline-flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n gap: config.gap,\r\n padding: config.padding,\r\n height: config.height,\r\n fontSize: config.fontSize,\r\n fontWeight: 400,\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n letterSpacing: \"0.02em\",\r\n borderRadius: config.borderRadius,\r\n cursor: isDisabled ? \"not-allowed\" : \"pointer\",\r\n opacity: isDisabled ? 0.5 : 1,\r\n outline: \"none\",\r\n textDecoration: \"none\",\r\n whiteSpace: \"nowrap\",\r\n userSelect: \"none\",\r\n width: fullWidth ? \"100%\" : \"auto\",\r\n position: \"relative\",\r\n ...getVariantStyles(),\r\n ...focusStyles,\r\n ...activeStyles,\r\n ...style,\r\n };\r\n\r\n // Loading spinner with smooth animation\r\n const spinner = (\r\n <svg\r\n width={config.iconSize}\r\n height={config.iconSize}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n style={{\r\n animation: \"zendir-spin 0.8s cubic-bezier(0.4, 0, 0.6, 1) infinite\",\r\n }}\r\n >\r\n <circle\r\n cx=\"12\"\r\n cy=\"12\"\r\n r=\"10\"\r\n stroke=\"currentColor\"\r\n strokeWidth=\"2.5\"\r\n strokeLinecap=\"round\"\r\n strokeDasharray=\"31.4 31.4\"\r\n strokeDashoffset=\"10\"\r\n opacity=\"0.9\"\r\n />\r\n <style>\r\n {`@keyframes zendir-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`}\r\n </style>\r\n </svg>\r\n );\r\n\r\n // Portal-based tooltip: position fixed relative to viewport, never clipped\r\n const internalRef = useRef<HTMLButtonElement>(null);\r\n const tooltipNodeRef = useRef<HTMLDivElement>(null);\r\n const [tooltipPos, setTooltipPos] = useState<{\r\n top: number;\r\n left: number;\r\n } | null>(null);\r\n\r\n const mergedRef = useCallback(\r\n (node: HTMLButtonElement | null) => {\r\n (\r\n internalRef as React.MutableRefObject<HTMLButtonElement | null>\r\n ).current = node;\r\n if (typeof ref === \"function\") ref(node);\r\n else if (ref)\r\n (ref as React.MutableRefObject<HTMLButtonElement | null>).current =\r\n node;\r\n },\r\n [ref],\r\n );\r\n\r\n useLayoutEffect(() => {\r\n if (!showTooltip || !tooltip || !internalRef.current) {\r\n setTooltipPos(null);\r\n return;\r\n }\r\n const btn = internalRef.current.getBoundingClientRect();\r\n const gap = 8;\r\n let top = btn.top - gap;\r\n let left = btn.left + btn.width / 2;\r\n // Adjust after tooltip renders so we can measure its width\r\n if (tooltipNodeRef.current) {\r\n const tt = tooltipNodeRef.current.getBoundingClientRect();\r\n top = btn.top - tt.height - gap;\r\n left = btn.left + (btn.width - tt.width) / 2;\r\n if (left < 8) left = 8;\r\n if (left + tt.width > window.innerWidth - 8)\r\n left = window.innerWidth - tt.width - 8;\r\n if (top < 8) top = btn.bottom + gap; // flip below if no room above\r\n }\r\n setTooltipPos({ top, left });\r\n }, [showTooltip, tooltip]);\r\n\r\n const tooltipPortal =\r\n tooltip &&\r\n showTooltip &&\r\n ReactDOM.createPortal(\r\n <div\r\n ref={tooltipNodeRef}\r\n role=\"tooltip\"\r\n style={{\r\n position: \"fixed\",\r\n top: tooltipPos ? tooltipPos.top : -9999,\r\n left: tooltipPos ? tooltipPos.left : -9999,\r\n padding: `${tokens.spacing.xs} ${tokens.spacing.smd}`,\r\n fontSize: \"0.75rem\",\r\n fontWeight: 500,\r\n color: tokens.colors.text.primary,\r\n backgroundColor: tokens.colors.background.elevated,\r\n border: tokens.borders.divider,\r\n borderRadius: tokens.borderRadius.md,\r\n boxShadow: tokens.shadows.md,\r\n whiteSpace: \"nowrap\",\r\n zIndex: 2147483100,\r\n pointerEvents: \"none\",\r\n opacity: tooltipPos ? 1 : 0,\r\n animation: tooltipPos\r\n ? `zendir-btn-tip ${tokens.animation.duration.fast}ms ${tokens.animation.easing.default} both`\r\n : \"none\",\r\n }}\r\n >\r\n {tooltip}\r\n <style>\r\n {`@keyframes zendir-btn-tip { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }`}\r\n </style>\r\n </div>,\r\n document.body,\r\n );\r\n\r\n return (\r\n <>\r\n <button\r\n ref={mergedRef}\r\n type=\"button\"\r\n disabled={isDisabled}\r\n className={classNames(\r\n \"zendir-button\",\r\n `zendir-button--${variant}`,\r\n `zendir-button--${size}`,\r\n className,\r\n )}\r\n style={baseStyle}\r\n title={tooltip && !showTooltip ? tooltip : undefined}\r\n onMouseDown={() => setIsActive(true)}\r\n onMouseUp={() => setIsActive(false)}\r\n onMouseEnter={(e) => {\r\n setIsHovered(true);\r\n setShowTooltip(true);\r\n onMouseEnter?.(e);\r\n }}\r\n onMouseLeave={(e) => {\r\n setIsHovered(false);\r\n setIsActive(false);\r\n setShowTooltip(false);\r\n onMouseLeave?.(e);\r\n }}\r\n onFocus={(e) => {\r\n setIsFocused(true);\r\n onFocus?.(e);\r\n }}\r\n onBlur={(e) => {\r\n setIsFocused(false);\r\n onBlur?.(e);\r\n }}\r\n {...rest}\r\n >\r\n {loading ? spinner : icon}\r\n {children && <span>{children}</span>}\r\n {iconEnd && !loading && iconEnd}\r\n </button>\r\n {tooltipPortal}\r\n </>\r\n );\r\n }),\r\n);\r\n\r\nexport default Button;\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Checkbox.js","names":[],"sources":["../../../../src/react/core/inputs/Checkbox.tsx"],"sourcesContent":["/**\n * @zendir/ui - Checkbox Component\n *\n * Checkbox and Radio button following Astro UX Design System with Zendir purple accents.\n *\n * Astro UX Compliance:\n * - Clear visual states\n * - Accessible keyboard support\n * - Indeterminate state for checkbox\n *\n * Zendir Enhancements:\n * - Purple accent when checked\n * - Glow effect on focus\n * - Smooth spring animation\n *\n * @example\n * ```tsx\n * <Checkbox checked={agreed} onChange={setAgreed} label=\"I agree to terms\" />\n * <Checkbox type=\"radio\" name=\"priority\" value=\"high\" label=\"High Priority\" />\n * ```\n */\n\nimport React, { memo, forwardRef, useId, useState } from \"react\";\nimport { useTheme } from \"../../theme\";\nimport { classNames } from \"../../utils\";\n\nexport interface CheckboxProps {\n /** Checkbox or radio */\n type?: \"checkbox\" | \"radio\";\n /** Checked state */\n checked?: boolean;\n /** Change handler */\n onChange?: (checked: boolean) => void;\n /** Label text */\n label?: string;\n /** Value (for radio groups) */\n value?: string;\n /** Name (for radio groups) */\n name?: string;\n /** Disabled state */\n disabled?: boolean;\n /** Indeterminate state (checkbox only) */\n indeterminate?: boolean;\n /** Size variant */\n size?: \"small\" | \"medium\";\n /** Custom className */\n className?: string;\n}\n\nexport const Checkbox = memo(\n forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\n {\n type = \"checkbox\",\n checked = false,\n onChange,\n label,\n value,\n name,\n disabled = false,\n indeterminate = false,\n size = \"medium\",\n className = \"\",\n },\n ref,\n ): React.ReactElement {\n const { tokens, theme } = useTheme();\n const isTransparentTheme =\n theme === \"transparent\" ||\n theme === \"transparent-bold\" ||\n theme === \"transparent-minimal\";\n const transparentDefault = tokens.colors.interactive.transparentDefault;\n const [focused, setFocused] = useState(false);\n const [hovered, setHovered] = useState(false);\n const id = useId();\n\n // WCAG 2.5.8: minimum 24×24 CSS px target size\n const sizeConfig = {\n small: { boxSize: 18, iconSize: 12, targetSize: 24 },\n medium: { boxSize: 20, iconSize: 14, targetSize: 24 },\n };\n const config = sizeConfig[size];\n\n const isRadio = type === \"radio\";\n const borderRadius = isRadio ? \"50%\" : tokens.borderRadius.md;\n\n const boxStyle: React.CSSProperties = {\n width: config.boxSize,\n height: config.boxSize,\n border: `${tokens.borders.width.thick} solid ${checked ? tokens.colors.accent.primary : hovered && !disabled ? `${tokens.colors.accent.primary}60` : tokens.colors.border.muted}`,\n borderRadius,\n backgroundColor: checked\n ? tokens.colors.accent.primary\n : isTransparentTheme && transparentDefault\n ? transparentDefault\n : \"transparent\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n transition: `${tokens.animation.normal}, opacity ${tokens.animation.duration.normal}ms ${tokens.animation.easing.default}`,\n flexShrink: 0,\n boxShadow: checked\n ? `0 0 12px ${tokens.colors.accent.primary}40`\n : focused\n ? tokens.borders.focusRing.subtle\n : \"none\",\n transform: checked\n ? \"scale(1)\"\n : hovered && !disabled\n ? \"scale(1.05)\"\n : \"scale(1)\",\n };\n\n // Check mark for checkbox\n const checkIcon = (\n <svg\n width={config.iconSize}\n height={config.iconSize}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"#ffffff\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n {indeterminate ? (\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n ) : (\n <polyline points=\"20 6 9 17 4 12\" />\n )}\n </svg>\n );\n\n // Dot for radio\n const radioIcon = (\n <div\n style={{\n width: config.iconSize - 4,\n height: config.iconSize - 4,\n borderRadius: \"50%\",\n backgroundColor: \"#ffffff\",\n }}\n />\n );\n\n return (\n <label\n htmlFor={id}\n className={classNames(\"zendir-checkbox\", className)}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n style={{\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: tokens.spacing.sm,\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n opacity: disabled ? 0.5 : 1,\n }}\n >\n {/* Touch target wrapper — ensures WCAG 2.5.8 24px minimum */}\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minWidth: config.targetSize,\n minHeight: config.targetSize,\n }}\n >\n <div style={boxStyle}>\n {checked && (isRadio ? radioIcon : checkIcon)}\n </div>\n </div>\n\n <input\n ref={ref}\n type={type}\n id={id}\n name={name}\n value={value}\n checked={checked}\n disabled={disabled}\n onChange={(e) => onChange?.(e.target.checked)}\n onFocus={() => setFocused(true)}\n onBlur={() => setFocused(false)}\n style={{\n position: \"absolute\",\n opacity: 0,\n width: 0,\n height: 0,\n }}\n />\n\n {label && (\n <span\n style={{\n fontSize: \"0.875rem\",\n color: tokens.colors.text.primary,\n transition: tokens.animation.fast,\n }}\n >\n {label}\n </span>\n )}\n </label>\n );\n }),\n);\n\nexport default Checkbox;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Checkbox.js","names":[],"sources":["../../../../src/react/core/inputs/Checkbox.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - Checkbox Component\r\n *\r\n * Checkbox and Radio button following Astro UX Design System with Zendir purple accents.\r\n *\r\n * Astro UX Compliance:\r\n * - Clear visual states\r\n * - Accessible keyboard support\r\n * - Indeterminate state for checkbox\r\n *\r\n * Zendir Enhancements:\r\n * - Purple accent when checked\r\n * - Glow effect on focus\r\n * - Smooth spring animation\r\n *\r\n * @example\r\n * ```tsx\r\n * <Checkbox checked={agreed} onChange={setAgreed} label=\"I agree to terms\" />\r\n * <Checkbox type=\"radio\" name=\"priority\" value=\"high\" label=\"High Priority\" />\r\n * ```\r\n */\r\n\r\nimport React, { memo, forwardRef, useId, useState } from \"react\";\r\nimport { useTheme } from \"../../theme\";\r\nimport { classNames } from \"../../utils\";\r\n\r\nexport interface CheckboxProps {\r\n /** Checkbox or radio */\r\n type?: \"checkbox\" | \"radio\";\r\n /** Checked state */\r\n checked?: boolean;\r\n /** Change handler */\r\n onChange?: (checked: boolean) => void;\r\n /** Label text */\r\n label?: string;\r\n /** Value (for radio groups) */\r\n value?: string;\r\n /** Name (for radio groups) */\r\n name?: string;\r\n /** Disabled state */\r\n disabled?: boolean;\r\n /** Indeterminate state (checkbox only) */\r\n indeterminate?: boolean;\r\n /** Size variant */\r\n size?: \"small\" | \"medium\";\r\n /** Custom className */\r\n className?: string;\r\n}\r\n\r\nexport const Checkbox = memo(\r\n forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\r\n {\r\n type = \"checkbox\",\r\n checked = false,\r\n onChange,\r\n label,\r\n value,\r\n name,\r\n disabled = false,\r\n indeterminate = false,\r\n size = \"medium\",\r\n className = \"\",\r\n },\r\n ref,\r\n ): React.ReactElement {\r\n const { tokens, theme } = useTheme();\r\n const isTransparentTheme =\r\n theme === \"transparent\" ||\r\n theme === \"transparent-bold\" ||\r\n theme === \"transparent-minimal\";\r\n const transparentDefault = tokens.colors.interactive.transparentDefault;\r\n const [focused, setFocused] = useState(false);\r\n const [hovered, setHovered] = useState(false);\r\n const id = useId();\r\n\r\n // WCAG 2.5.8: minimum 24×24 CSS px target size\r\n const sizeConfig = {\r\n small: { boxSize: 18, iconSize: 12, targetSize: 24 },\r\n medium: { boxSize: 20, iconSize: 14, targetSize: 24 },\r\n };\r\n const config = sizeConfig[size];\r\n\r\n const isRadio = type === \"radio\";\r\n const borderRadius = isRadio ? \"50%\" : tokens.borderRadius.md;\r\n\r\n const boxStyle: React.CSSProperties = {\r\n width: config.boxSize,\r\n height: config.boxSize,\r\n border: `${tokens.borders.width.thick} solid ${checked ? tokens.colors.accent.primary : hovered && !disabled ? `${tokens.colors.accent.primary}60` : tokens.colors.border.muted}`,\r\n borderRadius,\r\n backgroundColor: checked\r\n ? tokens.colors.accent.primary\r\n : isTransparentTheme && transparentDefault\r\n ? transparentDefault\r\n : \"transparent\",\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n transition: `${tokens.animation.normal}, opacity ${tokens.animation.duration.normal}ms ${tokens.animation.easing.default}`,\r\n flexShrink: 0,\r\n boxShadow: checked\r\n ? `0 0 12px ${tokens.colors.accent.primary}40`\r\n : focused\r\n ? tokens.borders.focusRing.subtle\r\n : \"none\",\r\n transform: checked\r\n ? \"scale(1)\"\r\n : hovered && !disabled\r\n ? \"scale(1.05)\"\r\n : \"scale(1)\",\r\n };\r\n\r\n // Check mark for checkbox\r\n const checkIcon = (\r\n <svg\r\n width={config.iconSize}\r\n height={config.iconSize}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"#ffffff\"\r\n strokeWidth=\"3\"\r\n strokeLinecap=\"round\"\r\n strokeLinejoin=\"round\"\r\n >\r\n {indeterminate ? (\r\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\r\n ) : (\r\n <polyline points=\"20 6 9 17 4 12\" />\r\n )}\r\n </svg>\r\n );\r\n\r\n // Dot for radio\r\n const radioIcon = (\r\n <div\r\n style={{\r\n width: config.iconSize - 4,\r\n height: config.iconSize - 4,\r\n borderRadius: \"50%\",\r\n backgroundColor: \"#ffffff\",\r\n }}\r\n />\r\n );\r\n\r\n return (\r\n <label\r\n htmlFor={id}\r\n className={classNames(\"zendir-checkbox\", className)}\r\n onMouseEnter={() => setHovered(true)}\r\n onMouseLeave={() => setHovered(false)}\r\n style={{\r\n display: \"inline-flex\",\r\n alignItems: \"center\",\r\n gap: tokens.spacing.sm,\r\n cursor: disabled ? \"not-allowed\" : \"pointer\",\r\n opacity: disabled ? 0.5 : 1,\r\n }}\r\n >\r\n {/* Touch target wrapper — ensures WCAG 2.5.8 24px minimum */}\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n minWidth: config.targetSize,\r\n minHeight: config.targetSize,\r\n }}\r\n >\r\n <div style={boxStyle}>\r\n {checked && (isRadio ? radioIcon : checkIcon)}\r\n </div>\r\n </div>\r\n\r\n <input\r\n ref={ref}\r\n type={type}\r\n id={id}\r\n name={name}\r\n value={value}\r\n checked={checked}\r\n disabled={disabled}\r\n onChange={(e) => onChange?.(e.target.checked)}\r\n onFocus={() => setFocused(true)}\r\n onBlur={() => setFocused(false)}\r\n style={{\r\n position: \"absolute\",\r\n opacity: 0,\r\n width: 0,\r\n height: 0,\r\n }}\r\n />\r\n\r\n {label && (\r\n <span\r\n style={{\r\n fontSize: \"0.875rem\",\r\n color: tokens.colors.text.primary,\r\n transition: tokens.animation.fast,\r\n }}\r\n >\r\n {label}\r\n </span>\r\n )}\r\n </label>\r\n );\r\n }),\r\n);\r\n\r\nexport default Checkbox;\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.js","names":[],"sources":["../../../../src/react/core/inputs/Input.tsx"],"sourcesContent":["/**\n * @zendir/ui - Input Component\n *\n * Text input field following Astro UX Design System with Zendir purple accents.\n *\n * Astro UX Compliance:\n * - Stacked inputs left-justified\n * - Consistent spacing\n * - Help text for validation (not placeholder)\n * - Required/optional indicators\n * - States: default, hover, focus, invalid, disabled\n *\n * Zendir Enhancements:\n * - Purple focus glow\n * - Smooth transitions\n * - Modern glassmorphism effects\n *\n * @example\n * ```tsx\n * <Input label=\"Spacecraft ID\" placeholder=\"Enter ID...\" />\n * <Input label=\"Password\" type=\"password\" />\n * <Input label=\"Search\" icon={<Icon name=\"search\" />} />\n * <Input label=\"Email\" error=\"Invalid email format\" />\n * ```\n */\n\nimport React, { memo, forwardRef, useState, useId } from \"react\";\nimport { useTheme } from \"../../theme\";\nimport { classNames, safeAccentText, type StatusLevel } from \"../../utils\";\nimport { useBreakpoint } from \"../layout/useBreakpoint\";\n\nexport type InputSize = \"small\" | \"medium\" | \"large\";\n\n/** Label placement relative to the input field */\nexport type LabelPlacement = \"above\" | \"outlined\";\n\nexport interface InputProps extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"size\"\n> {\n /** Label text or ReactNode (supports rich content like tooltip icons) */\n label?: React.ReactNode;\n /** Helper text below input */\n helperText?: string;\n /** Error message (shows error state) */\n error?: string;\n /** Size variant */\n size?: InputSize;\n /** Status for styling */\n status?: StatusLevel;\n /** Icon at start of input */\n icon?: React.ReactNode;\n /** Icon at end of input */\n iconEnd?: React.ReactNode;\n /** Full width */\n fullWidth?: boolean;\n /** Required indicator */\n required?: boolean;\n /** Optional indicator (shows \"optional\" text) */\n optional?: boolean;\n /** Custom className */\n className?: string;\n /**\n * Label placement style.\n * - `'outlined'` — label sits on the input border (notched/Material-style). **Default.**\n * - `'above'` — label sits above the input with a gap (classic stack).\n */\n labelPlacement?: LabelPlacement;\n}\n\nexport const Input = memo(\n forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n label,\n helperText,\n error,\n size = \"medium\",\n status,\n icon,\n iconEnd,\n fullWidth = false,\n required,\n optional,\n disabled,\n className = \"\",\n id: propId,\n style,\n onFocus,\n onBlur,\n labelPlacement = \"outlined\",\n type = \"text\",\n autoComplete,\n inputMode,\n onPaste: onPasteUser,\n ...rest\n },\n ref,\n ): React.ReactElement {\n // Mobile: email keyboard when type is email (industry standard).\n const resolvedInputMode =\n inputMode ?? (type === \"email\" ? \"email\" : undefined);\n const { tokens, theme } = useTheme();\n const { isMobile } = useBreakpoint();\n const [focused, setFocused] = useState(false);\n const [hovered, setHovered] = useState(false);\n const generatedId = useId();\n const inputId = propId ?? generatedId;\n\n // Trim pasted content for password, email, and username (industry standard: avoid auth/validation failures).\n const shouldTrimOnPaste =\n type === \"password\" ||\n type === \"email\" ||\n autoComplete === \"email\" ||\n autoComplete === \"username\";\n const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {\n if (shouldTrimOnPaste) {\n const pasted = e.clipboardData.getData(\"text\");\n const trimmed = pasted.trim();\n if (trimmed !== pasted) {\n e.preventDefault();\n const input = e.currentTarget;\n const start = input.selectionStart ?? 0;\n const end = input.selectionEnd ?? 0;\n const cur = (rest as { value?: string }).value ?? input.value ?? \"\";\n const newValue = cur.slice(0, start) + trimmed + cur.slice(end);\n rest.onChange?.({\n target: { ...input, value: newValue },\n } as React.ChangeEvent<HTMLInputElement>);\n requestAnimationFrame(() => {\n input.setSelectionRange(\n start + trimmed.length,\n start + trimmed.length,\n );\n });\n }\n }\n onPasteUser?.(e);\n };\n\n // Trim on blur for email/username so \" user@example.com \" becomes \"user@example.com\" when leaving the field.\n const shouldTrimOnBlur =\n type === \"email\" ||\n autoComplete === \"email\" ||\n autoComplete === \"username\";\n const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {\n if (shouldTrimOnBlur) {\n const input = e.currentTarget;\n const cur = input.value;\n const trimmed = cur.trim();\n if (trimmed !== cur && rest.onChange) {\n rest.onChange({\n target: { ...input, value: trimmed },\n } as React.ChangeEvent<HTMLInputElement>);\n }\n }\n setFocused(false);\n onBlur?.(e);\n };\n\n // Transparent theme detection (purple-hue transparent bg)\n const isTransparentTheme =\n theme === \"transparent\" ||\n theme === \"transparent-bold\" ||\n theme === \"transparent-minimal\";\n const _transparentInputBg = tokens.colors.interactive.transparentInputBg;\n\n const hasError = !!error;\n const isOutlined = labelPlacement === \"outlined\";\n\n // Determine border color based on state\n const getBorder = () => {\n if (hasError) return tokens.borders.input.error;\n if (status)\n return `${tokens.borders.width.thin} solid ${tokens.colors.status[status]}`;\n if (focused) return tokens.borders.input.focus;\n if (hovered && !disabled) return tokens.borders.input.hover;\n return tokens.borders.input.default;\n };\n\n const sizeConfig = {\n small: {\n padding: `${tokens.spacing.xs} ${tokens.spacing.sm}`,\n fontSize: tokens.typography.fontSize.xs,\n height: tokens.elementSize.sm,\n iconSize: 14,\n labelSize: \"0.6875rem\",\n outlinedLabelSize: \"0.625rem\",\n inputPaddingLeft: 10,\n },\n medium: {\n padding: `${tokens.spacing.sm} ${tokens.spacing.smd}`,\n fontSize: tokens.typography.fontSize.sm,\n height: tokens.elementSize.md,\n iconSize: 18,\n labelSize: \"0.75rem\",\n outlinedLabelSize: \"0.6875rem\",\n inputPaddingLeft: 12,\n },\n large: {\n padding: `${tokens.spacing.smd} ${tokens.spacing.md}`,\n fontSize: tokens.typography.fontSize.base,\n height: tokens.elementSize.lg,\n iconSize: 20,\n labelSize: \"0.875rem\",\n outlinedLabelSize: \"0.75rem\",\n inputPaddingLeft: 16,\n },\n };\n\n const config = sizeConfig[size];\n const controlHeight = isMobile ? \"44px\" : config.height;\n\n // Focus glow effect\n const focusGlow =\n focused && !hasError\n ? tokens.borders.focusRing.default\n : hasError && focused\n ? `0 0 0 3px ${tokens.colors.status.critical}20`\n : \"none\";\n\n // Background for the outlined label \"notch\" (must be opaque to mask the border)\n const labelBg = isTransparentTheme\n ? tokens.colors.background.base\n : tokens.colors.background.surface;\n\n // Shared label content (text + required/optional indicators)\n const labelContent = (\n <>\n {label}\n {required && (\n <span\n style={{ color: tokens.colors.status.critical, marginLeft: \"2px\" }}\n >\n *\n </span>\n )}\n {optional && (\n <span\n style={{\n color: tokens.colors.text.tertiary,\n fontWeight: 400,\n fontStyle: \"italic\",\n }}\n >\n {\" \"}\n (optional)\n </span>\n )}\n </>\n );\n\n return (\n <div\n className={classNames(\"zendir-input-wrapper\", className)}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: tokens.spacing.xs,\n width: fullWidth ? \"100%\" : \"auto\",\n minWidth: fullWidth ? undefined : isMobile ? \"100%\" : \"200px\",\n ...style,\n }}\n >\n {/* Label: 'above' variant — sits above the input with gap */}\n {label && !isOutlined && (\n <label\n htmlFor={inputId}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: tokens.spacing.xs,\n fontSize: config.labelSize,\n fontWeight: 500,\n color: tokens.colors.text.secondary,\n letterSpacing: \"0.01em\",\n }}\n >\n {labelContent}\n </label>\n )}\n\n <div\n style={{\n position: \"relative\",\n display: \"flex\",\n alignItems: \"center\",\n }}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n >\n {/* Label: 'outlined' variant — positioned on the input border */}\n {label && isOutlined && (\n <label\n htmlFor={inputId}\n style={{\n position: \"absolute\",\n top: 0,\n left: icon ? \"34px\" : `${config.inputPaddingLeft - 6}px`,\n transform: \"translateY(-50%)\",\n backgroundColor: labelBg,\n padding: `0 ${tokens.spacing.xs}`,\n fontSize: config.outlinedLabelSize,\n fontWeight: 500,\n color: focused\n ? hasError\n ? tokens.colors.status.critical\n : tokens.colors.accent.primary\n : hasError\n ? tokens.colors.status.critical\n : tokens.colors.text.secondary,\n letterSpacing: \"0.01em\",\n zIndex: 1,\n pointerEvents: \"none\",\n transition: \"color 200ms cubic-bezier(0.4, 0, 0.2, 1)\",\n lineHeight: 1.2,\n whiteSpace: \"nowrap\",\n }}\n >\n {labelContent}\n </label>\n )}\n\n {icon && (\n <span\n style={{\n position: \"absolute\",\n left: tokens.spacing.smd,\n display: \"flex\",\n alignItems: \"center\",\n color: focused\n ? safeAccentText(tokens.colors.accent.primary)\n : tokens.colors.text.tertiary,\n pointerEvents: \"none\",\n transition: tokens.animation.fast,\n }}\n >\n {icon}\n </span>\n )}\n\n <input\n ref={ref}\n id={inputId}\n disabled={disabled}\n required={required}\n aria-invalid={hasError}\n aria-describedby={\n helperText || error ? `${inputId}-helper` : undefined\n }\n onFocus={(e) => {\n setFocused(true);\n onFocus?.(e);\n }}\n onBlur={handleBlur}\n onPaste={handlePaste}\n type={type}\n autoComplete={autoComplete}\n inputMode={resolvedInputMode}\n style={{\n width: \"100%\",\n height: controlHeight,\n padding: config.padding,\n paddingLeft: icon ? \"40px\" : config.padding,\n paddingRight: iconEnd ? \"40px\" : config.padding,\n fontSize: config.fontSize,\n fontFamily: tokens.typography.fontFamily.primary,\n color: tokens.colors.text.primary,\n backgroundColor: isTransparentTheme\n ? \"rgba(10, 15, 25, 0.4)\"\n : tokens.colors.background.surface,\n border: getBorder(),\n borderRadius: tokens.borderRadius.md,\n outline: \"none\",\n transition: \"all 200ms cubic-bezier(0.4, 0, 0.2, 1)\",\n cursor: disabled ? \"not-allowed\" : \"text\",\n opacity: disabled ? 0.5 : 1,\n boxShadow: focusGlow,\n }}\n {...rest}\n />\n\n {iconEnd && (\n <span\n style={{\n position: \"absolute\",\n right: tokens.spacing.smd,\n display: \"flex\",\n alignItems: \"center\",\n color: tokens.colors.text.tertiary,\n cursor: \"pointer\",\n }}\n >\n {iconEnd}\n </span>\n )}\n </div>\n\n {(helperText || error) && (\n <span\n id={`${inputId}-helper`}\n role={hasError ? \"alert\" : undefined}\n style={{\n fontSize: \"0.6875rem\",\n fontWeight: hasError ? 500 : 400,\n color: hasError\n ? tokens.colors.status.critical\n : tokens.colors.text.tertiary,\n display: \"flex\",\n alignItems: \"center\",\n gap: tokens.spacing.xs,\n }}\n >\n {hasError && (\n <svg\n width=\"12\"\n height=\"12\"\n viewBox=\"0 0 24 24\"\n fill={tokens.colors.status.critical}\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z\" />\n </svg>\n )}\n {error || helperText}\n </span>\n )}\n </div>\n );\n }),\n);\n\nexport default Input;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Input.js","names":[],"sources":["../../../../src/react/core/inputs/Input.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - Input Component\r\n *\r\n * Text input field following Astro UX Design System with Zendir purple accents.\r\n *\r\n * Astro UX Compliance:\r\n * - Stacked inputs left-justified\r\n * - Consistent spacing\r\n * - Help text for validation (not placeholder)\r\n * - Required/optional indicators\r\n * - States: default, hover, focus, invalid, disabled\r\n *\r\n * Zendir Enhancements:\r\n * - Purple focus glow\r\n * - Smooth transitions\r\n * - Modern glassmorphism effects\r\n *\r\n * @example\r\n * ```tsx\r\n * <Input label=\"Spacecraft ID\" placeholder=\"Enter ID...\" />\r\n * <Input label=\"Password\" type=\"password\" />\r\n * <Input label=\"Search\" icon={<Icon name=\"search\" />} />\r\n * <Input label=\"Email\" error=\"Invalid email format\" />\r\n * ```\r\n */\r\n\r\nimport React, { memo, forwardRef, useState, useId } from \"react\";\r\nimport { useTheme } from \"../../theme\";\r\nimport { classNames, safeAccentText, type StatusLevel } from \"../../utils\";\r\nimport { useBreakpoint } from \"../layout/useBreakpoint\";\r\n\r\nexport type InputSize = \"small\" | \"medium\" | \"large\";\r\n\r\n/** Label placement relative to the input field */\r\nexport type LabelPlacement = \"above\" | \"outlined\";\r\n\r\nexport interface InputProps extends Omit<\r\n React.InputHTMLAttributes<HTMLInputElement>,\r\n \"size\"\r\n> {\r\n /** Label text or ReactNode (supports rich content like tooltip icons) */\r\n label?: React.ReactNode;\r\n /** Helper text below input */\r\n helperText?: string;\r\n /** Error message (shows error state) */\r\n error?: string;\r\n /** Size variant */\r\n size?: InputSize;\r\n /** Status for styling */\r\n status?: StatusLevel;\r\n /** Icon at start of input */\r\n icon?: React.ReactNode;\r\n /** Icon at end of input */\r\n iconEnd?: React.ReactNode;\r\n /** Full width */\r\n fullWidth?: boolean;\r\n /** Required indicator */\r\n required?: boolean;\r\n /** Optional indicator (shows \"optional\" text) */\r\n optional?: boolean;\r\n /** Custom className */\r\n className?: string;\r\n /**\r\n * Label placement style.\r\n * - `'outlined'` — label sits on the input border (notched/Material-style). **Default.**\r\n * - `'above'` — label sits above the input with a gap (classic stack).\r\n */\r\n labelPlacement?: LabelPlacement;\r\n}\r\n\r\nexport const Input = memo(\r\n forwardRef<HTMLInputElement, InputProps>(function Input(\r\n {\r\n label,\r\n helperText,\r\n error,\r\n size = \"medium\",\r\n status,\r\n icon,\r\n iconEnd,\r\n fullWidth = false,\r\n required,\r\n optional,\r\n disabled,\r\n className = \"\",\r\n id: propId,\r\n style,\r\n onFocus,\r\n onBlur,\r\n labelPlacement = \"outlined\",\r\n type = \"text\",\r\n autoComplete,\r\n inputMode,\r\n onPaste: onPasteUser,\r\n ...rest\r\n },\r\n ref,\r\n ): React.ReactElement {\r\n // Mobile: email keyboard when type is email (industry standard).\r\n const resolvedInputMode =\r\n inputMode ?? (type === \"email\" ? \"email\" : undefined);\r\n const { tokens, theme } = useTheme();\r\n const { isMobile } = useBreakpoint();\r\n const [focused, setFocused] = useState(false);\r\n const [hovered, setHovered] = useState(false);\r\n const generatedId = useId();\r\n const inputId = propId ?? generatedId;\r\n\r\n // Trim pasted content for password, email, and username (industry standard: avoid auth/validation failures).\r\n const shouldTrimOnPaste =\r\n type === \"password\" ||\r\n type === \"email\" ||\r\n autoComplete === \"email\" ||\r\n autoComplete === \"username\";\r\n const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {\r\n if (shouldTrimOnPaste) {\r\n const pasted = e.clipboardData.getData(\"text\");\r\n const trimmed = pasted.trim();\r\n if (trimmed !== pasted) {\r\n e.preventDefault();\r\n const input = e.currentTarget;\r\n const start = input.selectionStart ?? 0;\r\n const end = input.selectionEnd ?? 0;\r\n const cur = (rest as { value?: string }).value ?? input.value ?? \"\";\r\n const newValue = cur.slice(0, start) + trimmed + cur.slice(end);\r\n rest.onChange?.({\r\n target: { ...input, value: newValue },\r\n } as React.ChangeEvent<HTMLInputElement>);\r\n requestAnimationFrame(() => {\r\n input.setSelectionRange(\r\n start + trimmed.length,\r\n start + trimmed.length,\r\n );\r\n });\r\n }\r\n }\r\n onPasteUser?.(e);\r\n };\r\n\r\n // Trim on blur for email/username so \" user@example.com \" becomes \"user@example.com\" when leaving the field.\r\n const shouldTrimOnBlur =\r\n type === \"email\" ||\r\n autoComplete === \"email\" ||\r\n autoComplete === \"username\";\r\n const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {\r\n if (shouldTrimOnBlur) {\r\n const input = e.currentTarget;\r\n const cur = input.value;\r\n const trimmed = cur.trim();\r\n if (trimmed !== cur && rest.onChange) {\r\n rest.onChange({\r\n target: { ...input, value: trimmed },\r\n } as React.ChangeEvent<HTMLInputElement>);\r\n }\r\n }\r\n setFocused(false);\r\n onBlur?.(e);\r\n };\r\n\r\n // Transparent theme detection (purple-hue transparent bg)\r\n const isTransparentTheme =\r\n theme === \"transparent\" ||\r\n theme === \"transparent-bold\" ||\r\n theme === \"transparent-minimal\";\r\n const _transparentInputBg = tokens.colors.interactive.transparentInputBg;\r\n\r\n const hasError = !!error;\r\n const isOutlined = labelPlacement === \"outlined\";\r\n\r\n // Determine border color based on state\r\n const getBorder = () => {\r\n if (hasError) return tokens.borders.input.error;\r\n if (status)\r\n return `${tokens.borders.width.thin} solid ${tokens.colors.status[status]}`;\r\n if (focused) return tokens.borders.input.focus;\r\n if (hovered && !disabled) return tokens.borders.input.hover;\r\n return tokens.borders.input.default;\r\n };\r\n\r\n const sizeConfig = {\r\n small: {\r\n padding: `${tokens.spacing.xs} ${tokens.spacing.sm}`,\r\n fontSize: tokens.typography.fontSize.xs,\r\n height: tokens.elementSize.sm,\r\n iconSize: 14,\r\n labelSize: \"0.6875rem\",\r\n outlinedLabelSize: \"0.625rem\",\r\n inputPaddingLeft: 10,\r\n },\r\n medium: {\r\n padding: `${tokens.spacing.sm} ${tokens.spacing.smd}`,\r\n fontSize: tokens.typography.fontSize.sm,\r\n height: tokens.elementSize.md,\r\n iconSize: 18,\r\n labelSize: \"0.75rem\",\r\n outlinedLabelSize: \"0.6875rem\",\r\n inputPaddingLeft: 12,\r\n },\r\n large: {\r\n padding: `${tokens.spacing.smd} ${tokens.spacing.md}`,\r\n fontSize: tokens.typography.fontSize.base,\r\n height: tokens.elementSize.lg,\r\n iconSize: 20,\r\n labelSize: \"0.875rem\",\r\n outlinedLabelSize: \"0.75rem\",\r\n inputPaddingLeft: 16,\r\n },\r\n };\r\n\r\n const config = sizeConfig[size];\r\n const controlHeight = isMobile ? \"44px\" : config.height;\r\n\r\n // Focus glow effect\r\n const focusGlow =\r\n focused && !hasError\r\n ? tokens.borders.focusRing.default\r\n : hasError && focused\r\n ? `0 0 0 3px ${tokens.colors.status.critical}20`\r\n : \"none\";\r\n\r\n // Background for the outlined label \"notch\" (must be opaque to mask the border)\r\n const labelBg = isTransparentTheme\r\n ? tokens.colors.background.base\r\n : tokens.colors.background.surface;\r\n\r\n // Shared label content (text + required/optional indicators)\r\n const labelContent = (\r\n <>\r\n {label}\r\n {required && (\r\n <span\r\n style={{ color: tokens.colors.status.critical, marginLeft: \"2px\" }}\r\n >\r\n *\r\n </span>\r\n )}\r\n {optional && (\r\n <span\r\n style={{\r\n color: tokens.colors.text.tertiary,\r\n fontWeight: 400,\r\n fontStyle: \"italic\",\r\n }}\r\n >\r\n {\" \"}\r\n (optional)\r\n </span>\r\n )}\r\n </>\r\n );\r\n\r\n return (\r\n <div\r\n className={classNames(\"zendir-input-wrapper\", className)}\r\n style={{\r\n display: \"flex\",\r\n flexDirection: \"column\",\r\n gap: tokens.spacing.xs,\r\n width: fullWidth ? \"100%\" : \"auto\",\r\n minWidth: fullWidth ? undefined : isMobile ? \"100%\" : \"200px\",\r\n ...style,\r\n }}\r\n >\r\n {/* Label: 'above' variant — sits above the input with gap */}\r\n {label && !isOutlined && (\r\n <label\r\n htmlFor={inputId}\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n gap: tokens.spacing.xs,\r\n fontSize: config.labelSize,\r\n fontWeight: 500,\r\n color: tokens.colors.text.secondary,\r\n letterSpacing: \"0.01em\",\r\n }}\r\n >\r\n {labelContent}\r\n </label>\r\n )}\r\n\r\n <div\r\n style={{\r\n position: \"relative\",\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n }}\r\n onMouseEnter={() => setHovered(true)}\r\n onMouseLeave={() => setHovered(false)}\r\n >\r\n {/* Label: 'outlined' variant — positioned on the input border */}\r\n {label && isOutlined && (\r\n <label\r\n htmlFor={inputId}\r\n style={{\r\n position: \"absolute\",\r\n top: 0,\r\n left: icon ? \"34px\" : `${config.inputPaddingLeft - 6}px`,\r\n transform: \"translateY(-50%)\",\r\n backgroundColor: labelBg,\r\n padding: `0 ${tokens.spacing.xs}`,\r\n fontSize: config.outlinedLabelSize,\r\n fontWeight: 500,\r\n color: focused\r\n ? hasError\r\n ? tokens.colors.status.critical\r\n : tokens.colors.accent.primary\r\n : hasError\r\n ? tokens.colors.status.critical\r\n : tokens.colors.text.secondary,\r\n letterSpacing: \"0.01em\",\r\n zIndex: 1,\r\n pointerEvents: \"none\",\r\n transition: \"color 200ms cubic-bezier(0.4, 0, 0.2, 1)\",\r\n lineHeight: 1.2,\r\n whiteSpace: \"nowrap\",\r\n }}\r\n >\r\n {labelContent}\r\n </label>\r\n )}\r\n\r\n {icon && (\r\n <span\r\n style={{\r\n position: \"absolute\",\r\n left: tokens.spacing.smd,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n color: focused\r\n ? safeAccentText(tokens.colors.accent.primary)\r\n : tokens.colors.text.tertiary,\r\n pointerEvents: \"none\",\r\n transition: tokens.animation.fast,\r\n }}\r\n >\r\n {icon}\r\n </span>\r\n )}\r\n\r\n <input\r\n ref={ref}\r\n id={inputId}\r\n disabled={disabled}\r\n required={required}\r\n aria-invalid={hasError}\r\n aria-describedby={\r\n helperText || error ? `${inputId}-helper` : undefined\r\n }\r\n onFocus={(e) => {\r\n setFocused(true);\r\n onFocus?.(e);\r\n }}\r\n onBlur={handleBlur}\r\n onPaste={handlePaste}\r\n type={type}\r\n autoComplete={autoComplete}\r\n inputMode={resolvedInputMode}\r\n style={{\r\n width: \"100%\",\r\n height: controlHeight,\r\n padding: config.padding,\r\n paddingLeft: icon ? \"40px\" : config.padding,\r\n paddingRight: iconEnd ? \"40px\" : config.padding,\r\n fontSize: config.fontSize,\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n color: tokens.colors.text.primary,\r\n backgroundColor: isTransparentTheme\r\n ? \"rgba(10, 15, 25, 0.4)\"\r\n : tokens.colors.background.surface,\r\n border: getBorder(),\r\n borderRadius: tokens.borderRadius.md,\r\n outline: \"none\",\r\n transition: \"all 200ms cubic-bezier(0.4, 0, 0.2, 1)\",\r\n cursor: disabled ? \"not-allowed\" : \"text\",\r\n opacity: disabled ? 0.5 : 1,\r\n boxShadow: focusGlow,\r\n }}\r\n {...rest}\r\n />\r\n\r\n {iconEnd && (\r\n <span\r\n style={{\r\n position: \"absolute\",\r\n right: tokens.spacing.smd,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n color: tokens.colors.text.tertiary,\r\n cursor: \"pointer\",\r\n }}\r\n >\r\n {iconEnd}\r\n </span>\r\n )}\r\n </div>\r\n\r\n {(helperText || error) && (\r\n <span\r\n id={`${inputId}-helper`}\r\n role={hasError ? \"alert\" : undefined}\r\n style={{\r\n fontSize: \"0.6875rem\",\r\n fontWeight: hasError ? 500 : 400,\r\n color: hasError\r\n ? tokens.colors.status.critical\r\n : tokens.colors.text.tertiary,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n gap: tokens.spacing.xs,\r\n }}\r\n >\r\n {hasError && (\r\n <svg\r\n width=\"12\"\r\n height=\"12\"\r\n viewBox=\"0 0 24 24\"\r\n fill={tokens.colors.status.critical}\r\n aria-hidden=\"true\"\r\n focusable=\"false\"\r\n >\r\n <path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z\" />\r\n </svg>\r\n )}\r\n {error || helperText}\r\n </span>\r\n )}\r\n </div>\r\n );\r\n }),\r\n);\r\n\r\nexport default Input;\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LimitsBar.js","names":[],"sources":["../../../../src/react/core/inputs/LimitsBar.tsx"],"sourcesContent":["/**\n * @zendir/ui - LimitsBar Component\n *\n * Visual limits indicator for telemetry monitoring and spacecraft health assessment.\n * Displays a horizontal bar with colored zones (red-low, yellow-low, green, yellow-high, red-high)\n * and a marker showing the current value position within the range.\n *\n * Supports:\n * - 5-zone limits (red-low, yellow-low, green/normal, yellow-high, red-high)\n * - 3-zone limits (low, nominal, high)\n * - Blue zone for \"out of range\" / stale\n * - Animated value transitions\n * - Compact and full display modes\n * - Value label with units\n * - Configurable thresholds\n *\n * Astro UX Compliance:\n * - Uses official Astro status colors\n * - Consistent with Astro monitoring patterns\n *\n * @example\n * ```tsx\n * <LimitsBar\n * value={72.5}\n * redLow={10} yellowLow={25} greenLow={40}\n * greenHigh={80} yellowHigh={90} redHigh={100}\n * label=\"Temperature\" unit=\"°C\"\n * />\n * ```\n */\n\nimport React, { useMemo, memo } from \"react\";\nimport { useTheme } from \"../../theme\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type LimitsState =\n | \"red_low\"\n | \"yellow_low\"\n | \"green\"\n | \"yellow_high\"\n | \"red_high\"\n | \"blue\"\n | \"stale\";\n\nexport interface LimitsBarProps {\n /** Current value */\n value: number;\n /** Red low threshold (minimum critical) */\n redLow?: number;\n /** Yellow low threshold (minimum warning) */\n yellowLow?: number;\n /** Green low threshold (start of nominal) */\n greenLow?: number;\n /** Green high threshold (end of nominal) */\n greenHigh?: number;\n /** Yellow high threshold (maximum warning) */\n yellowHigh?: number;\n /** Red high threshold (maximum critical) */\n redHigh?: number;\n /** Overall min for the bar (defaults to redLow - 10%) */\n min?: number;\n /** Overall max for the bar (defaults to redHigh + 10%) */\n max?: number;\n /** Label text */\n label?: string;\n /** Unit string */\n unit?: string;\n /** Whether the value is stale / not updating */\n stale?: boolean;\n /** Compact mode (thinner bar, less text) */\n compact?: boolean;\n /** Show the numeric value on the bar */\n showValue?: boolean;\n /** Show limit threshold labels */\n showThresholds?: boolean;\n /** Width (default: '100%') */\n width?: number | string;\n /** Height of the bar (default: 20 for normal, 12 for compact) */\n barHeight?: number;\n /** Custom status override (forces a specific status regardless of value) */\n statusOverride?: LimitsState;\n /** Called when status changes */\n onStatusChange?: (status: LimitsState, value: number) => void;\n /** When false, lower-limit zones (red-low, yellow-low, green-low) are hidden even if threshold values are provided */\n showLower?: boolean;\n /** When false, upper-limit zones (green-high, yellow-high, red-high) are hidden even if threshold values are provided */\n showUpper?: boolean;\n /** CSS class */\n className?: string;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction getLimitsState(\n value: number,\n redLow?: number,\n yellowLow?: number,\n greenLow?: number,\n greenHigh?: number,\n yellowHigh?: number,\n redHigh?: number,\n): LimitsState {\n if (redLow !== undefined && value < redLow) return \"red_low\";\n if (yellowLow !== undefined && value < yellowLow) return \"yellow_low\";\n if (greenLow !== undefined && value < greenLow) return \"yellow_low\";\n if (redHigh !== undefined && value > redHigh) return \"red_high\";\n if (yellowHigh !== undefined && value > yellowHigh) return \"yellow_high\";\n if (greenHigh !== undefined && value > greenHigh) return \"yellow_high\";\n return \"green\";\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nexport const LimitsBar = memo(function LimitsBar({\n value,\n redLow: redLowProp,\n yellowLow: yellowLowProp,\n greenLow: greenLowProp,\n greenHigh: greenHighProp,\n yellowHigh: yellowHighProp,\n redHigh: redHighProp,\n min: minProp,\n max: maxProp,\n label,\n unit,\n stale = false,\n compact = false,\n showValue = true,\n showThresholds = false,\n showLower = true,\n showUpper = true,\n width = \"100%\",\n barHeight: barHeightProp,\n statusOverride,\n className = \"\",\n}: LimitsBarProps): React.ReactElement {\n // Mask thresholds based on showLower / showUpper flags\n const redLow = showLower ? redLowProp : undefined;\n const yellowLow = showLower ? yellowLowProp : undefined;\n const greenLow = showLower ? greenLowProp : undefined;\n const greenHigh = showUpper ? greenHighProp : undefined;\n const yellowHigh = showUpper ? yellowHighProp : undefined;\n const redHigh = showUpper ? redHighProp : undefined;\n const { tokens } = useTheme();\n const softStatusColors = useMemo(\n () => ({\n normal: tokens.colors.status.normal,\n caution: tokens.colors.status.caution,\n serious: tokens.colors.status.serious,\n critical: tokens.colors.status.critical,\n off: tokens.colors.status.off,\n standby: tokens.colors.status.standby,\n }),\n [tokens],\n );\n\n const barHeight = barHeightProp ?? (compact ? 12 : 20);\n\n const _thresholds = useMemo(() => {\n const rl = redLow ?? -Infinity;\n const yl = yellowLow ?? redLow ?? -Infinity;\n const gl = greenLow ?? yellowLow ?? redLow ?? -Infinity;\n const gh = greenHigh ?? yellowHigh ?? redHigh ?? Infinity;\n const yh = yellowHigh ?? redHigh ?? Infinity;\n const rh = redHigh ?? Infinity;\n return { rl, yl, gl, gh, yh, rh };\n }, [redLow, yellowLow, greenLow, greenHigh, yellowHigh, redHigh]);\n\n const rangeMin =\n minProp ??\n (redLow !== undefined\n ? redLow - (redHigh !== undefined ? (redHigh - redLow) * 0.1 : 10)\n : 0);\n const rangeMax =\n maxProp ??\n (redHigh !== undefined\n ? redHigh + (redLow !== undefined ? (redHigh - redLow) * 0.1 : 10)\n : 100);\n const range = rangeMax - rangeMin;\n\n const status = stale\n ? \"stale\"\n : (statusOverride ??\n getLimitsState(\n value,\n redLow,\n yellowLow,\n greenLow,\n greenHigh,\n yellowHigh,\n redHigh,\n ));\n\n const statusColors: Record<LimitsState, string> = {\n red_low: softStatusColors.critical,\n red_high: softStatusColors.critical,\n yellow_low: softStatusColors.caution,\n yellow_high: softStatusColors.caution,\n green: softStatusColors.normal,\n blue: softStatusColors.standby,\n stale: softStatusColors.off,\n };\n\n const valuePercent =\n range > 0\n ? Math.max(0, Math.min(100, ((value - rangeMin) / range) * 100))\n : 50;\n\n // Build zone segments\n const zones = useMemo(() => {\n const segs: { start: number; end: number; color: string }[] = [];\n const toPercent = (v: number) =>\n Math.max(0, Math.min(100, ((v - rangeMin) / range) * 100));\n\n const critical = softStatusColors.critical;\n const caution = softStatusColors.caution;\n const normal = softStatusColors.normal;\n\n if (redLow !== undefined) {\n segs.push({ start: 0, end: toPercent(redLow), color: `${critical}66` });\n }\n if (yellowLow !== undefined) {\n const s = redLow !== undefined ? toPercent(redLow) : 0;\n segs.push({ start: s, end: toPercent(yellowLow), color: `${caution}66` });\n }\n {\n const s =\n yellowLow !== undefined\n ? toPercent(yellowLow)\n : redLow !== undefined\n ? toPercent(redLow)\n : 0;\n const e =\n yellowHigh !== undefined\n ? toPercent(yellowHigh)\n : redHigh !== undefined\n ? toPercent(redHigh)\n : 100;\n segs.push({ start: s, end: e, color: `${normal}44` });\n }\n if (yellowHigh !== undefined) {\n const e = redHigh !== undefined ? toPercent(redHigh) : 100;\n segs.push({\n start: toPercent(yellowHigh),\n end: e,\n color: `${caution}66`,\n });\n }\n if (redHigh !== undefined) {\n segs.push({\n start: toPercent(redHigh),\n end: 100,\n color: `${critical}66`,\n });\n }\n\n return segs;\n }, [\n redLow,\n yellowLow,\n yellowHigh,\n redHigh,\n rangeMin,\n range,\n softStatusColors,\n ]);\n\n const markerColor = statusColors[status];\n\n return (\n <div\n className={`limits-bar ${className}`}\n role=\"meter\"\n aria-valuenow={value}\n aria-valuemin={rangeMin}\n aria-valuemax={rangeMax}\n aria-label={\n label\n ? `${label}: ${value}${unit ? \" \" + unit : \"\"}`\n : `${value}${unit ? \" \" + unit : \"\"}`\n }\n style={{ width, fontFamily: tokens.typography.fontFamily.primary }}\n >\n {/* Label row */}\n {(label || showValue) && !compact && (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: tokens.spacing.xs,\n fontSize: tokens.typography.fontSize.sm,\n }}\n >\n {label && (\n <span\n style={{\n color: tokens.colors.text.secondary,\n fontWeight: tokens.typography.fontWeight.medium,\n }}\n >\n {label}\n </span>\n )}\n {showValue && (\n <span\n style={{\n color: markerColor,\n fontWeight: tokens.typography.fontWeight.medium,\n fontFamily: tokens.typography.fontFamily.mono,\n }}\n >\n {typeof value === \"number\" ? value.toFixed(2) : value}\n {unit && (\n <span\n style={{\n fontSize: tokens.typography.fontSize.xxs,\n marginLeft: tokens.spacing.xxs,\n color: tokens.colors.text.muted,\n }}\n >\n {unit}\n </span>\n )}\n </span>\n )}\n </div>\n )}\n\n {/* Bar */}\n <div\n style={{\n position: \"relative\",\n height: barHeight,\n borderRadius: tokens.borderRadius.full,\n background: tokens.colors.background.base,\n border: `1px solid ${tokens.colors.border.muted}`,\n overflow: \"hidden\",\n }}\n >\n {/* Zone segments */}\n {zones.map((z, i) => (\n <div\n key={i}\n style={{\n position: \"absolute\",\n left: `${z.start}%`,\n width: `${z.end - z.start}%`,\n height: \"100%\",\n background: z.color,\n }}\n />\n ))}\n\n {/* Value marker */}\n <div\n style={{\n position: \"absolute\",\n left: `${valuePercent}%`,\n top: 0,\n bottom: 0,\n width: compact ? 3 : 4,\n marginLeft: compact ? -1.5 : -2,\n background: markerColor,\n borderRadius: tokens.borderRadius.sm,\n boxShadow: `0 0 8px ${markerColor}66, 0 0 3px ${markerColor}44`,\n transition:\n \"left 300ms cubic-bezier(0.4, 0, 0.2, 1), background 300ms ease\",\n }}\n />\n\n {/* Compact label */}\n {compact && showValue && (\n <div\n style={{\n position: \"absolute\",\n right: tokens.spacing.xs,\n top: \"50%\",\n transform: \"translateY(-50%)\",\n fontSize: tokens.typography.fontSize.micro,\n fontWeight: tokens.typography.fontWeight.medium,\n color: markerColor,\n fontFamily: tokens.typography.fontFamily.mono,\n textShadow: `0 0 4px ${tokens.colors.background.base}`,\n }}\n >\n {typeof value === \"number\" ? value.toFixed(1) : value}\n {unit || \"\"}\n </div>\n )}\n </div>\n\n {/* Threshold labels */}\n {showThresholds && !compact && (\n <div\n style={{\n position: \"relative\",\n height: 14,\n marginTop: tokens.spacing.xxs,\n }}\n >\n {[\n { val: redLow, color: softStatusColors.critical },\n { val: yellowLow, color: softStatusColors.caution },\n { val: greenHigh ?? yellowHigh, color: softStatusColors.caution },\n { val: redHigh, color: softStatusColors.critical },\n ]\n .filter((t) => t.val !== undefined)\n .map((t, i) => {\n const pct = range > 0 ? ((t.val! - rangeMin) / range) * 100 : 0;\n return (\n <span\n key={i}\n aria-label={`Threshold: ${t.val}`}\n style={{\n position: \"absolute\",\n left: `${pct}%`,\n transform: \"translateX(-50%)\",\n fontSize: tokens.typography.fontSize.micro,\n color: t.color,\n fontFamily: tokens.typography.fontFamily.mono,\n }}\n >\n {t.val}\n </span>\n );\n })}\n </div>\n )}\n </div>\n );\n});\n\nexport default LimitsBar;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LimitsBar.js","names":[],"sources":["../../../../src/react/core/inputs/LimitsBar.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - LimitsBar Component\r\n *\r\n * Visual limits indicator for telemetry monitoring and spacecraft health assessment.\r\n * Displays a horizontal bar with colored zones (red-low, yellow-low, green, yellow-high, red-high)\r\n * and a marker showing the current value position within the range.\r\n *\r\n * Supports:\r\n * - 5-zone limits (red-low, yellow-low, green/normal, yellow-high, red-high)\r\n * - 3-zone limits (low, nominal, high)\r\n * - Blue zone for \"out of range\" / stale\r\n * - Animated value transitions\r\n * - Compact and full display modes\r\n * - Value label with units\r\n * - Configurable thresholds\r\n *\r\n * Astro UX Compliance:\r\n * - Uses official Astro status colors\r\n * - Consistent with Astro monitoring patterns\r\n *\r\n * @example\r\n * ```tsx\r\n * <LimitsBar\r\n * value={72.5}\r\n * redLow={10} yellowLow={25} greenLow={40}\r\n * greenHigh={80} yellowHigh={90} redHigh={100}\r\n * label=\"Temperature\" unit=\"°C\"\r\n * />\r\n * ```\r\n */\r\n\r\nimport React, { useMemo, memo } from \"react\";\r\nimport { useTheme } from \"../../theme\";\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\nexport type LimitsState =\r\n | \"red_low\"\r\n | \"yellow_low\"\r\n | \"green\"\r\n | \"yellow_high\"\r\n | \"red_high\"\r\n | \"blue\"\r\n | \"stale\";\r\n\r\nexport interface LimitsBarProps {\r\n /** Current value */\r\n value: number;\r\n /** Red low threshold (minimum critical) */\r\n redLow?: number;\r\n /** Yellow low threshold (minimum warning) */\r\n yellowLow?: number;\r\n /** Green low threshold (start of nominal) */\r\n greenLow?: number;\r\n /** Green high threshold (end of nominal) */\r\n greenHigh?: number;\r\n /** Yellow high threshold (maximum warning) */\r\n yellowHigh?: number;\r\n /** Red high threshold (maximum critical) */\r\n redHigh?: number;\r\n /** Overall min for the bar (defaults to redLow - 10%) */\r\n min?: number;\r\n /** Overall max for the bar (defaults to redHigh + 10%) */\r\n max?: number;\r\n /** Label text */\r\n label?: string;\r\n /** Unit string */\r\n unit?: string;\r\n /** Whether the value is stale / not updating */\r\n stale?: boolean;\r\n /** Compact mode (thinner bar, less text) */\r\n compact?: boolean;\r\n /** Show the numeric value on the bar */\r\n showValue?: boolean;\r\n /** Show limit threshold labels */\r\n showThresholds?: boolean;\r\n /** Width (default: '100%') */\r\n width?: number | string;\r\n /** Height of the bar (default: 20 for normal, 12 for compact) */\r\n barHeight?: number;\r\n /** Custom status override (forces a specific status regardless of value) */\r\n statusOverride?: LimitsState;\r\n /** Called when status changes */\r\n onStatusChange?: (status: LimitsState, value: number) => void;\r\n /** When false, lower-limit zones (red-low, yellow-low, green-low) are hidden even if threshold values are provided */\r\n showLower?: boolean;\r\n /** When false, upper-limit zones (green-high, yellow-high, red-high) are hidden even if threshold values are provided */\r\n showUpper?: boolean;\r\n /** CSS class */\r\n className?: string;\r\n}\r\n\r\n// =============================================================================\r\n// Helpers\r\n// =============================================================================\r\n\r\nfunction getLimitsState(\r\n value: number,\r\n redLow?: number,\r\n yellowLow?: number,\r\n greenLow?: number,\r\n greenHigh?: number,\r\n yellowHigh?: number,\r\n redHigh?: number,\r\n): LimitsState {\r\n if (redLow !== undefined && value < redLow) return \"red_low\";\r\n if (yellowLow !== undefined && value < yellowLow) return \"yellow_low\";\r\n if (greenLow !== undefined && value < greenLow) return \"yellow_low\";\r\n if (redHigh !== undefined && value > redHigh) return \"red_high\";\r\n if (yellowHigh !== undefined && value > yellowHigh) return \"yellow_high\";\r\n if (greenHigh !== undefined && value > greenHigh) return \"yellow_high\";\r\n return \"green\";\r\n}\r\n\r\n// =============================================================================\r\n// Component\r\n// =============================================================================\r\n\r\nexport const LimitsBar = memo(function LimitsBar({\r\n value,\r\n redLow: redLowProp,\r\n yellowLow: yellowLowProp,\r\n greenLow: greenLowProp,\r\n greenHigh: greenHighProp,\r\n yellowHigh: yellowHighProp,\r\n redHigh: redHighProp,\r\n min: minProp,\r\n max: maxProp,\r\n label,\r\n unit,\r\n stale = false,\r\n compact = false,\r\n showValue = true,\r\n showThresholds = false,\r\n showLower = true,\r\n showUpper = true,\r\n width = \"100%\",\r\n barHeight: barHeightProp,\r\n statusOverride,\r\n className = \"\",\r\n}: LimitsBarProps): React.ReactElement {\r\n // Mask thresholds based on showLower / showUpper flags\r\n const redLow = showLower ? redLowProp : undefined;\r\n const yellowLow = showLower ? yellowLowProp : undefined;\r\n const greenLow = showLower ? greenLowProp : undefined;\r\n const greenHigh = showUpper ? greenHighProp : undefined;\r\n const yellowHigh = showUpper ? yellowHighProp : undefined;\r\n const redHigh = showUpper ? redHighProp : undefined;\r\n const { tokens } = useTheme();\r\n const softStatusColors = useMemo(\r\n () => ({\r\n normal: tokens.colors.status.normal,\r\n caution: tokens.colors.status.caution,\r\n serious: tokens.colors.status.serious,\r\n critical: tokens.colors.status.critical,\r\n off: tokens.colors.status.off,\r\n standby: tokens.colors.status.standby,\r\n }),\r\n [tokens],\r\n );\r\n\r\n const barHeight = barHeightProp ?? (compact ? 12 : 20);\r\n\r\n const _thresholds = useMemo(() => {\r\n const rl = redLow ?? -Infinity;\r\n const yl = yellowLow ?? redLow ?? -Infinity;\r\n const gl = greenLow ?? yellowLow ?? redLow ?? -Infinity;\r\n const gh = greenHigh ?? yellowHigh ?? redHigh ?? Infinity;\r\n const yh = yellowHigh ?? redHigh ?? Infinity;\r\n const rh = redHigh ?? Infinity;\r\n return { rl, yl, gl, gh, yh, rh };\r\n }, [redLow, yellowLow, greenLow, greenHigh, yellowHigh, redHigh]);\r\n\r\n const rangeMin =\r\n minProp ??\r\n (redLow !== undefined\r\n ? redLow - (redHigh !== undefined ? (redHigh - redLow) * 0.1 : 10)\r\n : 0);\r\n const rangeMax =\r\n maxProp ??\r\n (redHigh !== undefined\r\n ? redHigh + (redLow !== undefined ? (redHigh - redLow) * 0.1 : 10)\r\n : 100);\r\n const range = rangeMax - rangeMin;\r\n\r\n const status = stale\r\n ? \"stale\"\r\n : (statusOverride ??\r\n getLimitsState(\r\n value,\r\n redLow,\r\n yellowLow,\r\n greenLow,\r\n greenHigh,\r\n yellowHigh,\r\n redHigh,\r\n ));\r\n\r\n const statusColors: Record<LimitsState, string> = {\r\n red_low: softStatusColors.critical,\r\n red_high: softStatusColors.critical,\r\n yellow_low: softStatusColors.caution,\r\n yellow_high: softStatusColors.caution,\r\n green: softStatusColors.normal,\r\n blue: softStatusColors.standby,\r\n stale: softStatusColors.off,\r\n };\r\n\r\n const valuePercent =\r\n range > 0\r\n ? Math.max(0, Math.min(100, ((value - rangeMin) / range) * 100))\r\n : 50;\r\n\r\n // Build zone segments\r\n const zones = useMemo(() => {\r\n const segs: { start: number; end: number; color: string }[] = [];\r\n const toPercent = (v: number) =>\r\n Math.max(0, Math.min(100, ((v - rangeMin) / range) * 100));\r\n\r\n const critical = softStatusColors.critical;\r\n const caution = softStatusColors.caution;\r\n const normal = softStatusColors.normal;\r\n\r\n if (redLow !== undefined) {\r\n segs.push({ start: 0, end: toPercent(redLow), color: `${critical}66` });\r\n }\r\n if (yellowLow !== undefined) {\r\n const s = redLow !== undefined ? toPercent(redLow) : 0;\r\n segs.push({ start: s, end: toPercent(yellowLow), color: `${caution}66` });\r\n }\r\n {\r\n const s =\r\n yellowLow !== undefined\r\n ? toPercent(yellowLow)\r\n : redLow !== undefined\r\n ? toPercent(redLow)\r\n : 0;\r\n const e =\r\n yellowHigh !== undefined\r\n ? toPercent(yellowHigh)\r\n : redHigh !== undefined\r\n ? toPercent(redHigh)\r\n : 100;\r\n segs.push({ start: s, end: e, color: `${normal}44` });\r\n }\r\n if (yellowHigh !== undefined) {\r\n const e = redHigh !== undefined ? toPercent(redHigh) : 100;\r\n segs.push({\r\n start: toPercent(yellowHigh),\r\n end: e,\r\n color: `${caution}66`,\r\n });\r\n }\r\n if (redHigh !== undefined) {\r\n segs.push({\r\n start: toPercent(redHigh),\r\n end: 100,\r\n color: `${critical}66`,\r\n });\r\n }\r\n\r\n return segs;\r\n }, [\r\n redLow,\r\n yellowLow,\r\n yellowHigh,\r\n redHigh,\r\n rangeMin,\r\n range,\r\n softStatusColors,\r\n ]);\r\n\r\n const markerColor = statusColors[status];\r\n\r\n return (\r\n <div\r\n className={`limits-bar ${className}`}\r\n role=\"meter\"\r\n aria-valuenow={value}\r\n aria-valuemin={rangeMin}\r\n aria-valuemax={rangeMax}\r\n aria-label={\r\n label\r\n ? `${label}: ${value}${unit ? \" \" + unit : \"\"}`\r\n : `${value}${unit ? \" \" + unit : \"\"}`\r\n }\r\n style={{ width, fontFamily: tokens.typography.fontFamily.primary }}\r\n >\r\n {/* Label row */}\r\n {(label || showValue) && !compact && (\r\n <div\r\n style={{\r\n display: \"flex\",\r\n justifyContent: \"space-between\",\r\n marginBottom: tokens.spacing.xs,\r\n fontSize: tokens.typography.fontSize.sm,\r\n }}\r\n >\r\n {label && (\r\n <span\r\n style={{\r\n color: tokens.colors.text.secondary,\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n }}\r\n >\r\n {label}\r\n </span>\r\n )}\r\n {showValue && (\r\n <span\r\n style={{\r\n color: markerColor,\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n fontFamily: tokens.typography.fontFamily.mono,\r\n }}\r\n >\r\n {typeof value === \"number\" ? value.toFixed(2) : value}\r\n {unit && (\r\n <span\r\n style={{\r\n fontSize: tokens.typography.fontSize.xxs,\r\n marginLeft: tokens.spacing.xxs,\r\n color: tokens.colors.text.muted,\r\n }}\r\n >\r\n {unit}\r\n </span>\r\n )}\r\n </span>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Bar */}\r\n <div\r\n style={{\r\n position: \"relative\",\r\n height: barHeight,\r\n borderRadius: tokens.borderRadius.full,\r\n background: tokens.colors.background.base,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n overflow: \"hidden\",\r\n }}\r\n >\r\n {/* Zone segments */}\r\n {zones.map((z, i) => (\r\n <div\r\n key={i}\r\n style={{\r\n position: \"absolute\",\r\n left: `${z.start}%`,\r\n width: `${z.end - z.start}%`,\r\n height: \"100%\",\r\n background: z.color,\r\n }}\r\n />\r\n ))}\r\n\r\n {/* Value marker */}\r\n <div\r\n style={{\r\n position: \"absolute\",\r\n left: `${valuePercent}%`,\r\n top: 0,\r\n bottom: 0,\r\n width: compact ? 3 : 4,\r\n marginLeft: compact ? -1.5 : -2,\r\n background: markerColor,\r\n borderRadius: tokens.borderRadius.sm,\r\n boxShadow: `0 0 8px ${markerColor}66, 0 0 3px ${markerColor}44`,\r\n transition:\r\n \"left 300ms cubic-bezier(0.4, 0, 0.2, 1), background 300ms ease\",\r\n }}\r\n />\r\n\r\n {/* Compact label */}\r\n {compact && showValue && (\r\n <div\r\n style={{\r\n position: \"absolute\",\r\n right: tokens.spacing.xs,\r\n top: \"50%\",\r\n transform: \"translateY(-50%)\",\r\n fontSize: tokens.typography.fontSize.micro,\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n color: markerColor,\r\n fontFamily: tokens.typography.fontFamily.mono,\r\n textShadow: `0 0 4px ${tokens.colors.background.base}`,\r\n }}\r\n >\r\n {typeof value === \"number\" ? value.toFixed(1) : value}\r\n {unit || \"\"}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Threshold labels */}\r\n {showThresholds && !compact && (\r\n <div\r\n style={{\r\n position: \"relative\",\r\n height: 14,\r\n marginTop: tokens.spacing.xxs,\r\n }}\r\n >\r\n {[\r\n { val: redLow, color: softStatusColors.critical },\r\n { val: yellowLow, color: softStatusColors.caution },\r\n { val: greenHigh ?? yellowHigh, color: softStatusColors.caution },\r\n { val: redHigh, color: softStatusColors.critical },\r\n ]\r\n .filter((t) => t.val !== undefined)\r\n .map((t, i) => {\r\n const pct = range > 0 ? ((t.val! - rangeMin) / range) * 100 : 0;\r\n return (\r\n <span\r\n key={i}\r\n aria-label={`Threshold: ${t.val}`}\r\n style={{\r\n position: \"absolute\",\r\n left: `${pct}%`,\r\n transform: \"translateX(-50%)\",\r\n fontSize: tokens.typography.fontSize.micro,\r\n color: t.color,\r\n fontFamily: tokens.typography.fontFamily.mono,\r\n }}\r\n >\r\n {t.val}\r\n </span>\r\n );\r\n })}\r\n </div>\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\nexport default LimitsBar;\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkGA,SAAS,eACP,OACA,QACA,WACA,UACA,WACA,YACA,SACa;CACb,IAAI,WAAW,KAAA,KAAa,QAAQ,QAAQ,OAAO;CACnD,IAAI,cAAc,KAAA,KAAa,QAAQ,WAAW,OAAO;CACzD,IAAI,aAAa,KAAA,KAAa,QAAQ,UAAU,OAAO;CACvD,IAAI,YAAY,KAAA,KAAa,QAAQ,SAAS,OAAO;CACrD,IAAI,eAAe,KAAA,KAAa,QAAQ,YAAY,OAAO;CAC3D,IAAI,cAAc,KAAA,KAAa,QAAQ,WAAW,OAAO;CACzD,OAAO;AACT;AAMA,IAAa,YAAY,KAAK,SAAS,UAAU,EAC/C,OACA,QAAQ,YACR,WAAW,eACX,UAAU,cACV,WAAW,eACX,YAAY,gBACZ,SAAS,aACT,KAAK,SACL,KAAK,SACL,OACA,MACA,QAAQ,OACR,UAAU,OACV,YAAY,MACZ,iBAAiB,OACjB,YAAY,MACZ,YAAY,MACZ,QAAQ,QACR,WAAW,eACX,gBACA,YAAY,MACyB;CAErC,MAAM,SAAS,YAAY,aAAa,KAAA;CACxC,MAAM,YAAY,YAAY,gBAAgB,KAAA;CAC9C,MAAM,WAAW,YAAY,eAAe,KAAA;CAC5C,MAAM,YAAY,YAAY,gBAAgB,KAAA;CAC9C,MAAM,aAAa,YAAY,iBAAiB,KAAA;CAChD,MAAM,UAAU,YAAY,cAAc,KAAA;CAC1C,MAAM,EAAE,WAAW,SAAS;CAC5B,MAAM,mBAAmB,eAChB;EACL,QAAQ,OAAO,OAAO,OAAO;EAC7B,SAAS,OAAO,OAAO,OAAO;EAC9B,SAAS,OAAO,OAAO,OAAO;EAC9B,UAAU,OAAO,OAAO,OAAO;EAC/B,KAAK,OAAO,OAAO,OAAO;EAC1B,SAAS,OAAO,OAAO,OAAO;CAChC,IACA,CAAC,MAAM,CACT;CAEA,MAAM,YAAY,kBAAkB,UAAU,KAAK;CAE/B,cAAc;EAOhC,OAAO;GAAE,IANE,UAAU;GAMR,IALF,aAAa,UAAU;GAKjB,IAJN,YAAY,aAAa,UAAU;GAIzB,IAHV,aAAa,cAAc,WAAW;GAGxB,IAFd,cAAc,WAAW;GAEP,IADlB,WAAW;EACU;CAClC,GAAG;EAAC;EAAQ;EAAW;EAAU;EAAW;EAAY;CAAO,CAAC;CAEhE,MAAM,WACJ,YACC,WAAW,KAAA,IACR,UAAU,YAAY,KAAA,KAAa,UAAU,UAAU,KAAM,MAC7D;CACN,MAAM,WACJ,YACC,YAAY,KAAA,IACT,WAAW,WAAW,KAAA,KAAa,UAAU,UAAU,KAAM,MAC7D;CACN,MAAM,QAAQ,WAAW;CAEzB,MAAM,SAAS,QACX,UACC,kBACD,eACE,OACA,QACA,WACA,UACA,WACA,YACA,OACF;CAEJ,MAAM,eAA4C;EAChD,SAAS,iBAAiB;EAC1B,UAAU,iBAAiB;EAC3B,YAAY,iBAAiB;EAC7B,aAAa,iBAAiB;EAC9B,OAAO,iBAAiB;EACxB,MAAM,iBAAiB;EACvB,OAAO,iBAAiB;CAC1B;CAEA,MAAM,eACJ,QAAQ,IACJ,KAAK,IAAI,GAAG,KAAK,IAAI,MAAO,QAAQ,YAAY,QAAS,GAAG,CAAC,IAC7D;CAGN,MAAM,QAAQ,cAAc;EAC1B,MAAM,OAAwD,CAAC;EAC/D,MAAM,aAAa,MACjB,KAAK,IAAI,GAAG,KAAK,IAAI,MAAO,IAAI,YAAY,QAAS,GAAG,CAAC;EAE3D,MAAM,WAAW,iBAAiB;EAClC,MAAM,UAAU,iBAAiB;EACjC,MAAM,SAAS,iBAAiB;EAEhC,IAAI,WAAW,KAAA,GACb,KAAK,KAAK;GAAE,OAAO;GAAG,KAAK,UAAU,MAAM;GAAG,OAAO,GAAG,SAAS;EAAI,CAAC;EAExE,IAAI,cAAc,KAAA,GAAW;GAC3B,MAAM,IAAI,WAAW,KAAA,IAAY,UAAU,MAAM,IAAI;GACrD,KAAK,KAAK;IAAE,OAAO;IAAG,KAAK,UAAU,SAAS;IAAG,OAAO,GAAG,QAAQ;GAAI,CAAC;EAC1E;EACA;GACE,MAAM,IACJ,cAAc,KAAA,IACV,UAAU,SAAS,IACnB,WAAW,KAAA,IACT,UAAU,MAAM,IAChB;GACR,MAAM,IACJ,eAAe,KAAA,IACX,UAAU,UAAU,IACpB,YAAY,KAAA,IACV,UAAU,OAAO,IACjB;GACR,KAAK,KAAK;IAAE,OAAO;IAAG,KAAK;IAAG,OAAO,GAAG,OAAO;GAAI,CAAC;EACtD;EACA,IAAI,eAAe,KAAA,GAAW;GAC5B,MAAM,IAAI,YAAY,KAAA,IAAY,UAAU,OAAO,IAAI;GACvD,KAAK,KAAK;IACR,OAAO,UAAU,UAAU;IAC3B,KAAK;IACL,OAAO,GAAG,QAAQ;GACpB,CAAC;EACH;EACA,IAAI,YAAY,KAAA,GACd,KAAK,KAAK;GACR,OAAO,UAAU,OAAO;GACxB,KAAK;GACL,OAAO,GAAG,SAAS;EACrB,CAAC;EAGH,OAAO;CACT,GAAG;EACD;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,MAAM,cAAc,aAAa;CAEjC,OACE,qBAAC,OAAD;EACE,WAAW,cAAc;EACzB,MAAK;EACL,iBAAe;EACf,iBAAe;EACf,iBAAe;EACf,cACE,QACI,GAAG,MAAM,IAAI,QAAQ,OAAO,MAAM,OAAO,OACzC,GAAG,QAAQ,OAAO,MAAM,OAAO;EAErC,OAAO;GAAE;GAAO,YAAY,OAAO,WAAW,WAAW;EAAQ;YAXnE;IAcI,SAAS,cAAc,CAAC,WACxB,qBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,gBAAgB;KAChB,cAAc,OAAO,QAAQ;KAC7B,UAAU,OAAO,WAAW,SAAS;IACvC;cANF,CAQG,SACC,oBAAC,QAAD;KACE,OAAO;MACL,OAAO,OAAO,OAAO,KAAK;MAC1B,YAAY,OAAO,WAAW,WAAW;KAC3C;eAEC;IACG,CAAA,GAEP,aACC,qBAAC,QAAD;KACE,OAAO;MACL,OAAO;MACP,YAAY,OAAO,WAAW,WAAW;MACzC,YAAY,OAAO,WAAW,WAAW;KAC3C;eALF,CAOG,OAAO,UAAU,WAAW,MAAM,QAAQ,CAAC,IAAI,OAC/C,QACC,oBAAC,QAAD;MACE,OAAO;OACL,UAAU,OAAO,WAAW,SAAS;OACrC,YAAY,OAAO,QAAQ;OAC3B,OAAO,OAAO,OAAO,KAAK;MAC5B;gBAEC;KACG,CAAA,CAEJ;MAEL;;GAIP,qBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,QAAQ;KACR,cAAc,OAAO,aAAa;KAClC,YAAY,OAAO,OAAO,WAAW;KACrC,QAAQ,aAAa,OAAO,OAAO,OAAO;KAC1C,UAAU;IACZ;cARF;KAWG,MAAM,KAAK,GAAG,MACb,oBAAC,OAAD,EAEE,OAAO;MACL,UAAU;MACV,MAAM,GAAG,EAAE,MAAM;MACjB,OAAO,GAAG,EAAE,MAAM,EAAE,MAAM;MAC1B,QAAQ;MACR,YAAY,EAAE;KAChB,EACD,GARM,CAQN,CACF;KAGD,oBAAC,OAAD,EACE,OAAO;MACL,UAAU;MACV,MAAM,GAAG,aAAa;MACtB,KAAK;MACL,QAAQ;MACR,OAAO,UAAU,IAAI;MACrB,YAAY,UAAU,OAAO;MAC7B,YAAY;MACZ,cAAc,OAAO,aAAa;MAClC,WAAW,WAAW,YAAY,cAAc,YAAY;MAC5D,YACE;KACJ,EACD,CAAA;KAGA,WAAW,aACV,qBAAC,OAAD;MACE,OAAO;OACL,UAAU;OACV,OAAO,OAAO,QAAQ;OACtB,KAAK;OACL,WAAW;OACX,UAAU,OAAO,WAAW,SAAS;OACrC,YAAY,OAAO,WAAW,WAAW;OACzC,OAAO;OACP,YAAY,OAAO,WAAW,WAAW;OACzC,YAAY,WAAW,OAAO,OAAO,WAAW;MAClD;gBAXF,CAaG,OAAO,UAAU,WAAW,MAAM,QAAQ,CAAC,IAAI,OAC/C,QAAQ,EACN;;IAEJ;;GAGJ,kBAAkB,CAAC,WAClB,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,QAAQ;KACR,WAAW,OAAO,QAAQ;IAC5B;cAEC;KACC;MAAE,KAAK;MAAQ,OAAO,iBAAiB;KAAS;KAChD;MAAE,KAAK;MAAW,OAAO,iBAAiB;KAAQ;KAClD;MAAE,KAAK,aAAa;MAAY,OAAO,iBAAiB;KAAQ;KAChE;MAAE,KAAK;MAAS,OAAO,iBAAiB;KAAS;IACnD,CAAC,CACE,QAAQ,MAAM,EAAE,QAAQ,KAAA,CAAS,CAAC,CAClC,KAAK,GAAG,MAAM;KACb,MAAM,MAAM,QAAQ,KAAM,EAAE,MAAO,YAAY,QAAS,MAAM;KAC9D,OACE,oBAAC,QAAD;MAEE,cAAY,cAAc,EAAE;MAC5B,OAAO;OACL,UAAU;OACV,MAAM,GAAG,IAAI;OACb,WAAW;OACX,UAAU,OAAO,WAAW,SAAS;OACrC,OAAO,EAAE;OACT,YAAY,OAAO,WAAW,WAAW;MAC3C;gBAEC,EAAE;KACC,GAZC,CAYD;IAEV,CAAC;GACA,CAAA;EAEJ;;AAET,CAAC"}
|