@epilot/spark-ui 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/components/accordion.cjs +4 -4
  2. package/dist/components/accordion.cjs.map +1 -1
  3. package/dist/components/accordion.d.ts +1 -1
  4. package/dist/components/accordion.js +4 -4
  5. package/dist/components/accordion.js.map +1 -1
  6. package/dist/components/attachment.cjs +3 -3
  7. package/dist/components/attachment.cjs.map +1 -1
  8. package/dist/components/attachment.js +3 -3
  9. package/dist/components/attachment.js.map +1 -1
  10. package/dist/components/badge.cjs +1 -1
  11. package/dist/components/badge.cjs.map +1 -1
  12. package/dist/components/badge.js +1 -1
  13. package/dist/components/badge.js.map +1 -1
  14. package/dist/components/bar-chart.js +1 -1
  15. package/dist/components/box.cjs +1 -1
  16. package/dist/components/box.cjs.map +1 -1
  17. package/dist/components/box.d.cts +1 -1
  18. package/dist/components/box.d.ts +1 -1
  19. package/dist/components/box.js +1 -1
  20. package/dist/components/box.js.map +1 -1
  21. package/dist/components/breadcrumbs.cjs +1 -1
  22. package/dist/components/breadcrumbs.cjs.map +1 -1
  23. package/dist/components/breadcrumbs.js +1 -1
  24. package/dist/components/breadcrumbs.js.map +1 -1
  25. package/dist/components/bubble.cjs +1 -1
  26. package/dist/components/bubble.cjs.map +1 -1
  27. package/dist/components/bubble.d.cts +1 -1
  28. package/dist/components/bubble.d.ts +1 -1
  29. package/dist/components/bubble.js +1 -1
  30. package/dist/components/bubble.js.map +1 -1
  31. package/dist/components/button.d.cts +2 -2
  32. package/dist/components/button.d.ts +2 -2
  33. package/dist/components/callout.d.cts +1 -1
  34. package/dist/components/callout.d.ts +1 -1
  35. package/dist/components/card.cjs +2 -2
  36. package/dist/components/card.cjs.map +1 -1
  37. package/dist/components/card.d.cts +1 -1
  38. package/dist/components/card.d.ts +1 -1
  39. package/dist/components/card.js +2 -2
  40. package/dist/components/card.js.map +1 -1
  41. package/dist/components/checkbox.cjs +1 -1
  42. package/dist/components/checkbox.cjs.map +1 -1
  43. package/dist/components/checkbox.js +1 -1
  44. package/dist/components/checkbox.js.map +1 -1
  45. package/dist/components/day-of-week-picker.cjs +2 -2
  46. package/dist/components/day-of-week-picker.cjs.map +1 -1
  47. package/dist/components/day-of-week-picker.js +2 -2
  48. package/dist/components/day-of-week-picker.js.map +1 -1
  49. package/dist/components/device-card.cjs +2 -2
  50. package/dist/components/device-card.cjs.map +1 -1
  51. package/dist/components/device-card.js +2 -2
  52. package/dist/components/device-card.js.map +1 -1
  53. package/dist/components/drawer.cjs +1 -1
  54. package/dist/components/drawer.cjs.map +1 -1
  55. package/dist/components/drawer.js +1 -1
  56. package/dist/components/drawer.js.map +1 -1
  57. package/dist/components/dropdown-menu.cjs +4 -4
  58. package/dist/components/dropdown-menu.cjs.map +1 -1
  59. package/dist/components/dropdown-menu.js +4 -4
  60. package/dist/components/dropdown-menu.js.map +1 -1
  61. package/dist/components/empty-state.cjs +1 -1
  62. package/dist/components/empty-state.cjs.map +1 -1
  63. package/dist/components/empty-state.js +1 -1
  64. package/dist/components/empty-state.js.map +1 -1
  65. package/dist/components/feed-in-hero.cjs +1 -1
  66. package/dist/components/feed-in-hero.cjs.map +1 -1
  67. package/dist/components/feed-in-hero.js +1 -1
  68. package/dist/components/feed-in-hero.js.map +1 -1
  69. package/dist/components/field.cjs +1 -1
  70. package/dist/components/field.cjs.map +1 -1
  71. package/dist/components/field.js +1 -1
  72. package/dist/components/field.js.map +1 -1
  73. package/dist/components/info-block.cjs +2 -2
  74. package/dist/components/info-block.cjs.map +1 -1
  75. package/dist/components/info-block.js +2 -2
  76. package/dist/components/info-block.js.map +1 -1
  77. package/dist/components/input-group.cjs +1 -1
  78. package/dist/components/input-group.cjs.map +1 -1
  79. package/dist/components/input-group.d.cts +1 -1
  80. package/dist/components/input-group.d.ts +1 -1
  81. package/dist/components/input-group.js +1 -1
  82. package/dist/components/input-group.js.map +1 -1
  83. package/dist/components/input-otp.cjs +1 -1
  84. package/dist/components/input-otp.cjs.map +1 -1
  85. package/dist/components/input-otp.js +1 -1
  86. package/dist/components/input-otp.js.map +1 -1
  87. package/dist/components/input.cjs +1 -1
  88. package/dist/components/input.cjs.map +1 -1
  89. package/dist/components/input.js +1 -1
  90. package/dist/components/input.js.map +1 -1
  91. package/dist/components/line-chart.cjs +1 -1
  92. package/dist/components/line-chart.js +2 -2
  93. package/dist/components/list.cjs +10 -10
  94. package/dist/components/list.cjs.map +1 -1
  95. package/dist/components/list.d.cts +1 -1
  96. package/dist/components/list.d.ts +1 -1
  97. package/dist/components/list.js +10 -10
  98. package/dist/components/list.js.map +1 -1
  99. package/dist/components/message-scroller.cjs +1 -1
  100. package/dist/components/message-scroller.js +1 -1
  101. package/dist/components/page-header.d.cts +1 -1
  102. package/dist/components/page-header.d.ts +1 -1
  103. package/dist/components/power-history-chart.cjs +1 -1
  104. package/dist/components/power-history-chart.js +2 -2
  105. package/dist/components/power-history.cjs +1 -1
  106. package/dist/components/power-history.js +1 -1
  107. package/dist/components/power-network.cjs +7 -7
  108. package/dist/components/power-network.cjs.map +1 -1
  109. package/dist/components/power-network.js +7 -7
  110. package/dist/components/power-network.js.map +1 -1
  111. package/dist/components/schedule-list.cjs +3 -3
  112. package/dist/components/schedule-list.cjs.map +1 -1
  113. package/dist/components/schedule-list.js +3 -3
  114. package/dist/components/schedule-list.js.map +1 -1
  115. package/dist/components/select.cjs +2 -2
  116. package/dist/components/select.cjs.map +1 -1
  117. package/dist/components/select.js +2 -2
  118. package/dist/components/select.js.map +1 -1
  119. package/dist/components/series-legend.cjs +1 -1
  120. package/dist/components/series-legend.cjs.map +1 -1
  121. package/dist/components/series-legend.js +1 -1
  122. package/dist/components/series-legend.js.map +1 -1
  123. package/dist/components/skeleton.cjs +1 -1
  124. package/dist/components/skeleton.cjs.map +1 -1
  125. package/dist/components/skeleton.js +1 -1
  126. package/dist/components/skeleton.js.map +1 -1
  127. package/dist/components/stat-card.cjs +1 -1
  128. package/dist/components/stat-card.cjs.map +1 -1
  129. package/dist/components/stat-card.js +1 -1
  130. package/dist/components/stat-card.js.map +1 -1
  131. package/dist/components/stepper.cjs +3 -3
  132. package/dist/components/stepper.cjs.map +1 -1
  133. package/dist/components/stepper.js +3 -3
  134. package/dist/components/stepper.js.map +1 -1
  135. package/dist/components/textarea.cjs +1 -1
  136. package/dist/components/textarea.cjs.map +1 -1
  137. package/dist/components/textarea.js +1 -1
  138. package/dist/components/textarea.js.map +1 -1
  139. package/dist/components/time-picker.cjs +1 -1
  140. package/dist/components/time-picker.cjs.map +1 -1
  141. package/dist/components/time-picker.js +1 -1
  142. package/dist/components/time-picker.js.map +1 -1
  143. package/dist/components/toast.cjs +1 -1
  144. package/dist/components/toast.cjs.map +1 -1
  145. package/dist/components/toast.js +1 -1
  146. package/dist/components/toast.js.map +1 -1
  147. package/dist/styles/theme.css +1 -2
  148. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"power-network.cjs","names":["useReducedMotion","SectionTitle","NumberFlow","Skeleton","MaterialSymbol"],"sources":["../../src/components/power-network.tsx"],"sourcesContent":["import {\n createContext,\n type ReactElement,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useReducedMotion } from '../hooks/use-reduced-motion'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\nimport NumberFlow from './number-flow'\nimport { Skeleton } from './skeleton'\nimport { SectionTitle } from './typography'\n\n// --- Variants -----------------------------------------------------------\n\nexport type PowerSourceVariant = 'grid' | 'solar' | 'battery' | 'neutral'\n\ninterface SourceVariantStyles {\n activeClassName: string\n flowStroke: string\n initialDashGap: number\n}\n\nconst SOURCE_VARIANTS: Record<PowerSourceVariant, SourceVariantStyles> = {\n grid: {\n activeClassName: 'border-blue-a9 bg-blue-a1 shadow-blue-a4',\n flowStroke: 'var(--blue-11)',\n initialDashGap: 10,\n },\n solar: {\n activeClassName: 'border-orange-a9 bg-orange-a1 shadow-orange-a3',\n flowStroke: 'var(--orange-a9)',\n initialDashGap: 5,\n },\n battery: {\n activeClassName: 'border-green-a9 bg-green-a1 shadow-green-a3',\n flowStroke: 'var(--green-a9)',\n initialDashGap: 7,\n },\n neutral: {\n activeClassName: 'border-gray-a9 bg-gray-a1 shadow-gray-a3',\n flowStroke: 'var(--neutral-a9)',\n initialDashGap: 9,\n },\n}\n\nconst DEVICE_FLOW_STROKE = 'var(--neutral-a9)'\nconst DEVICE_INITIAL_DASH_GAP = 9\n\n// Drawn under every connector and revealed when nothing flows, so a 0 W node\n// still reads as wired up and online rather than disconnected.\nconst IDLE_STROKE = 'var(--neutral-a6)'\nconst SKELETON_STROKE = 'var(--neutral-a6)'\nconst STROKE_WIDTH = 2\nconst STRAIGHT_TOLERANCE_PX = 3\nconst SOURCE_HOME_OFFSET_STEP_PX = 10\nconst DEVICE_HOME_OFFSET_STEP_PX = 10\n\n// --- Flow animation parameters ------------------------------------------\n\nconst FLOW_DASH_SOLID = 1\nconst FLOW_GAP_MAX = 14\nconst FLOW_GAP_MIN = 5\nconst FLOW_SPEED_MIN = 10\nconst FLOW_SPEED_MAX = 56\nconst FLOW_SMOOTHING = 5.5\n\ninterface FlowVisual {\n gap: number\n speed: number\n opacity: number\n}\n\ninterface FlowAnimationState extends FlowVisual {\n offset: number\n}\n\n// --- Context ------------------------------------------------------------\n\ntype NodeKind = 'source' | 'device' | 'home'\n\ninterface RegisteredNode {\n id: string\n kind: NodeKind\n variant?: PowerSourceVariant\n watts: number\n element: HTMLDivElement | null\n}\n\ninterface PowerNetworkContextValue {\n loading: boolean\n register: (node: RegisteredNode) => void\n unregister: (id: string) => void\n totalSourceWatts: number\n}\n\nconst PowerNetworkContext = createContext<PowerNetworkContextValue | null>(null)\n\nfunction usePowerNetwork(): PowerNetworkContextValue {\n const ctx = useContext(PowerNetworkContext)\n if (!ctx) {\n throw new Error(\n 'Power network sub-components must be used inside <PowerNetworkDiagram>.',\n )\n }\n return ctx\n}\n\n// --- Geometry helpers ---------------------------------------------------\n\ninterface Point {\n x: number\n y: number\n}\n\nfunction anchorTop(rect: DOMRect, containerRect: DOMRect): Point {\n return {\n x: rect.left + rect.width / 2 - containerRect.left,\n y: rect.top - containerRect.top,\n }\n}\n\nfunction anchorBottom(rect: DOMRect, containerRect: DOMRect): Point {\n return {\n x: rect.left + rect.width / 2 - containerRect.left,\n y: rect.bottom - containerRect.top,\n }\n}\n\nfunction buildAdaptivePath(from: Point, to: Point): string {\n if (Math.abs(from.x - to.x) < STRAIGHT_TOLERANCE_PX) {\n return `M ${from.x} ${from.y} L ${to.x} ${to.y}`\n }\n const dy = to.y - from.y\n const c1y = from.y + dy * 0.56\n const c2y = to.y - dy * 0.58\n return `M ${from.x} ${from.y} C ${from.x} ${c1y} ${to.x} ${c2y} ${to.x} ${to.y}`\n}\n\n// --- PowerNetworkDiagram ------------------------------------------------\n\nexport interface PowerNetworkDiagramProps {\n title?: string\n loading?: boolean\n className?: string\n children?: ReactNode\n}\n\ninterface ConnectorPath {\n id: string\n d: string\n kind: NodeKind\n variant?: PowerSourceVariant\n reversed: boolean\n}\n\nfunction connectorPathsEqual(a: ConnectorPath[], b: ConnectorPath[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n const ap = a[i]\n const bp = b[i]\n if (!ap || !bp) return false\n if (\n ap.id !== bp.id ||\n ap.d !== bp.d ||\n ap.variant !== bp.variant ||\n ap.kind !== bp.kind ||\n ap.reversed !== bp.reversed\n ) {\n return false\n }\n }\n return true\n}\n\nexport function PowerNetworkDiagram({\n title,\n loading = false,\n className,\n children,\n}: PowerNetworkDiagramProps): ReactElement {\n const containerRef = useRef<HTMLDivElement>(null)\n const reducedMotion = useReducedMotion()\n const [nodes, setNodes] = useState<Map<string, RegisteredNode>>(\n () => new Map(),\n )\n const [connectorPaths, setConnectorPaths] = useState<ConnectorPath[]>([])\n const connectorPathsRef = useRef<ConnectorPath[]>([])\n const flowPathRefs = useRef<Record<string, SVGPathElement | null>>({})\n const idlePathRefs = useRef<Record<string, SVGPathElement | null>>({})\n const currentFlowVisualsRef = useRef<Record<string, FlowAnimationState>>({})\n const targetFlowVisualsRef = useRef<Record<string, FlowVisual>>({})\n\n const register = useCallback((node: RegisteredNode) => {\n setNodes((prev) => {\n const existing = prev.get(node.id)\n if (\n existing &&\n existing.kind === node.kind &&\n existing.variant === node.variant &&\n existing.watts === node.watts &&\n existing.element === node.element\n ) {\n return prev\n }\n const next = new Map(prev)\n next.set(node.id, node)\n return next\n })\n }, [])\n\n const unregister = useCallback((id: string) => {\n setNodes((prev) => {\n if (!prev.has(id)) return prev\n const next = new Map(prev)\n next.delete(id)\n return next\n })\n }, [])\n\n const totalSourceWatts = useMemo(() => {\n let total = 0\n for (const node of nodes.values()) {\n if (node.kind === 'source') total += node.watts\n }\n return total\n }, [nodes])\n\n useEffect(() => {\n const container = containerRef.current\n if (!container) return\n\n let homeNode: RegisteredNode | undefined\n const sources: RegisteredNode[] = []\n const devices: RegisteredNode[] = []\n for (const node of nodes.values()) {\n if (node.kind === 'home') {\n homeNode = node\n } else if (node.element) {\n if (node.kind === 'source') sources.push(node)\n else devices.push(node)\n }\n }\n\n const updatePaths = () => {\n if (!homeNode || !homeNode.element) {\n setConnectorPaths((prev) => (prev.length === 0 ? prev : []))\n return\n }\n const containerRect = container.getBoundingClientRect()\n const homeRect = homeNode.element.getBoundingClientRect()\n const homeTop = anchorTop(homeRect, containerRect)\n const homeBottom = anchorBottom(homeRect, containerRect)\n const next: ConnectorPath[] = []\n\n const sourceCount = sources.length\n const deviceCount = devices.length\n\n sources.forEach((source, index) => {\n if (!source.element) return\n const from = anchorBottom(\n source.element.getBoundingClientRect(),\n containerRect,\n )\n const offsetX =\n SOURCE_HOME_OFFSET_STEP_PX * (index - (sourceCount - 1) / 2)\n const to = { x: homeTop.x + offsetX, y: homeTop.y }\n next.push({\n id: source.id,\n d: buildAdaptivePath(from, to),\n kind: 'source',\n variant: source.variant,\n reversed: source.watts < 0,\n })\n })\n\n devices.forEach((device, index) => {\n if (!device.element) return\n const to = anchorTop(\n device.element.getBoundingClientRect(),\n containerRect,\n )\n const offsetX =\n DEVICE_HOME_OFFSET_STEP_PX * (index - (deviceCount - 1) / 2)\n const from = { x: homeBottom.x + offsetX, y: homeBottom.y }\n next.push({\n id: device.id,\n d: buildAdaptivePath(from, to),\n kind: 'device',\n reversed: false,\n })\n })\n\n setConnectorPaths((prev) =>\n connectorPathsEqual(prev, next) ? prev : next,\n )\n }\n\n updatePaths()\n\n const resizeObserver = new ResizeObserver(updatePaths)\n resizeObserver.observe(container)\n for (const node of nodes.values()) {\n if (node.element) resizeObserver.observe(node.element)\n }\n window.addEventListener('resize', updatePaths)\n\n return () => {\n resizeObserver.disconnect()\n window.removeEventListener('resize', updatePaths)\n }\n }, [nodes])\n\n useEffect(() => {\n connectorPathsRef.current = connectorPaths\n }, [connectorPaths])\n\n useEffect(() => {\n const targets: Record<string, FlowVisual> = {}\n const homeWatts = totalSourceWatts\n for (const node of nodes.values()) {\n if (node.kind === 'home') continue\n const watts = node.watts\n const relative =\n homeWatts > 0\n ? Math.max(0, Math.min(1, Math.abs(watts) / homeWatts))\n : 0\n targets[node.id] = {\n gap: FLOW_GAP_MAX - (FLOW_GAP_MAX - FLOW_GAP_MIN) * relative,\n speed: FLOW_SPEED_MIN + (FLOW_SPEED_MAX - FLOW_SPEED_MIN) * relative,\n opacity: Math.abs(watts) > 0 ? 1 : 0,\n }\n }\n targetFlowVisualsRef.current = targets\n }, [nodes, totalSourceWatts])\n\n useEffect(() => {\n if (loading || reducedMotion) return\n let frameId = 0\n let lastFrameTime = performance.now()\n\n const animate = (now: number) => {\n const deltaSeconds = Math.max(0.001, (now - lastFrameTime) / 1000)\n lastFrameTime = now\n const smoothing = 1 - Math.exp(-FLOW_SMOOTHING * deltaSeconds)\n\n for (const connectorPath of connectorPathsRef.current) {\n const id = connectorPath.id\n const target = targetFlowVisualsRef.current[id]\n const pathElement = flowPathRefs.current[id]\n if (!target || !pathElement) continue\n\n const current = currentFlowVisualsRef.current[id] ?? {\n gap: target.gap,\n offset: 0,\n speed: target.speed,\n opacity: target.opacity,\n }\n\n current.gap += (target.gap - current.gap) * smoothing\n current.speed += (target.speed - current.speed) * smoothing\n current.opacity += (target.opacity - current.opacity) * smoothing\n\n const dashCycle = FLOW_DASH_SOLID + Math.max(0.001, current.gap)\n const direction = connectorPath.reversed ? 1 : -1\n const nextOffset =\n current.offset + direction * current.speed * deltaSeconds\n const wrappedOffset = ((nextOffset % dashCycle) + dashCycle) % dashCycle\n current.offset = wrappedOffset - dashCycle\n\n pathElement.setAttribute(\n 'stroke-dasharray',\n `${FLOW_DASH_SOLID} ${current.gap.toFixed(3)}`,\n )\n pathElement.setAttribute('stroke-dashoffset', current.offset.toFixed(3))\n pathElement.setAttribute('opacity', current.opacity.toFixed(3))\n idlePathRefs.current[id]?.setAttribute(\n 'opacity',\n (1 - current.opacity).toFixed(3),\n )\n\n currentFlowVisualsRef.current[id] = current\n }\n\n frameId = window.requestAnimationFrame(animate)\n }\n\n frameId = window.requestAnimationFrame(animate)\n return () => window.cancelAnimationFrame(frameId)\n }, [loading, reducedMotion])\n\n // Reduced motion: freeze the marching dashes but keep the dash gap tracking\n // live magnitude, and let opacity fade in/out via the CSS transition on each\n // flow path (see the flow <path> style below).\n useEffect(() => {\n if (loading || !reducedMotion) return\n for (const connectorPath of connectorPathsRef.current) {\n const target = targetFlowVisualsRef.current[connectorPath.id]\n const pathElement = flowPathRefs.current[connectorPath.id]\n if (!target || !pathElement) continue\n pathElement.setAttribute(\n 'stroke-dasharray',\n `${FLOW_DASH_SOLID} ${target.gap.toFixed(3)}`,\n )\n pathElement.setAttribute('stroke-dashoffset', '0')\n pathElement.setAttribute('opacity', target.opacity.toFixed(3))\n idlePathRefs.current[connectorPath.id]?.setAttribute(\n 'opacity',\n (1 - target.opacity).toFixed(3),\n )\n }\n }, [loading, reducedMotion, nodes, totalSourceWatts])\n\n const contextValue = useMemo<PowerNetworkContextValue>(\n () => ({ loading, register, unregister, totalSourceWatts }),\n [loading, register, unregister, totalSourceWatts],\n )\n\n return (\n <PowerNetworkContext.Provider value={contextValue}>\n <div\n ref={containerRef}\n className={cn('@container relative flex flex-col', className)}\n >\n {title && <SectionTitle>{title}</SectionTitle>}\n {connectorPaths.length > 0 && (\n <svg className=\"pointer-events-none absolute inset-0 size-full\">\n {connectorPaths.map((path) => {\n if (loading) {\n return (\n <path\n key={path.id}\n d={path.d}\n fill=\"none\"\n stroke={SKELETON_STROKE}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n />\n )\n }\n\n const flowStroke =\n path.kind === 'source' && path.variant\n ? SOURCE_VARIANTS[path.variant].flowStroke\n : DEVICE_FLOW_STROKE\n const initialDashGap =\n path.kind === 'source' && path.variant\n ? SOURCE_VARIANTS[path.variant].initialDashGap\n : DEVICE_INITIAL_DASH_GAP\n\n return (\n <g key={path.id}>\n <path\n d={path.d}\n fill=\"none\"\n stroke={IDLE_STROKE}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n ref={(el) => {\n idlePathRefs.current[path.id] = el\n }}\n opacity={1}\n style={\n reducedMotion\n ? { transition: 'opacity 300ms ease' }\n : undefined\n }\n />\n <path\n d={path.d}\n fill=\"none\"\n stroke={flowStroke}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n ref={(el) => {\n flowPathRefs.current[path.id] = el\n }}\n strokeDasharray={`${FLOW_DASH_SOLID} ${initialDashGap}`}\n strokeDashoffset={0}\n opacity={0}\n style={\n reducedMotion\n ? { transition: 'opacity 300ms ease' }\n : undefined\n }\n />\n </g>\n )\n })}\n </svg>\n )}\n <div className=\"flex flex-col gap-y-14\">{children}</div>\n </div>\n </PowerNetworkContext.Provider>\n )\n}\n\n// --- Row containers -----------------------------------------------------\n\nexport interface PowerNetworkRowProps {\n className?: string\n children?: ReactNode\n}\n\nexport function PowerNetworkSources({\n className,\n children,\n}: PowerNetworkRowProps): ReactElement {\n return (\n <div\n className={cn(\n 'flex items-center justify-center gap-[clamp(1.5rem,10cqw,4rem)]',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\nexport function PowerNetworkDevices({\n className,\n children,\n}: PowerNetworkRowProps): ReactElement {\n return (\n <div\n className={cn(\n 'flex items-center justify-center gap-[clamp(1.5rem,10cqw,4rem)]',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\n// --- PowerSource --------------------------------------------------------\n\nexport interface PowerSourceProps {\n id?: string\n watts: number\n icon: ReactNode\n variant?: PowerSourceVariant\n className?: string\n /**\n * Optional 0–100 progress (e.g. battery SoC). When set, a thin colored arc\n * is drawn around the icon circle as a non-intrusive indicator.\n */\n progress?: number\n /** Keeps the variant colors at 0 W, for a source that is online but idle. */\n standby?: boolean\n}\n\nexport function PowerSource({\n id: idProp,\n watts,\n icon,\n variant = 'neutral',\n className,\n progress,\n standby = false,\n}: PowerSourceProps): ReactElement {\n const autoId = useId()\n const id = idProp ?? autoId\n const { register, unregister, loading } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (!element) return\n register({ id, kind: 'source', variant, watts, element })\n }, [element, register, id, variant, watts])\n\n useEffect(() => {\n return () => unregister(id)\n }, [unregister, id])\n\n if (loading) {\n return <SourceSkeleton anchorRef={setElement} />\n }\n\n const isActive = watts !== 0\n const isColored = isActive || standby\n const variantStyles = SOURCE_VARIANTS[variant]\n const clampedProgress =\n progress == null ? null : Math.max(0, Math.min(100, progress))\n const showRing = clampedProgress != null\n const RING_RADIUS = 48\n const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS\n const ringDash =\n clampedProgress != null ? (clampedProgress / 100) * RING_CIRCUMFERENCE : 0\n\n return (\n <div\n className={cn(\n 'flex flex-col items-center justify-center gap-1 transition-opacity duration-500',\n !isColored && 'opacity-45',\n className,\n )}\n >\n <NumberFlow\n value={Math.abs(watts)}\n suffix=\"W\"\n className=\"mt-1 text-sm leading-none font-medium\"\n />\n <div\n ref={setElement}\n className={cn(\n 'relative flex aspect-square items-center justify-center rounded-circle border p-4 shadow-sm',\n isColored\n ? variantStyles.activeClassName\n : 'border-gray bg-gray-a3 shadow-gray-a4 saturate-0',\n )}\n >\n {showRing && (\n <svg\n className=\"pointer-events-none absolute inset-0 size-full -rotate-90\"\n viewBox=\"0 0 100 100\"\n aria-hidden=\"true\"\n >\n <circle\n cx=\"50\"\n cy=\"50\"\n r={RING_RADIUS}\n fill=\"none\"\n stroke={variantStyles.flowStroke}\n strokeWidth=\"4\"\n strokeLinecap=\"round\"\n strokeDasharray={`${ringDash} ${RING_CIRCUMFERENCE}`}\n opacity={isColored ? 1 : 0.5}\n />\n </svg>\n )}\n {icon}\n </div>\n </div>\n )\n}\n\nfunction SourceSkeleton({\n anchorRef,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n}): ReactElement {\n return (\n <div className=\"flex flex-col items-center justify-center gap-1\">\n <Skeleton className=\"h-3.5 w-10\" />\n <div\n ref={anchorRef}\n className=\"flex aspect-square animate-pulse items-center justify-center rounded-circle border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none\"\n >\n <div className=\"size-7\" />\n </div>\n </div>\n )\n}\n\n// --- PowerDevice --------------------------------------------------------\n\nexport interface PowerDeviceProps {\n id?: string\n watts: number\n icon: ReactNode\n className?: string\n}\n\nexport function PowerDevice({\n id: idProp,\n watts,\n icon,\n className,\n}: PowerDeviceProps): ReactElement {\n const autoId = useId()\n const id = idProp ?? autoId\n const { register, unregister, loading } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (!element) return\n register({ id, kind: 'device', watts, element })\n }, [element, register, id, watts])\n\n useEffect(() => {\n return () => unregister(id)\n }, [unregister, id])\n\n if (loading) {\n return <DeviceSkeleton anchorRef={setElement} />\n }\n\n return (\n <div\n className={cn(\n 'flex flex-col items-center justify-center gap-1',\n className,\n )}\n >\n <div\n ref={setElement}\n className=\"flex aspect-square items-center justify-center rounded-circle border border-gray bg-gray-a1 p-4\"\n >\n {icon}\n </div>\n <NumberFlow\n value={watts}\n suffix=\"W\"\n className=\"text-sm font-medium tabular-nums\"\n />\n </div>\n )\n}\n\nfunction DeviceSkeleton({\n anchorRef,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n}): ReactElement {\n return (\n <div className=\"flex flex-col items-center justify-center gap-1\">\n <div\n ref={anchorRef}\n className=\"flex aspect-square animate-pulse items-center justify-center rounded-circle border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none\"\n >\n <div className=\"size-5\" />\n </div>\n <Skeleton className=\"h-3.5 w-10\" />\n </div>\n )\n}\n\n// --- PowerNetworkHome ---------------------------------------------------\n\nexport interface PowerNetworkHomeProps {\n /** Override total power. Defaults to the sum of registered source watts. */\n watts?: number\n /** Self-sufficiency ratio in 0..1. Renders a green ring when provided. */\n autarky?: number\n className?: string\n}\n\nexport function PowerNetworkHome({\n watts,\n autarky,\n className,\n}: PowerNetworkHomeProps): ReactElement {\n const autoId = useId()\n const { register, unregister, loading, totalSourceWatts } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n const effectiveWatts = watts ?? totalSourceWatts\n\n useEffect(() => {\n if (!element) return\n register({ id: autoId, kind: 'home', watts: effectiveWatts, element })\n }, [element, register, autoId, effectiveWatts])\n\n useEffect(() => {\n return () => unregister(autoId)\n }, [unregister, autoId])\n\n if (loading) {\n return <HomeSkeleton anchorRef={setElement} className={className} />\n }\n\n return (\n <HomeSection\n anchorRef={setElement}\n autarky={autarky ?? null}\n watts={effectiveWatts}\n className={className}\n />\n )\n}\n\nfunction HomeSection({\n anchorRef,\n autarky,\n className,\n watts,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n autarky: number | null\n className?: string\n watts: number\n}): ReactElement {\n const reducedMotion = useReducedMotion()\n const targetAutarky =\n autarky == null ? 0 : Math.max(0, Math.min(100, autarky * 100))\n const [animatedAutarky, setAnimatedAutarky] = useState(targetAutarky)\n const animatedAutarkyRef = useRef(animatedAutarky)\n const nodeSize = 78\n const ringStrokeWidth = 2\n const radius = (nodeSize - ringStrokeWidth) / 2\n const circumference = 2 * Math.PI * radius\n const progressLength = (animatedAutarky / 100) * circumference\n\n useEffect(() => {\n animatedAutarkyRef.current = animatedAutarky\n }, [animatedAutarky])\n\n useEffect(() => {\n if (reducedMotion) {\n animatedAutarkyRef.current = targetAutarky\n setAnimatedAutarky(targetAutarky)\n return\n }\n\n let frameId = 0\n let lastTime = performance.now()\n\n const animate = (now: number) => {\n const deltaSeconds = Math.max(0.001, (now - lastTime) / 1000)\n lastTime = now\n const smoothing = 1 - Math.exp(-7 * deltaSeconds)\n const next =\n animatedAutarkyRef.current +\n (targetAutarky - animatedAutarkyRef.current) * smoothing\n\n animatedAutarkyRef.current = next\n setAnimatedAutarky(next)\n\n if (Math.abs(targetAutarky - next) > 0.05) {\n frameId = window.requestAnimationFrame(animate)\n } else {\n animatedAutarkyRef.current = targetAutarky\n setAnimatedAutarky(targetAutarky)\n }\n }\n\n frameId = window.requestAnimationFrame(animate)\n return () => window.cancelAnimationFrame(frameId)\n }, [targetAutarky, reducedMotion])\n\n return (\n <div\n ref={anchorRef}\n className={cn('relative mx-auto size-[78px]', className)}\n >\n {autarky != null && (\n <svg\n className=\"absolute inset-0 size-full -rotate-90\"\n viewBox={`0 0 ${nodeSize} ${nodeSize}`}\n >\n <circle\n cx={nodeSize / 2}\n cy={nodeSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"var(--neutral-a5)\"\n strokeWidth={ringStrokeWidth}\n />\n {progressLength > 0 && (\n <circle\n cx={nodeSize / 2}\n cy={nodeSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"var(--green-a9)\"\n strokeLinecap=\"round\"\n strokeWidth={ringStrokeWidth}\n strokeDasharray={`${progressLength} ${circumference}`}\n />\n )}\n </svg>\n )}\n <div className=\"absolute inset-0 flex flex-col items-center justify-center rounded-circle border border-gray-a5 shadow-sm shadow-gray-a1\">\n <MaterialSymbol className=\"text-[14px]\" name=\"home\" />\n <NumberFlow\n value={watts}\n suffix=\"W\"\n className=\"mt-1 text-sm leading-none font-medium\"\n />\n </div>\n </div>\n )\n}\n\nfunction HomeSkeleton({\n anchorRef,\n className,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n className?: string\n}): ReactElement {\n return (\n <div\n ref={anchorRef}\n className={cn(\n 'mx-auto size-[78px] animate-pulse rounded-circle border border-gray motion-reduce:animate-none',\n className,\n )}\n />\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"power-network.cjs","names":["useReducedMotion","SectionTitle","NumberFlow","Skeleton","MaterialSymbol"],"sources":["../../src/components/power-network.tsx"],"sourcesContent":["import {\n createContext,\n type ReactElement,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useReducedMotion } from '../hooks/use-reduced-motion'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\nimport NumberFlow from './number-flow'\nimport { Skeleton } from './skeleton'\nimport { SectionTitle } from './typography'\n\n// --- Variants -----------------------------------------------------------\n\nexport type PowerSourceVariant = 'grid' | 'solar' | 'battery' | 'neutral'\n\ninterface SourceVariantStyles {\n activeClassName: string\n flowStroke: string\n initialDashGap: number\n}\n\nconst SOURCE_VARIANTS: Record<PowerSourceVariant, SourceVariantStyles> = {\n grid: {\n activeClassName: 'border-blue-a9 bg-blue-a1 shadow-blue-a4',\n flowStroke: 'var(--blue-11)',\n initialDashGap: 10,\n },\n solar: {\n activeClassName: 'border-orange-a9 bg-orange-a1 shadow-orange-a3',\n flowStroke: 'var(--orange-a9)',\n initialDashGap: 5,\n },\n battery: {\n activeClassName: 'border-green-a9 bg-green-a1 shadow-green-a3',\n flowStroke: 'var(--green-a9)',\n initialDashGap: 7,\n },\n neutral: {\n activeClassName: 'border-gray-a9 bg-gray-a1 shadow-gray-a3',\n flowStroke: 'var(--neutral-a9)',\n initialDashGap: 9,\n },\n}\n\nconst DEVICE_FLOW_STROKE = 'var(--neutral-a9)'\nconst DEVICE_INITIAL_DASH_GAP = 9\n\n// Drawn under every connector and revealed when nothing flows, so a 0 W node\n// still reads as wired up and online rather than disconnected.\nconst IDLE_STROKE = 'var(--neutral-a6)'\nconst SKELETON_STROKE = 'var(--neutral-a6)'\nconst STROKE_WIDTH = 2\nconst STRAIGHT_TOLERANCE_PX = 3\nconst SOURCE_HOME_OFFSET_STEP_PX = 10\nconst DEVICE_HOME_OFFSET_STEP_PX = 10\n\n// --- Flow animation parameters ------------------------------------------\n\nconst FLOW_DASH_SOLID = 1\nconst FLOW_GAP_MAX = 14\nconst FLOW_GAP_MIN = 5\nconst FLOW_SPEED_MIN = 10\nconst FLOW_SPEED_MAX = 56\nconst FLOW_SMOOTHING = 5.5\n\ninterface FlowVisual {\n gap: number\n speed: number\n opacity: number\n}\n\ninterface FlowAnimationState extends FlowVisual {\n offset: number\n}\n\n// --- Context ------------------------------------------------------------\n\ntype NodeKind = 'source' | 'device' | 'home'\n\ninterface RegisteredNode {\n id: string\n kind: NodeKind\n variant?: PowerSourceVariant\n watts: number\n element: HTMLDivElement | null\n}\n\ninterface PowerNetworkContextValue {\n loading: boolean\n register: (node: RegisteredNode) => void\n unregister: (id: string) => void\n totalSourceWatts: number\n}\n\nconst PowerNetworkContext = createContext<PowerNetworkContextValue | null>(null)\n\nfunction usePowerNetwork(): PowerNetworkContextValue {\n const ctx = useContext(PowerNetworkContext)\n if (!ctx) {\n throw new Error(\n 'Power network sub-components must be used inside <PowerNetworkDiagram>.',\n )\n }\n return ctx\n}\n\n// --- Geometry helpers ---------------------------------------------------\n\ninterface Point {\n x: number\n y: number\n}\n\nfunction anchorTop(rect: DOMRect, containerRect: DOMRect): Point {\n return {\n x: rect.left + rect.width / 2 - containerRect.left,\n y: rect.top - containerRect.top,\n }\n}\n\nfunction anchorBottom(rect: DOMRect, containerRect: DOMRect): Point {\n return {\n x: rect.left + rect.width / 2 - containerRect.left,\n y: rect.bottom - containerRect.top,\n }\n}\n\nfunction buildAdaptivePath(from: Point, to: Point): string {\n if (Math.abs(from.x - to.x) < STRAIGHT_TOLERANCE_PX) {\n return `M ${from.x} ${from.y} L ${to.x} ${to.y}`\n }\n const dy = to.y - from.y\n const c1y = from.y + dy * 0.56\n const c2y = to.y - dy * 0.58\n return `M ${from.x} ${from.y} C ${from.x} ${c1y} ${to.x} ${c2y} ${to.x} ${to.y}`\n}\n\n// --- PowerNetworkDiagram ------------------------------------------------\n\nexport interface PowerNetworkDiagramProps {\n title?: string\n loading?: boolean\n className?: string\n children?: ReactNode\n}\n\ninterface ConnectorPath {\n id: string\n d: string\n kind: NodeKind\n variant?: PowerSourceVariant\n reversed: boolean\n}\n\nfunction connectorPathsEqual(a: ConnectorPath[], b: ConnectorPath[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n const ap = a[i]\n const bp = b[i]\n if (!ap || !bp) return false\n if (\n ap.id !== bp.id ||\n ap.d !== bp.d ||\n ap.variant !== bp.variant ||\n ap.kind !== bp.kind ||\n ap.reversed !== bp.reversed\n ) {\n return false\n }\n }\n return true\n}\n\nexport function PowerNetworkDiagram({\n title,\n loading = false,\n className,\n children,\n}: PowerNetworkDiagramProps): ReactElement {\n const containerRef = useRef<HTMLDivElement>(null)\n const reducedMotion = useReducedMotion()\n const [nodes, setNodes] = useState<Map<string, RegisteredNode>>(\n () => new Map(),\n )\n const [connectorPaths, setConnectorPaths] = useState<ConnectorPath[]>([])\n const connectorPathsRef = useRef<ConnectorPath[]>([])\n const flowPathRefs = useRef<Record<string, SVGPathElement | null>>({})\n const idlePathRefs = useRef<Record<string, SVGPathElement | null>>({})\n const currentFlowVisualsRef = useRef<Record<string, FlowAnimationState>>({})\n const targetFlowVisualsRef = useRef<Record<string, FlowVisual>>({})\n\n const register = useCallback((node: RegisteredNode) => {\n setNodes((prev) => {\n const existing = prev.get(node.id)\n if (\n existing &&\n existing.kind === node.kind &&\n existing.variant === node.variant &&\n existing.watts === node.watts &&\n existing.element === node.element\n ) {\n return prev\n }\n const next = new Map(prev)\n next.set(node.id, node)\n return next\n })\n }, [])\n\n const unregister = useCallback((id: string) => {\n setNodes((prev) => {\n if (!prev.has(id)) return prev\n const next = new Map(prev)\n next.delete(id)\n return next\n })\n }, [])\n\n const totalSourceWatts = useMemo(() => {\n let total = 0\n for (const node of nodes.values()) {\n if (node.kind === 'source') total += node.watts\n }\n return total\n }, [nodes])\n\n useEffect(() => {\n const container = containerRef.current\n if (!container) return\n\n let homeNode: RegisteredNode | undefined\n const sources: RegisteredNode[] = []\n const devices: RegisteredNode[] = []\n for (const node of nodes.values()) {\n if (node.kind === 'home') {\n homeNode = node\n } else if (node.element) {\n if (node.kind === 'source') sources.push(node)\n else devices.push(node)\n }\n }\n\n const updatePaths = () => {\n if (!homeNode || !homeNode.element) {\n setConnectorPaths((prev) => (prev.length === 0 ? prev : []))\n return\n }\n const containerRect = container.getBoundingClientRect()\n const homeRect = homeNode.element.getBoundingClientRect()\n const homeTop = anchorTop(homeRect, containerRect)\n const homeBottom = anchorBottom(homeRect, containerRect)\n const next: ConnectorPath[] = []\n\n const sourceCount = sources.length\n const deviceCount = devices.length\n\n sources.forEach((source, index) => {\n if (!source.element) return\n const from = anchorBottom(\n source.element.getBoundingClientRect(),\n containerRect,\n )\n const offsetX =\n SOURCE_HOME_OFFSET_STEP_PX * (index - (sourceCount - 1) / 2)\n const to = { x: homeTop.x + offsetX, y: homeTop.y }\n next.push({\n id: source.id,\n d: buildAdaptivePath(from, to),\n kind: 'source',\n variant: source.variant,\n reversed: source.watts < 0,\n })\n })\n\n devices.forEach((device, index) => {\n if (!device.element) return\n const to = anchorTop(\n device.element.getBoundingClientRect(),\n containerRect,\n )\n const offsetX =\n DEVICE_HOME_OFFSET_STEP_PX * (index - (deviceCount - 1) / 2)\n const from = { x: homeBottom.x + offsetX, y: homeBottom.y }\n next.push({\n id: device.id,\n d: buildAdaptivePath(from, to),\n kind: 'device',\n reversed: false,\n })\n })\n\n setConnectorPaths((prev) =>\n connectorPathsEqual(prev, next) ? prev : next,\n )\n }\n\n updatePaths()\n\n const resizeObserver = new ResizeObserver(updatePaths)\n resizeObserver.observe(container)\n for (const node of nodes.values()) {\n if (node.element) resizeObserver.observe(node.element)\n }\n window.addEventListener('resize', updatePaths)\n\n return () => {\n resizeObserver.disconnect()\n window.removeEventListener('resize', updatePaths)\n }\n }, [nodes])\n\n useEffect(() => {\n connectorPathsRef.current = connectorPaths\n }, [connectorPaths])\n\n useEffect(() => {\n const targets: Record<string, FlowVisual> = {}\n const homeWatts = totalSourceWatts\n for (const node of nodes.values()) {\n if (node.kind === 'home') continue\n const watts = node.watts\n const relative =\n homeWatts > 0\n ? Math.max(0, Math.min(1, Math.abs(watts) / homeWatts))\n : 0\n targets[node.id] = {\n gap: FLOW_GAP_MAX - (FLOW_GAP_MAX - FLOW_GAP_MIN) * relative,\n speed: FLOW_SPEED_MIN + (FLOW_SPEED_MAX - FLOW_SPEED_MIN) * relative,\n opacity: Math.abs(watts) > 0 ? 1 : 0,\n }\n }\n targetFlowVisualsRef.current = targets\n }, [nodes, totalSourceWatts])\n\n useEffect(() => {\n if (loading || reducedMotion) return\n let frameId = 0\n let lastFrameTime = performance.now()\n\n const animate = (now: number) => {\n const deltaSeconds = Math.max(0.001, (now - lastFrameTime) / 1000)\n lastFrameTime = now\n const smoothing = 1 - Math.exp(-FLOW_SMOOTHING * deltaSeconds)\n\n for (const connectorPath of connectorPathsRef.current) {\n const id = connectorPath.id\n const target = targetFlowVisualsRef.current[id]\n const pathElement = flowPathRefs.current[id]\n if (!target || !pathElement) continue\n\n const current = currentFlowVisualsRef.current[id] ?? {\n gap: target.gap,\n offset: 0,\n speed: target.speed,\n opacity: target.opacity,\n }\n\n current.gap += (target.gap - current.gap) * smoothing\n current.speed += (target.speed - current.speed) * smoothing\n current.opacity += (target.opacity - current.opacity) * smoothing\n\n const dashCycle = FLOW_DASH_SOLID + Math.max(0.001, current.gap)\n const direction = connectorPath.reversed ? 1 : -1\n const nextOffset =\n current.offset + direction * current.speed * deltaSeconds\n const wrappedOffset = ((nextOffset % dashCycle) + dashCycle) % dashCycle\n current.offset = wrappedOffset - dashCycle\n\n pathElement.setAttribute(\n 'stroke-dasharray',\n `${FLOW_DASH_SOLID} ${current.gap.toFixed(3)}`,\n )\n pathElement.setAttribute('stroke-dashoffset', current.offset.toFixed(3))\n pathElement.setAttribute('opacity', current.opacity.toFixed(3))\n idlePathRefs.current[id]?.setAttribute(\n 'opacity',\n (1 - current.opacity).toFixed(3),\n )\n\n currentFlowVisualsRef.current[id] = current\n }\n\n frameId = window.requestAnimationFrame(animate)\n }\n\n frameId = window.requestAnimationFrame(animate)\n return () => window.cancelAnimationFrame(frameId)\n }, [loading, reducedMotion])\n\n // Reduced motion: freeze the marching dashes but keep the dash gap tracking\n // live magnitude, and let opacity fade in/out via the CSS transition on each\n // flow path (see the flow <path> style below).\n useEffect(() => {\n if (loading || !reducedMotion) return\n for (const connectorPath of connectorPathsRef.current) {\n const target = targetFlowVisualsRef.current[connectorPath.id]\n const pathElement = flowPathRefs.current[connectorPath.id]\n if (!target || !pathElement) continue\n pathElement.setAttribute(\n 'stroke-dasharray',\n `${FLOW_DASH_SOLID} ${target.gap.toFixed(3)}`,\n )\n pathElement.setAttribute('stroke-dashoffset', '0')\n pathElement.setAttribute('opacity', target.opacity.toFixed(3))\n idlePathRefs.current[connectorPath.id]?.setAttribute(\n 'opacity',\n (1 - target.opacity).toFixed(3),\n )\n }\n }, [loading, reducedMotion, nodes, totalSourceWatts])\n\n const contextValue = useMemo<PowerNetworkContextValue>(\n () => ({ loading, register, unregister, totalSourceWatts }),\n [loading, register, unregister, totalSourceWatts],\n )\n\n return (\n <PowerNetworkContext.Provider value={contextValue}>\n <div\n ref={containerRef}\n className={cn('@container relative flex flex-col', className)}\n >\n {title && <SectionTitle>{title}</SectionTitle>}\n {connectorPaths.length > 0 && (\n <svg className=\"pointer-events-none absolute inset-0 size-full\">\n {connectorPaths.map((path) => {\n if (loading) {\n return (\n <path\n key={path.id}\n d={path.d}\n fill=\"none\"\n stroke={SKELETON_STROKE}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n />\n )\n }\n\n const flowStroke =\n path.kind === 'source' && path.variant\n ? SOURCE_VARIANTS[path.variant].flowStroke\n : DEVICE_FLOW_STROKE\n const initialDashGap =\n path.kind === 'source' && path.variant\n ? SOURCE_VARIANTS[path.variant].initialDashGap\n : DEVICE_INITIAL_DASH_GAP\n\n return (\n <g key={path.id}>\n <path\n d={path.d}\n fill=\"none\"\n stroke={IDLE_STROKE}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n ref={(el) => {\n idlePathRefs.current[path.id] = el\n }}\n opacity={1}\n style={\n reducedMotion\n ? { transition: 'opacity 300ms ease' }\n : undefined\n }\n />\n <path\n d={path.d}\n fill=\"none\"\n stroke={flowStroke}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n ref={(el) => {\n flowPathRefs.current[path.id] = el\n }}\n strokeDasharray={`${FLOW_DASH_SOLID} ${initialDashGap}`}\n strokeDashoffset={0}\n opacity={0}\n style={\n reducedMotion\n ? { transition: 'opacity 300ms ease' }\n : undefined\n }\n />\n </g>\n )\n })}\n </svg>\n )}\n <div className=\"flex flex-col gap-y-14\">{children}</div>\n </div>\n </PowerNetworkContext.Provider>\n )\n}\n\n// --- Row containers -----------------------------------------------------\n\nexport interface PowerNetworkRowProps {\n className?: string\n children?: ReactNode\n}\n\nexport function PowerNetworkSources({\n className,\n children,\n}: PowerNetworkRowProps): ReactElement {\n return (\n <div\n className={cn(\n 'flex items-center justify-center gap-[clamp(1.5rem,10cqw,4rem)]',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\nexport function PowerNetworkDevices({\n className,\n children,\n}: PowerNetworkRowProps): ReactElement {\n return (\n <div\n className={cn(\n 'flex items-center justify-center gap-[clamp(1.5rem,10cqw,4rem)]',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\n// --- PowerSource --------------------------------------------------------\n\nexport interface PowerSourceProps {\n id?: string\n watts: number\n icon: ReactNode\n variant?: PowerSourceVariant\n className?: string\n /**\n * Optional 0–100 progress (e.g. battery SoC). When set, a thin colored arc\n * is drawn around the icon circle as a non-intrusive indicator.\n */\n progress?: number\n /** Keeps the variant colors at 0 W, for a source that is online but idle. */\n standby?: boolean\n}\n\nexport function PowerSource({\n id: idProp,\n watts,\n icon,\n variant = 'neutral',\n className,\n progress,\n standby = false,\n}: PowerSourceProps): ReactElement {\n const autoId = useId()\n const id = idProp ?? autoId\n const { register, unregister, loading } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (!element) return\n register({ id, kind: 'source', variant, watts, element })\n }, [element, register, id, variant, watts])\n\n useEffect(() => {\n return () => unregister(id)\n }, [unregister, id])\n\n if (loading) {\n return <SourceSkeleton anchorRef={setElement} />\n }\n\n const isActive = watts !== 0\n const isColored = isActive || standby\n const variantStyles = SOURCE_VARIANTS[variant]\n const clampedProgress =\n progress == null ? null : Math.max(0, Math.min(100, progress))\n const showRing = clampedProgress != null\n const RING_RADIUS = 48\n const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS\n const ringDash =\n clampedProgress != null ? (clampedProgress / 100) * RING_CIRCUMFERENCE : 0\n\n return (\n <div\n className={cn(\n 'flex flex-col items-center justify-center gap-1 transition-opacity duration-500',\n !isColored && 'opacity-45',\n className,\n )}\n >\n <NumberFlow\n value={Math.abs(watts)}\n suffix=\"W\"\n className=\"mt-1 text-sm leading-none font-medium\"\n />\n <div\n ref={setElement}\n className={cn(\n 'relative flex aspect-square items-center justify-center rounded-full border p-4 shadow-sm',\n isColored\n ? variantStyles.activeClassName\n : 'border-gray bg-gray-a3 shadow-gray-a4 saturate-0',\n )}\n >\n {showRing && (\n <svg\n className=\"pointer-events-none absolute inset-0 size-full -rotate-90\"\n viewBox=\"0 0 100 100\"\n aria-hidden=\"true\"\n >\n <circle\n cx=\"50\"\n cy=\"50\"\n r={RING_RADIUS}\n fill=\"none\"\n stroke={variantStyles.flowStroke}\n strokeWidth=\"4\"\n strokeLinecap=\"round\"\n strokeDasharray={`${ringDash} ${RING_CIRCUMFERENCE}`}\n opacity={isColored ? 1 : 0.5}\n />\n </svg>\n )}\n {icon}\n </div>\n </div>\n )\n}\n\nfunction SourceSkeleton({\n anchorRef,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n}): ReactElement {\n return (\n <div className=\"flex flex-col items-center justify-center gap-1\">\n <Skeleton className=\"h-3.5 w-10\" />\n <div\n ref={anchorRef}\n className=\"flex aspect-square animate-pulse items-center justify-center rounded-full border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none\"\n >\n <div className=\"size-7\" />\n </div>\n </div>\n )\n}\n\n// --- PowerDevice --------------------------------------------------------\n\nexport interface PowerDeviceProps {\n id?: string\n watts: number\n icon: ReactNode\n className?: string\n}\n\nexport function PowerDevice({\n id: idProp,\n watts,\n icon,\n className,\n}: PowerDeviceProps): ReactElement {\n const autoId = useId()\n const id = idProp ?? autoId\n const { register, unregister, loading } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (!element) return\n register({ id, kind: 'device', watts, element })\n }, [element, register, id, watts])\n\n useEffect(() => {\n return () => unregister(id)\n }, [unregister, id])\n\n if (loading) {\n return <DeviceSkeleton anchorRef={setElement} />\n }\n\n return (\n <div\n className={cn(\n 'flex flex-col items-center justify-center gap-1',\n className,\n )}\n >\n <div\n ref={setElement}\n className=\"flex aspect-square items-center justify-center rounded-full border border-gray bg-gray-a1 p-4\"\n >\n {icon}\n </div>\n <NumberFlow\n value={watts}\n suffix=\"W\"\n className=\"text-sm font-medium tabular-nums\"\n />\n </div>\n )\n}\n\nfunction DeviceSkeleton({\n anchorRef,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n}): ReactElement {\n return (\n <div className=\"flex flex-col items-center justify-center gap-1\">\n <div\n ref={anchorRef}\n className=\"flex aspect-square animate-pulse items-center justify-center rounded-full border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none\"\n >\n <div className=\"size-5\" />\n </div>\n <Skeleton className=\"h-3.5 w-10\" />\n </div>\n )\n}\n\n// --- PowerNetworkHome ---------------------------------------------------\n\nexport interface PowerNetworkHomeProps {\n /** Override total power. Defaults to the sum of registered source watts. */\n watts?: number\n /** Self-sufficiency ratio in 0..1. Renders a green ring when provided. */\n autarky?: number\n className?: string\n}\n\nexport function PowerNetworkHome({\n watts,\n autarky,\n className,\n}: PowerNetworkHomeProps): ReactElement {\n const autoId = useId()\n const { register, unregister, loading, totalSourceWatts } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n const effectiveWatts = watts ?? totalSourceWatts\n\n useEffect(() => {\n if (!element) return\n register({ id: autoId, kind: 'home', watts: effectiveWatts, element })\n }, [element, register, autoId, effectiveWatts])\n\n useEffect(() => {\n return () => unregister(autoId)\n }, [unregister, autoId])\n\n if (loading) {\n return <HomeSkeleton anchorRef={setElement} className={className} />\n }\n\n return (\n <HomeSection\n anchorRef={setElement}\n autarky={autarky ?? null}\n watts={effectiveWatts}\n className={className}\n />\n )\n}\n\nfunction HomeSection({\n anchorRef,\n autarky,\n className,\n watts,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n autarky: number | null\n className?: string\n watts: number\n}): ReactElement {\n const reducedMotion = useReducedMotion()\n const targetAutarky =\n autarky == null ? 0 : Math.max(0, Math.min(100, autarky * 100))\n const [animatedAutarky, setAnimatedAutarky] = useState(targetAutarky)\n const animatedAutarkyRef = useRef(animatedAutarky)\n const nodeSize = 78\n const ringStrokeWidth = 2\n const radius = (nodeSize - ringStrokeWidth) / 2\n const circumference = 2 * Math.PI * radius\n const progressLength = (animatedAutarky / 100) * circumference\n\n useEffect(() => {\n animatedAutarkyRef.current = animatedAutarky\n }, [animatedAutarky])\n\n useEffect(() => {\n if (reducedMotion) {\n animatedAutarkyRef.current = targetAutarky\n setAnimatedAutarky(targetAutarky)\n return\n }\n\n let frameId = 0\n let lastTime = performance.now()\n\n const animate = (now: number) => {\n const deltaSeconds = Math.max(0.001, (now - lastTime) / 1000)\n lastTime = now\n const smoothing = 1 - Math.exp(-7 * deltaSeconds)\n const next =\n animatedAutarkyRef.current +\n (targetAutarky - animatedAutarkyRef.current) * smoothing\n\n animatedAutarkyRef.current = next\n setAnimatedAutarky(next)\n\n if (Math.abs(targetAutarky - next) > 0.05) {\n frameId = window.requestAnimationFrame(animate)\n } else {\n animatedAutarkyRef.current = targetAutarky\n setAnimatedAutarky(targetAutarky)\n }\n }\n\n frameId = window.requestAnimationFrame(animate)\n return () => window.cancelAnimationFrame(frameId)\n }, [targetAutarky, reducedMotion])\n\n return (\n <div\n ref={anchorRef}\n className={cn('relative mx-auto size-[78px]', className)}\n >\n {autarky != null && (\n <svg\n className=\"absolute inset-0 size-full -rotate-90\"\n viewBox={`0 0 ${nodeSize} ${nodeSize}`}\n >\n <circle\n cx={nodeSize / 2}\n cy={nodeSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"var(--neutral-a5)\"\n strokeWidth={ringStrokeWidth}\n />\n {progressLength > 0 && (\n <circle\n cx={nodeSize / 2}\n cy={nodeSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"var(--green-a9)\"\n strokeLinecap=\"round\"\n strokeWidth={ringStrokeWidth}\n strokeDasharray={`${progressLength} ${circumference}`}\n />\n )}\n </svg>\n )}\n <div className=\"absolute inset-0 flex flex-col items-center justify-center rounded-full border border-gray-a5 shadow-sm shadow-gray-a1\">\n <MaterialSymbol className=\"text-[14px]\" name=\"home\" />\n <NumberFlow\n value={watts}\n suffix=\"W\"\n className=\"mt-1 text-sm leading-none font-medium\"\n />\n </div>\n </div>\n )\n}\n\nfunction HomeSkeleton({\n anchorRef,\n className,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n className?: string\n}): ReactElement {\n return (\n <div\n ref={anchorRef}\n className={cn(\n 'mx-auto size-[78px] animate-pulse rounded-full border border-gray motion-reduce:animate-none',\n className,\n )}\n />\n )\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { useReducedMotion } from "../hooks/use-reduced-motion.js";
3
2
  import { cn } from "../utils/cn.js";
4
3
  import { MaterialSymbol } from "../icons/material-symbol.js";
4
+ import { useReducedMotion } from "../hooks/use-reduced-motion.js";
5
5
  import { SectionTitle } from "./typography.js";
6
6
  import NumberFlow from "./number-flow.js";
7
7
  import { Skeleton } from "./skeleton.js";
@@ -362,7 +362,7 @@ function PowerSource({ id: idProp, watts, icon, variant = "neutral", className,
362
362
  className: "mt-1 text-sm leading-none font-medium"
363
363
  }), /* @__PURE__ */ jsxs("div", {
364
364
  ref: setElement,
365
- className: cn("relative flex aspect-square items-center justify-center rounded-circle border p-4 shadow-sm", isColored ? variantStyles.activeClassName : "border-gray bg-gray-a3 shadow-gray-a4 saturate-0"),
365
+ className: cn("relative flex aspect-square items-center justify-center rounded-full border p-4 shadow-sm", isColored ? variantStyles.activeClassName : "border-gray bg-gray-a3 shadow-gray-a4 saturate-0"),
366
366
  children: [showRing && /* @__PURE__ */ jsx("svg", {
367
367
  className: "pointer-events-none absolute inset-0 size-full -rotate-90",
368
368
  viewBox: "0 0 100 100",
@@ -387,7 +387,7 @@ function SourceSkeleton({ anchorRef }) {
387
387
  className: "flex flex-col items-center justify-center gap-1",
388
388
  children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-10" }), /* @__PURE__ */ jsx("div", {
389
389
  ref: anchorRef,
390
- className: "flex aspect-square animate-pulse items-center justify-center rounded-circle border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none",
390
+ className: "flex aspect-square animate-pulse items-center justify-center rounded-full border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none",
391
391
  children: /* @__PURE__ */ jsx("div", { className: "size-7" })
392
392
  })]
393
393
  });
@@ -419,7 +419,7 @@ function PowerDevice({ id: idProp, watts, icon, className }) {
419
419
  className: cn("flex flex-col items-center justify-center gap-1", className),
420
420
  children: [/* @__PURE__ */ jsx("div", {
421
421
  ref: setElement,
422
- className: "flex aspect-square items-center justify-center rounded-circle border border-gray bg-gray-a1 p-4",
422
+ className: "flex aspect-square items-center justify-center rounded-full border border-gray bg-gray-a1 p-4",
423
423
  children: icon
424
424
  }), /* @__PURE__ */ jsx(NumberFlow, {
425
425
  value: watts,
@@ -433,7 +433,7 @@ function DeviceSkeleton({ anchorRef }) {
433
433
  className: "flex flex-col items-center justify-center gap-1",
434
434
  children: [/* @__PURE__ */ jsx("div", {
435
435
  ref: anchorRef,
436
- className: "flex aspect-square animate-pulse items-center justify-center rounded-circle border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none",
436
+ className: "flex aspect-square animate-pulse items-center justify-center rounded-full border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none",
437
437
  children: /* @__PURE__ */ jsx("div", { className: "size-5" })
438
438
  }), /* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-10" })]
439
439
  });
@@ -532,7 +532,7 @@ function HomeSection({ anchorRef, autarky, className, watts }) {
532
532
  strokeDasharray: `${progressLength} ${circumference}`
533
533
  })]
534
534
  }), /* @__PURE__ */ jsxs("div", {
535
- className: "absolute inset-0 flex flex-col items-center justify-center rounded-circle border border-gray-a5 shadow-sm shadow-gray-a1",
535
+ className: "absolute inset-0 flex flex-col items-center justify-center rounded-full border border-gray-a5 shadow-sm shadow-gray-a1",
536
536
  children: [/* @__PURE__ */ jsx(MaterialSymbol, {
537
537
  className: "text-[14px]",
538
538
  name: "home"
@@ -547,7 +547,7 @@ function HomeSection({ anchorRef, autarky, className, watts }) {
547
547
  function HomeSkeleton({ anchorRef, className }) {
548
548
  return /* @__PURE__ */ jsx("div", {
549
549
  ref: anchorRef,
550
- className: cn("mx-auto size-[78px] animate-pulse rounded-circle border border-gray motion-reduce:animate-none", className)
550
+ className: cn("mx-auto size-[78px] animate-pulse rounded-full border border-gray motion-reduce:animate-none", className)
551
551
  });
552
552
  }
553
553
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"power-network.js","names":[],"sources":["../../src/components/power-network.tsx"],"sourcesContent":["import {\n createContext,\n type ReactElement,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useReducedMotion } from '../hooks/use-reduced-motion'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\nimport NumberFlow from './number-flow'\nimport { Skeleton } from './skeleton'\nimport { SectionTitle } from './typography'\n\n// --- Variants -----------------------------------------------------------\n\nexport type PowerSourceVariant = 'grid' | 'solar' | 'battery' | 'neutral'\n\ninterface SourceVariantStyles {\n activeClassName: string\n flowStroke: string\n initialDashGap: number\n}\n\nconst SOURCE_VARIANTS: Record<PowerSourceVariant, SourceVariantStyles> = {\n grid: {\n activeClassName: 'border-blue-a9 bg-blue-a1 shadow-blue-a4',\n flowStroke: 'var(--blue-11)',\n initialDashGap: 10,\n },\n solar: {\n activeClassName: 'border-orange-a9 bg-orange-a1 shadow-orange-a3',\n flowStroke: 'var(--orange-a9)',\n initialDashGap: 5,\n },\n battery: {\n activeClassName: 'border-green-a9 bg-green-a1 shadow-green-a3',\n flowStroke: 'var(--green-a9)',\n initialDashGap: 7,\n },\n neutral: {\n activeClassName: 'border-gray-a9 bg-gray-a1 shadow-gray-a3',\n flowStroke: 'var(--neutral-a9)',\n initialDashGap: 9,\n },\n}\n\nconst DEVICE_FLOW_STROKE = 'var(--neutral-a9)'\nconst DEVICE_INITIAL_DASH_GAP = 9\n\n// Drawn under every connector and revealed when nothing flows, so a 0 W node\n// still reads as wired up and online rather than disconnected.\nconst IDLE_STROKE = 'var(--neutral-a6)'\nconst SKELETON_STROKE = 'var(--neutral-a6)'\nconst STROKE_WIDTH = 2\nconst STRAIGHT_TOLERANCE_PX = 3\nconst SOURCE_HOME_OFFSET_STEP_PX = 10\nconst DEVICE_HOME_OFFSET_STEP_PX = 10\n\n// --- Flow animation parameters ------------------------------------------\n\nconst FLOW_DASH_SOLID = 1\nconst FLOW_GAP_MAX = 14\nconst FLOW_GAP_MIN = 5\nconst FLOW_SPEED_MIN = 10\nconst FLOW_SPEED_MAX = 56\nconst FLOW_SMOOTHING = 5.5\n\ninterface FlowVisual {\n gap: number\n speed: number\n opacity: number\n}\n\ninterface FlowAnimationState extends FlowVisual {\n offset: number\n}\n\n// --- Context ------------------------------------------------------------\n\ntype NodeKind = 'source' | 'device' | 'home'\n\ninterface RegisteredNode {\n id: string\n kind: NodeKind\n variant?: PowerSourceVariant\n watts: number\n element: HTMLDivElement | null\n}\n\ninterface PowerNetworkContextValue {\n loading: boolean\n register: (node: RegisteredNode) => void\n unregister: (id: string) => void\n totalSourceWatts: number\n}\n\nconst PowerNetworkContext = createContext<PowerNetworkContextValue | null>(null)\n\nfunction usePowerNetwork(): PowerNetworkContextValue {\n const ctx = useContext(PowerNetworkContext)\n if (!ctx) {\n throw new Error(\n 'Power network sub-components must be used inside <PowerNetworkDiagram>.',\n )\n }\n return ctx\n}\n\n// --- Geometry helpers ---------------------------------------------------\n\ninterface Point {\n x: number\n y: number\n}\n\nfunction anchorTop(rect: DOMRect, containerRect: DOMRect): Point {\n return {\n x: rect.left + rect.width / 2 - containerRect.left,\n y: rect.top - containerRect.top,\n }\n}\n\nfunction anchorBottom(rect: DOMRect, containerRect: DOMRect): Point {\n return {\n x: rect.left + rect.width / 2 - containerRect.left,\n y: rect.bottom - containerRect.top,\n }\n}\n\nfunction buildAdaptivePath(from: Point, to: Point): string {\n if (Math.abs(from.x - to.x) < STRAIGHT_TOLERANCE_PX) {\n return `M ${from.x} ${from.y} L ${to.x} ${to.y}`\n }\n const dy = to.y - from.y\n const c1y = from.y + dy * 0.56\n const c2y = to.y - dy * 0.58\n return `M ${from.x} ${from.y} C ${from.x} ${c1y} ${to.x} ${c2y} ${to.x} ${to.y}`\n}\n\n// --- PowerNetworkDiagram ------------------------------------------------\n\nexport interface PowerNetworkDiagramProps {\n title?: string\n loading?: boolean\n className?: string\n children?: ReactNode\n}\n\ninterface ConnectorPath {\n id: string\n d: string\n kind: NodeKind\n variant?: PowerSourceVariant\n reversed: boolean\n}\n\nfunction connectorPathsEqual(a: ConnectorPath[], b: ConnectorPath[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n const ap = a[i]\n const bp = b[i]\n if (!ap || !bp) return false\n if (\n ap.id !== bp.id ||\n ap.d !== bp.d ||\n ap.variant !== bp.variant ||\n ap.kind !== bp.kind ||\n ap.reversed !== bp.reversed\n ) {\n return false\n }\n }\n return true\n}\n\nexport function PowerNetworkDiagram({\n title,\n loading = false,\n className,\n children,\n}: PowerNetworkDiagramProps): ReactElement {\n const containerRef = useRef<HTMLDivElement>(null)\n const reducedMotion = useReducedMotion()\n const [nodes, setNodes] = useState<Map<string, RegisteredNode>>(\n () => new Map(),\n )\n const [connectorPaths, setConnectorPaths] = useState<ConnectorPath[]>([])\n const connectorPathsRef = useRef<ConnectorPath[]>([])\n const flowPathRefs = useRef<Record<string, SVGPathElement | null>>({})\n const idlePathRefs = useRef<Record<string, SVGPathElement | null>>({})\n const currentFlowVisualsRef = useRef<Record<string, FlowAnimationState>>({})\n const targetFlowVisualsRef = useRef<Record<string, FlowVisual>>({})\n\n const register = useCallback((node: RegisteredNode) => {\n setNodes((prev) => {\n const existing = prev.get(node.id)\n if (\n existing &&\n existing.kind === node.kind &&\n existing.variant === node.variant &&\n existing.watts === node.watts &&\n existing.element === node.element\n ) {\n return prev\n }\n const next = new Map(prev)\n next.set(node.id, node)\n return next\n })\n }, [])\n\n const unregister = useCallback((id: string) => {\n setNodes((prev) => {\n if (!prev.has(id)) return prev\n const next = new Map(prev)\n next.delete(id)\n return next\n })\n }, [])\n\n const totalSourceWatts = useMemo(() => {\n let total = 0\n for (const node of nodes.values()) {\n if (node.kind === 'source') total += node.watts\n }\n return total\n }, [nodes])\n\n useEffect(() => {\n const container = containerRef.current\n if (!container) return\n\n let homeNode: RegisteredNode | undefined\n const sources: RegisteredNode[] = []\n const devices: RegisteredNode[] = []\n for (const node of nodes.values()) {\n if (node.kind === 'home') {\n homeNode = node\n } else if (node.element) {\n if (node.kind === 'source') sources.push(node)\n else devices.push(node)\n }\n }\n\n const updatePaths = () => {\n if (!homeNode || !homeNode.element) {\n setConnectorPaths((prev) => (prev.length === 0 ? prev : []))\n return\n }\n const containerRect = container.getBoundingClientRect()\n const homeRect = homeNode.element.getBoundingClientRect()\n const homeTop = anchorTop(homeRect, containerRect)\n const homeBottom = anchorBottom(homeRect, containerRect)\n const next: ConnectorPath[] = []\n\n const sourceCount = sources.length\n const deviceCount = devices.length\n\n sources.forEach((source, index) => {\n if (!source.element) return\n const from = anchorBottom(\n source.element.getBoundingClientRect(),\n containerRect,\n )\n const offsetX =\n SOURCE_HOME_OFFSET_STEP_PX * (index - (sourceCount - 1) / 2)\n const to = { x: homeTop.x + offsetX, y: homeTop.y }\n next.push({\n id: source.id,\n d: buildAdaptivePath(from, to),\n kind: 'source',\n variant: source.variant,\n reversed: source.watts < 0,\n })\n })\n\n devices.forEach((device, index) => {\n if (!device.element) return\n const to = anchorTop(\n device.element.getBoundingClientRect(),\n containerRect,\n )\n const offsetX =\n DEVICE_HOME_OFFSET_STEP_PX * (index - (deviceCount - 1) / 2)\n const from = { x: homeBottom.x + offsetX, y: homeBottom.y }\n next.push({\n id: device.id,\n d: buildAdaptivePath(from, to),\n kind: 'device',\n reversed: false,\n })\n })\n\n setConnectorPaths((prev) =>\n connectorPathsEqual(prev, next) ? prev : next,\n )\n }\n\n updatePaths()\n\n const resizeObserver = new ResizeObserver(updatePaths)\n resizeObserver.observe(container)\n for (const node of nodes.values()) {\n if (node.element) resizeObserver.observe(node.element)\n }\n window.addEventListener('resize', updatePaths)\n\n return () => {\n resizeObserver.disconnect()\n window.removeEventListener('resize', updatePaths)\n }\n }, [nodes])\n\n useEffect(() => {\n connectorPathsRef.current = connectorPaths\n }, [connectorPaths])\n\n useEffect(() => {\n const targets: Record<string, FlowVisual> = {}\n const homeWatts = totalSourceWatts\n for (const node of nodes.values()) {\n if (node.kind === 'home') continue\n const watts = node.watts\n const relative =\n homeWatts > 0\n ? Math.max(0, Math.min(1, Math.abs(watts) / homeWatts))\n : 0\n targets[node.id] = {\n gap: FLOW_GAP_MAX - (FLOW_GAP_MAX - FLOW_GAP_MIN) * relative,\n speed: FLOW_SPEED_MIN + (FLOW_SPEED_MAX - FLOW_SPEED_MIN) * relative,\n opacity: Math.abs(watts) > 0 ? 1 : 0,\n }\n }\n targetFlowVisualsRef.current = targets\n }, [nodes, totalSourceWatts])\n\n useEffect(() => {\n if (loading || reducedMotion) return\n let frameId = 0\n let lastFrameTime = performance.now()\n\n const animate = (now: number) => {\n const deltaSeconds = Math.max(0.001, (now - lastFrameTime) / 1000)\n lastFrameTime = now\n const smoothing = 1 - Math.exp(-FLOW_SMOOTHING * deltaSeconds)\n\n for (const connectorPath of connectorPathsRef.current) {\n const id = connectorPath.id\n const target = targetFlowVisualsRef.current[id]\n const pathElement = flowPathRefs.current[id]\n if (!target || !pathElement) continue\n\n const current = currentFlowVisualsRef.current[id] ?? {\n gap: target.gap,\n offset: 0,\n speed: target.speed,\n opacity: target.opacity,\n }\n\n current.gap += (target.gap - current.gap) * smoothing\n current.speed += (target.speed - current.speed) * smoothing\n current.opacity += (target.opacity - current.opacity) * smoothing\n\n const dashCycle = FLOW_DASH_SOLID + Math.max(0.001, current.gap)\n const direction = connectorPath.reversed ? 1 : -1\n const nextOffset =\n current.offset + direction * current.speed * deltaSeconds\n const wrappedOffset = ((nextOffset % dashCycle) + dashCycle) % dashCycle\n current.offset = wrappedOffset - dashCycle\n\n pathElement.setAttribute(\n 'stroke-dasharray',\n `${FLOW_DASH_SOLID} ${current.gap.toFixed(3)}`,\n )\n pathElement.setAttribute('stroke-dashoffset', current.offset.toFixed(3))\n pathElement.setAttribute('opacity', current.opacity.toFixed(3))\n idlePathRefs.current[id]?.setAttribute(\n 'opacity',\n (1 - current.opacity).toFixed(3),\n )\n\n currentFlowVisualsRef.current[id] = current\n }\n\n frameId = window.requestAnimationFrame(animate)\n }\n\n frameId = window.requestAnimationFrame(animate)\n return () => window.cancelAnimationFrame(frameId)\n }, [loading, reducedMotion])\n\n // Reduced motion: freeze the marching dashes but keep the dash gap tracking\n // live magnitude, and let opacity fade in/out via the CSS transition on each\n // flow path (see the flow <path> style below).\n useEffect(() => {\n if (loading || !reducedMotion) return\n for (const connectorPath of connectorPathsRef.current) {\n const target = targetFlowVisualsRef.current[connectorPath.id]\n const pathElement = flowPathRefs.current[connectorPath.id]\n if (!target || !pathElement) continue\n pathElement.setAttribute(\n 'stroke-dasharray',\n `${FLOW_DASH_SOLID} ${target.gap.toFixed(3)}`,\n )\n pathElement.setAttribute('stroke-dashoffset', '0')\n pathElement.setAttribute('opacity', target.opacity.toFixed(3))\n idlePathRefs.current[connectorPath.id]?.setAttribute(\n 'opacity',\n (1 - target.opacity).toFixed(3),\n )\n }\n }, [loading, reducedMotion, nodes, totalSourceWatts])\n\n const contextValue = useMemo<PowerNetworkContextValue>(\n () => ({ loading, register, unregister, totalSourceWatts }),\n [loading, register, unregister, totalSourceWatts],\n )\n\n return (\n <PowerNetworkContext.Provider value={contextValue}>\n <div\n ref={containerRef}\n className={cn('@container relative flex flex-col', className)}\n >\n {title && <SectionTitle>{title}</SectionTitle>}\n {connectorPaths.length > 0 && (\n <svg className=\"pointer-events-none absolute inset-0 size-full\">\n {connectorPaths.map((path) => {\n if (loading) {\n return (\n <path\n key={path.id}\n d={path.d}\n fill=\"none\"\n stroke={SKELETON_STROKE}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n />\n )\n }\n\n const flowStroke =\n path.kind === 'source' && path.variant\n ? SOURCE_VARIANTS[path.variant].flowStroke\n : DEVICE_FLOW_STROKE\n const initialDashGap =\n path.kind === 'source' && path.variant\n ? SOURCE_VARIANTS[path.variant].initialDashGap\n : DEVICE_INITIAL_DASH_GAP\n\n return (\n <g key={path.id}>\n <path\n d={path.d}\n fill=\"none\"\n stroke={IDLE_STROKE}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n ref={(el) => {\n idlePathRefs.current[path.id] = el\n }}\n opacity={1}\n style={\n reducedMotion\n ? { transition: 'opacity 300ms ease' }\n : undefined\n }\n />\n <path\n d={path.d}\n fill=\"none\"\n stroke={flowStroke}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n ref={(el) => {\n flowPathRefs.current[path.id] = el\n }}\n strokeDasharray={`${FLOW_DASH_SOLID} ${initialDashGap}`}\n strokeDashoffset={0}\n opacity={0}\n style={\n reducedMotion\n ? { transition: 'opacity 300ms ease' }\n : undefined\n }\n />\n </g>\n )\n })}\n </svg>\n )}\n <div className=\"flex flex-col gap-y-14\">{children}</div>\n </div>\n </PowerNetworkContext.Provider>\n )\n}\n\n// --- Row containers -----------------------------------------------------\n\nexport interface PowerNetworkRowProps {\n className?: string\n children?: ReactNode\n}\n\nexport function PowerNetworkSources({\n className,\n children,\n}: PowerNetworkRowProps): ReactElement {\n return (\n <div\n className={cn(\n 'flex items-center justify-center gap-[clamp(1.5rem,10cqw,4rem)]',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\nexport function PowerNetworkDevices({\n className,\n children,\n}: PowerNetworkRowProps): ReactElement {\n return (\n <div\n className={cn(\n 'flex items-center justify-center gap-[clamp(1.5rem,10cqw,4rem)]',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\n// --- PowerSource --------------------------------------------------------\n\nexport interface PowerSourceProps {\n id?: string\n watts: number\n icon: ReactNode\n variant?: PowerSourceVariant\n className?: string\n /**\n * Optional 0–100 progress (e.g. battery SoC). When set, a thin colored arc\n * is drawn around the icon circle as a non-intrusive indicator.\n */\n progress?: number\n /** Keeps the variant colors at 0 W, for a source that is online but idle. */\n standby?: boolean\n}\n\nexport function PowerSource({\n id: idProp,\n watts,\n icon,\n variant = 'neutral',\n className,\n progress,\n standby = false,\n}: PowerSourceProps): ReactElement {\n const autoId = useId()\n const id = idProp ?? autoId\n const { register, unregister, loading } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (!element) return\n register({ id, kind: 'source', variant, watts, element })\n }, [element, register, id, variant, watts])\n\n useEffect(() => {\n return () => unregister(id)\n }, [unregister, id])\n\n if (loading) {\n return <SourceSkeleton anchorRef={setElement} />\n }\n\n const isActive = watts !== 0\n const isColored = isActive || standby\n const variantStyles = SOURCE_VARIANTS[variant]\n const clampedProgress =\n progress == null ? null : Math.max(0, Math.min(100, progress))\n const showRing = clampedProgress != null\n const RING_RADIUS = 48\n const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS\n const ringDash =\n clampedProgress != null ? (clampedProgress / 100) * RING_CIRCUMFERENCE : 0\n\n return (\n <div\n className={cn(\n 'flex flex-col items-center justify-center gap-1 transition-opacity duration-500',\n !isColored && 'opacity-45',\n className,\n )}\n >\n <NumberFlow\n value={Math.abs(watts)}\n suffix=\"W\"\n className=\"mt-1 text-sm leading-none font-medium\"\n />\n <div\n ref={setElement}\n className={cn(\n 'relative flex aspect-square items-center justify-center rounded-circle border p-4 shadow-sm',\n isColored\n ? variantStyles.activeClassName\n : 'border-gray bg-gray-a3 shadow-gray-a4 saturate-0',\n )}\n >\n {showRing && (\n <svg\n className=\"pointer-events-none absolute inset-0 size-full -rotate-90\"\n viewBox=\"0 0 100 100\"\n aria-hidden=\"true\"\n >\n <circle\n cx=\"50\"\n cy=\"50\"\n r={RING_RADIUS}\n fill=\"none\"\n stroke={variantStyles.flowStroke}\n strokeWidth=\"4\"\n strokeLinecap=\"round\"\n strokeDasharray={`${ringDash} ${RING_CIRCUMFERENCE}`}\n opacity={isColored ? 1 : 0.5}\n />\n </svg>\n )}\n {icon}\n </div>\n </div>\n )\n}\n\nfunction SourceSkeleton({\n anchorRef,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n}): ReactElement {\n return (\n <div className=\"flex flex-col items-center justify-center gap-1\">\n <Skeleton className=\"h-3.5 w-10\" />\n <div\n ref={anchorRef}\n className=\"flex aspect-square animate-pulse items-center justify-center rounded-circle border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none\"\n >\n <div className=\"size-7\" />\n </div>\n </div>\n )\n}\n\n// --- PowerDevice --------------------------------------------------------\n\nexport interface PowerDeviceProps {\n id?: string\n watts: number\n icon: ReactNode\n className?: string\n}\n\nexport function PowerDevice({\n id: idProp,\n watts,\n icon,\n className,\n}: PowerDeviceProps): ReactElement {\n const autoId = useId()\n const id = idProp ?? autoId\n const { register, unregister, loading } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (!element) return\n register({ id, kind: 'device', watts, element })\n }, [element, register, id, watts])\n\n useEffect(() => {\n return () => unregister(id)\n }, [unregister, id])\n\n if (loading) {\n return <DeviceSkeleton anchorRef={setElement} />\n }\n\n return (\n <div\n className={cn(\n 'flex flex-col items-center justify-center gap-1',\n className,\n )}\n >\n <div\n ref={setElement}\n className=\"flex aspect-square items-center justify-center rounded-circle border border-gray bg-gray-a1 p-4\"\n >\n {icon}\n </div>\n <NumberFlow\n value={watts}\n suffix=\"W\"\n className=\"text-sm font-medium tabular-nums\"\n />\n </div>\n )\n}\n\nfunction DeviceSkeleton({\n anchorRef,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n}): ReactElement {\n return (\n <div className=\"flex flex-col items-center justify-center gap-1\">\n <div\n ref={anchorRef}\n className=\"flex aspect-square animate-pulse items-center justify-center rounded-circle border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none\"\n >\n <div className=\"size-5\" />\n </div>\n <Skeleton className=\"h-3.5 w-10\" />\n </div>\n )\n}\n\n// --- PowerNetworkHome ---------------------------------------------------\n\nexport interface PowerNetworkHomeProps {\n /** Override total power. Defaults to the sum of registered source watts. */\n watts?: number\n /** Self-sufficiency ratio in 0..1. Renders a green ring when provided. */\n autarky?: number\n className?: string\n}\n\nexport function PowerNetworkHome({\n watts,\n autarky,\n className,\n}: PowerNetworkHomeProps): ReactElement {\n const autoId = useId()\n const { register, unregister, loading, totalSourceWatts } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n const effectiveWatts = watts ?? totalSourceWatts\n\n useEffect(() => {\n if (!element) return\n register({ id: autoId, kind: 'home', watts: effectiveWatts, element })\n }, [element, register, autoId, effectiveWatts])\n\n useEffect(() => {\n return () => unregister(autoId)\n }, [unregister, autoId])\n\n if (loading) {\n return <HomeSkeleton anchorRef={setElement} className={className} />\n }\n\n return (\n <HomeSection\n anchorRef={setElement}\n autarky={autarky ?? null}\n watts={effectiveWatts}\n className={className}\n />\n )\n}\n\nfunction HomeSection({\n anchorRef,\n autarky,\n className,\n watts,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n autarky: number | null\n className?: string\n watts: number\n}): ReactElement {\n const reducedMotion = useReducedMotion()\n const targetAutarky =\n autarky == null ? 0 : Math.max(0, Math.min(100, autarky * 100))\n const [animatedAutarky, setAnimatedAutarky] = useState(targetAutarky)\n const animatedAutarkyRef = useRef(animatedAutarky)\n const nodeSize = 78\n const ringStrokeWidth = 2\n const radius = (nodeSize - ringStrokeWidth) / 2\n const circumference = 2 * Math.PI * radius\n const progressLength = (animatedAutarky / 100) * circumference\n\n useEffect(() => {\n animatedAutarkyRef.current = animatedAutarky\n }, [animatedAutarky])\n\n useEffect(() => {\n if (reducedMotion) {\n animatedAutarkyRef.current = targetAutarky\n setAnimatedAutarky(targetAutarky)\n return\n }\n\n let frameId = 0\n let lastTime = performance.now()\n\n const animate = (now: number) => {\n const deltaSeconds = Math.max(0.001, (now - lastTime) / 1000)\n lastTime = now\n const smoothing = 1 - Math.exp(-7 * deltaSeconds)\n const next =\n animatedAutarkyRef.current +\n (targetAutarky - animatedAutarkyRef.current) * smoothing\n\n animatedAutarkyRef.current = next\n setAnimatedAutarky(next)\n\n if (Math.abs(targetAutarky - next) > 0.05) {\n frameId = window.requestAnimationFrame(animate)\n } else {\n animatedAutarkyRef.current = targetAutarky\n setAnimatedAutarky(targetAutarky)\n }\n }\n\n frameId = window.requestAnimationFrame(animate)\n return () => window.cancelAnimationFrame(frameId)\n }, [targetAutarky, reducedMotion])\n\n return (\n <div\n ref={anchorRef}\n className={cn('relative mx-auto size-[78px]', className)}\n >\n {autarky != null && (\n <svg\n className=\"absolute inset-0 size-full -rotate-90\"\n viewBox={`0 0 ${nodeSize} ${nodeSize}`}\n >\n <circle\n cx={nodeSize / 2}\n cy={nodeSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"var(--neutral-a5)\"\n strokeWidth={ringStrokeWidth}\n />\n {progressLength > 0 && (\n <circle\n cx={nodeSize / 2}\n cy={nodeSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"var(--green-a9)\"\n strokeLinecap=\"round\"\n strokeWidth={ringStrokeWidth}\n strokeDasharray={`${progressLength} ${circumference}`}\n />\n )}\n </svg>\n )}\n <div className=\"absolute inset-0 flex flex-col items-center justify-center rounded-circle border border-gray-a5 shadow-sm shadow-gray-a1\">\n <MaterialSymbol className=\"text-[14px]\" name=\"home\" />\n <NumberFlow\n value={watts}\n suffix=\"W\"\n className=\"mt-1 text-sm leading-none font-medium\"\n />\n </div>\n </div>\n )\n}\n\nfunction HomeSkeleton({\n anchorRef,\n className,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n className?: string\n}): ReactElement {\n return (\n <div\n ref={anchorRef}\n className={cn(\n 'mx-auto size-[78px] animate-pulse rounded-circle border border-gray motion-reduce:animate-none',\n className,\n )}\n />\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"power-network.js","names":[],"sources":["../../src/components/power-network.tsx"],"sourcesContent":["import {\n createContext,\n type ReactElement,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useReducedMotion } from '../hooks/use-reduced-motion'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\nimport NumberFlow from './number-flow'\nimport { Skeleton } from './skeleton'\nimport { SectionTitle } from './typography'\n\n// --- Variants -----------------------------------------------------------\n\nexport type PowerSourceVariant = 'grid' | 'solar' | 'battery' | 'neutral'\n\ninterface SourceVariantStyles {\n activeClassName: string\n flowStroke: string\n initialDashGap: number\n}\n\nconst SOURCE_VARIANTS: Record<PowerSourceVariant, SourceVariantStyles> = {\n grid: {\n activeClassName: 'border-blue-a9 bg-blue-a1 shadow-blue-a4',\n flowStroke: 'var(--blue-11)',\n initialDashGap: 10,\n },\n solar: {\n activeClassName: 'border-orange-a9 bg-orange-a1 shadow-orange-a3',\n flowStroke: 'var(--orange-a9)',\n initialDashGap: 5,\n },\n battery: {\n activeClassName: 'border-green-a9 bg-green-a1 shadow-green-a3',\n flowStroke: 'var(--green-a9)',\n initialDashGap: 7,\n },\n neutral: {\n activeClassName: 'border-gray-a9 bg-gray-a1 shadow-gray-a3',\n flowStroke: 'var(--neutral-a9)',\n initialDashGap: 9,\n },\n}\n\nconst DEVICE_FLOW_STROKE = 'var(--neutral-a9)'\nconst DEVICE_INITIAL_DASH_GAP = 9\n\n// Drawn under every connector and revealed when nothing flows, so a 0 W node\n// still reads as wired up and online rather than disconnected.\nconst IDLE_STROKE = 'var(--neutral-a6)'\nconst SKELETON_STROKE = 'var(--neutral-a6)'\nconst STROKE_WIDTH = 2\nconst STRAIGHT_TOLERANCE_PX = 3\nconst SOURCE_HOME_OFFSET_STEP_PX = 10\nconst DEVICE_HOME_OFFSET_STEP_PX = 10\n\n// --- Flow animation parameters ------------------------------------------\n\nconst FLOW_DASH_SOLID = 1\nconst FLOW_GAP_MAX = 14\nconst FLOW_GAP_MIN = 5\nconst FLOW_SPEED_MIN = 10\nconst FLOW_SPEED_MAX = 56\nconst FLOW_SMOOTHING = 5.5\n\ninterface FlowVisual {\n gap: number\n speed: number\n opacity: number\n}\n\ninterface FlowAnimationState extends FlowVisual {\n offset: number\n}\n\n// --- Context ------------------------------------------------------------\n\ntype NodeKind = 'source' | 'device' | 'home'\n\ninterface RegisteredNode {\n id: string\n kind: NodeKind\n variant?: PowerSourceVariant\n watts: number\n element: HTMLDivElement | null\n}\n\ninterface PowerNetworkContextValue {\n loading: boolean\n register: (node: RegisteredNode) => void\n unregister: (id: string) => void\n totalSourceWatts: number\n}\n\nconst PowerNetworkContext = createContext<PowerNetworkContextValue | null>(null)\n\nfunction usePowerNetwork(): PowerNetworkContextValue {\n const ctx = useContext(PowerNetworkContext)\n if (!ctx) {\n throw new Error(\n 'Power network sub-components must be used inside <PowerNetworkDiagram>.',\n )\n }\n return ctx\n}\n\n// --- Geometry helpers ---------------------------------------------------\n\ninterface Point {\n x: number\n y: number\n}\n\nfunction anchorTop(rect: DOMRect, containerRect: DOMRect): Point {\n return {\n x: rect.left + rect.width / 2 - containerRect.left,\n y: rect.top - containerRect.top,\n }\n}\n\nfunction anchorBottom(rect: DOMRect, containerRect: DOMRect): Point {\n return {\n x: rect.left + rect.width / 2 - containerRect.left,\n y: rect.bottom - containerRect.top,\n }\n}\n\nfunction buildAdaptivePath(from: Point, to: Point): string {\n if (Math.abs(from.x - to.x) < STRAIGHT_TOLERANCE_PX) {\n return `M ${from.x} ${from.y} L ${to.x} ${to.y}`\n }\n const dy = to.y - from.y\n const c1y = from.y + dy * 0.56\n const c2y = to.y - dy * 0.58\n return `M ${from.x} ${from.y} C ${from.x} ${c1y} ${to.x} ${c2y} ${to.x} ${to.y}`\n}\n\n// --- PowerNetworkDiagram ------------------------------------------------\n\nexport interface PowerNetworkDiagramProps {\n title?: string\n loading?: boolean\n className?: string\n children?: ReactNode\n}\n\ninterface ConnectorPath {\n id: string\n d: string\n kind: NodeKind\n variant?: PowerSourceVariant\n reversed: boolean\n}\n\nfunction connectorPathsEqual(a: ConnectorPath[], b: ConnectorPath[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n const ap = a[i]\n const bp = b[i]\n if (!ap || !bp) return false\n if (\n ap.id !== bp.id ||\n ap.d !== bp.d ||\n ap.variant !== bp.variant ||\n ap.kind !== bp.kind ||\n ap.reversed !== bp.reversed\n ) {\n return false\n }\n }\n return true\n}\n\nexport function PowerNetworkDiagram({\n title,\n loading = false,\n className,\n children,\n}: PowerNetworkDiagramProps): ReactElement {\n const containerRef = useRef<HTMLDivElement>(null)\n const reducedMotion = useReducedMotion()\n const [nodes, setNodes] = useState<Map<string, RegisteredNode>>(\n () => new Map(),\n )\n const [connectorPaths, setConnectorPaths] = useState<ConnectorPath[]>([])\n const connectorPathsRef = useRef<ConnectorPath[]>([])\n const flowPathRefs = useRef<Record<string, SVGPathElement | null>>({})\n const idlePathRefs = useRef<Record<string, SVGPathElement | null>>({})\n const currentFlowVisualsRef = useRef<Record<string, FlowAnimationState>>({})\n const targetFlowVisualsRef = useRef<Record<string, FlowVisual>>({})\n\n const register = useCallback((node: RegisteredNode) => {\n setNodes((prev) => {\n const existing = prev.get(node.id)\n if (\n existing &&\n existing.kind === node.kind &&\n existing.variant === node.variant &&\n existing.watts === node.watts &&\n existing.element === node.element\n ) {\n return prev\n }\n const next = new Map(prev)\n next.set(node.id, node)\n return next\n })\n }, [])\n\n const unregister = useCallback((id: string) => {\n setNodes((prev) => {\n if (!prev.has(id)) return prev\n const next = new Map(prev)\n next.delete(id)\n return next\n })\n }, [])\n\n const totalSourceWatts = useMemo(() => {\n let total = 0\n for (const node of nodes.values()) {\n if (node.kind === 'source') total += node.watts\n }\n return total\n }, [nodes])\n\n useEffect(() => {\n const container = containerRef.current\n if (!container) return\n\n let homeNode: RegisteredNode | undefined\n const sources: RegisteredNode[] = []\n const devices: RegisteredNode[] = []\n for (const node of nodes.values()) {\n if (node.kind === 'home') {\n homeNode = node\n } else if (node.element) {\n if (node.kind === 'source') sources.push(node)\n else devices.push(node)\n }\n }\n\n const updatePaths = () => {\n if (!homeNode || !homeNode.element) {\n setConnectorPaths((prev) => (prev.length === 0 ? prev : []))\n return\n }\n const containerRect = container.getBoundingClientRect()\n const homeRect = homeNode.element.getBoundingClientRect()\n const homeTop = anchorTop(homeRect, containerRect)\n const homeBottom = anchorBottom(homeRect, containerRect)\n const next: ConnectorPath[] = []\n\n const sourceCount = sources.length\n const deviceCount = devices.length\n\n sources.forEach((source, index) => {\n if (!source.element) return\n const from = anchorBottom(\n source.element.getBoundingClientRect(),\n containerRect,\n )\n const offsetX =\n SOURCE_HOME_OFFSET_STEP_PX * (index - (sourceCount - 1) / 2)\n const to = { x: homeTop.x + offsetX, y: homeTop.y }\n next.push({\n id: source.id,\n d: buildAdaptivePath(from, to),\n kind: 'source',\n variant: source.variant,\n reversed: source.watts < 0,\n })\n })\n\n devices.forEach((device, index) => {\n if (!device.element) return\n const to = anchorTop(\n device.element.getBoundingClientRect(),\n containerRect,\n )\n const offsetX =\n DEVICE_HOME_OFFSET_STEP_PX * (index - (deviceCount - 1) / 2)\n const from = { x: homeBottom.x + offsetX, y: homeBottom.y }\n next.push({\n id: device.id,\n d: buildAdaptivePath(from, to),\n kind: 'device',\n reversed: false,\n })\n })\n\n setConnectorPaths((prev) =>\n connectorPathsEqual(prev, next) ? prev : next,\n )\n }\n\n updatePaths()\n\n const resizeObserver = new ResizeObserver(updatePaths)\n resizeObserver.observe(container)\n for (const node of nodes.values()) {\n if (node.element) resizeObserver.observe(node.element)\n }\n window.addEventListener('resize', updatePaths)\n\n return () => {\n resizeObserver.disconnect()\n window.removeEventListener('resize', updatePaths)\n }\n }, [nodes])\n\n useEffect(() => {\n connectorPathsRef.current = connectorPaths\n }, [connectorPaths])\n\n useEffect(() => {\n const targets: Record<string, FlowVisual> = {}\n const homeWatts = totalSourceWatts\n for (const node of nodes.values()) {\n if (node.kind === 'home') continue\n const watts = node.watts\n const relative =\n homeWatts > 0\n ? Math.max(0, Math.min(1, Math.abs(watts) / homeWatts))\n : 0\n targets[node.id] = {\n gap: FLOW_GAP_MAX - (FLOW_GAP_MAX - FLOW_GAP_MIN) * relative,\n speed: FLOW_SPEED_MIN + (FLOW_SPEED_MAX - FLOW_SPEED_MIN) * relative,\n opacity: Math.abs(watts) > 0 ? 1 : 0,\n }\n }\n targetFlowVisualsRef.current = targets\n }, [nodes, totalSourceWatts])\n\n useEffect(() => {\n if (loading || reducedMotion) return\n let frameId = 0\n let lastFrameTime = performance.now()\n\n const animate = (now: number) => {\n const deltaSeconds = Math.max(0.001, (now - lastFrameTime) / 1000)\n lastFrameTime = now\n const smoothing = 1 - Math.exp(-FLOW_SMOOTHING * deltaSeconds)\n\n for (const connectorPath of connectorPathsRef.current) {\n const id = connectorPath.id\n const target = targetFlowVisualsRef.current[id]\n const pathElement = flowPathRefs.current[id]\n if (!target || !pathElement) continue\n\n const current = currentFlowVisualsRef.current[id] ?? {\n gap: target.gap,\n offset: 0,\n speed: target.speed,\n opacity: target.opacity,\n }\n\n current.gap += (target.gap - current.gap) * smoothing\n current.speed += (target.speed - current.speed) * smoothing\n current.opacity += (target.opacity - current.opacity) * smoothing\n\n const dashCycle = FLOW_DASH_SOLID + Math.max(0.001, current.gap)\n const direction = connectorPath.reversed ? 1 : -1\n const nextOffset =\n current.offset + direction * current.speed * deltaSeconds\n const wrappedOffset = ((nextOffset % dashCycle) + dashCycle) % dashCycle\n current.offset = wrappedOffset - dashCycle\n\n pathElement.setAttribute(\n 'stroke-dasharray',\n `${FLOW_DASH_SOLID} ${current.gap.toFixed(3)}`,\n )\n pathElement.setAttribute('stroke-dashoffset', current.offset.toFixed(3))\n pathElement.setAttribute('opacity', current.opacity.toFixed(3))\n idlePathRefs.current[id]?.setAttribute(\n 'opacity',\n (1 - current.opacity).toFixed(3),\n )\n\n currentFlowVisualsRef.current[id] = current\n }\n\n frameId = window.requestAnimationFrame(animate)\n }\n\n frameId = window.requestAnimationFrame(animate)\n return () => window.cancelAnimationFrame(frameId)\n }, [loading, reducedMotion])\n\n // Reduced motion: freeze the marching dashes but keep the dash gap tracking\n // live magnitude, and let opacity fade in/out via the CSS transition on each\n // flow path (see the flow <path> style below).\n useEffect(() => {\n if (loading || !reducedMotion) return\n for (const connectorPath of connectorPathsRef.current) {\n const target = targetFlowVisualsRef.current[connectorPath.id]\n const pathElement = flowPathRefs.current[connectorPath.id]\n if (!target || !pathElement) continue\n pathElement.setAttribute(\n 'stroke-dasharray',\n `${FLOW_DASH_SOLID} ${target.gap.toFixed(3)}`,\n )\n pathElement.setAttribute('stroke-dashoffset', '0')\n pathElement.setAttribute('opacity', target.opacity.toFixed(3))\n idlePathRefs.current[connectorPath.id]?.setAttribute(\n 'opacity',\n (1 - target.opacity).toFixed(3),\n )\n }\n }, [loading, reducedMotion, nodes, totalSourceWatts])\n\n const contextValue = useMemo<PowerNetworkContextValue>(\n () => ({ loading, register, unregister, totalSourceWatts }),\n [loading, register, unregister, totalSourceWatts],\n )\n\n return (\n <PowerNetworkContext.Provider value={contextValue}>\n <div\n ref={containerRef}\n className={cn('@container relative flex flex-col', className)}\n >\n {title && <SectionTitle>{title}</SectionTitle>}\n {connectorPaths.length > 0 && (\n <svg className=\"pointer-events-none absolute inset-0 size-full\">\n {connectorPaths.map((path) => {\n if (loading) {\n return (\n <path\n key={path.id}\n d={path.d}\n fill=\"none\"\n stroke={SKELETON_STROKE}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n />\n )\n }\n\n const flowStroke =\n path.kind === 'source' && path.variant\n ? SOURCE_VARIANTS[path.variant].flowStroke\n : DEVICE_FLOW_STROKE\n const initialDashGap =\n path.kind === 'source' && path.variant\n ? SOURCE_VARIANTS[path.variant].initialDashGap\n : DEVICE_INITIAL_DASH_GAP\n\n return (\n <g key={path.id}>\n <path\n d={path.d}\n fill=\"none\"\n stroke={IDLE_STROKE}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n ref={(el) => {\n idlePathRefs.current[path.id] = el\n }}\n opacity={1}\n style={\n reducedMotion\n ? { transition: 'opacity 300ms ease' }\n : undefined\n }\n />\n <path\n d={path.d}\n fill=\"none\"\n stroke={flowStroke}\n strokeWidth={STROKE_WIDTH}\n strokeLinecap=\"round\"\n ref={(el) => {\n flowPathRefs.current[path.id] = el\n }}\n strokeDasharray={`${FLOW_DASH_SOLID} ${initialDashGap}`}\n strokeDashoffset={0}\n opacity={0}\n style={\n reducedMotion\n ? { transition: 'opacity 300ms ease' }\n : undefined\n }\n />\n </g>\n )\n })}\n </svg>\n )}\n <div className=\"flex flex-col gap-y-14\">{children}</div>\n </div>\n </PowerNetworkContext.Provider>\n )\n}\n\n// --- Row containers -----------------------------------------------------\n\nexport interface PowerNetworkRowProps {\n className?: string\n children?: ReactNode\n}\n\nexport function PowerNetworkSources({\n className,\n children,\n}: PowerNetworkRowProps): ReactElement {\n return (\n <div\n className={cn(\n 'flex items-center justify-center gap-[clamp(1.5rem,10cqw,4rem)]',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\nexport function PowerNetworkDevices({\n className,\n children,\n}: PowerNetworkRowProps): ReactElement {\n return (\n <div\n className={cn(\n 'flex items-center justify-center gap-[clamp(1.5rem,10cqw,4rem)]',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\n// --- PowerSource --------------------------------------------------------\n\nexport interface PowerSourceProps {\n id?: string\n watts: number\n icon: ReactNode\n variant?: PowerSourceVariant\n className?: string\n /**\n * Optional 0–100 progress (e.g. battery SoC). When set, a thin colored arc\n * is drawn around the icon circle as a non-intrusive indicator.\n */\n progress?: number\n /** Keeps the variant colors at 0 W, for a source that is online but idle. */\n standby?: boolean\n}\n\nexport function PowerSource({\n id: idProp,\n watts,\n icon,\n variant = 'neutral',\n className,\n progress,\n standby = false,\n}: PowerSourceProps): ReactElement {\n const autoId = useId()\n const id = idProp ?? autoId\n const { register, unregister, loading } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (!element) return\n register({ id, kind: 'source', variant, watts, element })\n }, [element, register, id, variant, watts])\n\n useEffect(() => {\n return () => unregister(id)\n }, [unregister, id])\n\n if (loading) {\n return <SourceSkeleton anchorRef={setElement} />\n }\n\n const isActive = watts !== 0\n const isColored = isActive || standby\n const variantStyles = SOURCE_VARIANTS[variant]\n const clampedProgress =\n progress == null ? null : Math.max(0, Math.min(100, progress))\n const showRing = clampedProgress != null\n const RING_RADIUS = 48\n const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS\n const ringDash =\n clampedProgress != null ? (clampedProgress / 100) * RING_CIRCUMFERENCE : 0\n\n return (\n <div\n className={cn(\n 'flex flex-col items-center justify-center gap-1 transition-opacity duration-500',\n !isColored && 'opacity-45',\n className,\n )}\n >\n <NumberFlow\n value={Math.abs(watts)}\n suffix=\"W\"\n className=\"mt-1 text-sm leading-none font-medium\"\n />\n <div\n ref={setElement}\n className={cn(\n 'relative flex aspect-square items-center justify-center rounded-full border p-4 shadow-sm',\n isColored\n ? variantStyles.activeClassName\n : 'border-gray bg-gray-a3 shadow-gray-a4 saturate-0',\n )}\n >\n {showRing && (\n <svg\n className=\"pointer-events-none absolute inset-0 size-full -rotate-90\"\n viewBox=\"0 0 100 100\"\n aria-hidden=\"true\"\n >\n <circle\n cx=\"50\"\n cy=\"50\"\n r={RING_RADIUS}\n fill=\"none\"\n stroke={variantStyles.flowStroke}\n strokeWidth=\"4\"\n strokeLinecap=\"round\"\n strokeDasharray={`${ringDash} ${RING_CIRCUMFERENCE}`}\n opacity={isColored ? 1 : 0.5}\n />\n </svg>\n )}\n {icon}\n </div>\n </div>\n )\n}\n\nfunction SourceSkeleton({\n anchorRef,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n}): ReactElement {\n return (\n <div className=\"flex flex-col items-center justify-center gap-1\">\n <Skeleton className=\"h-3.5 w-10\" />\n <div\n ref={anchorRef}\n className=\"flex aspect-square animate-pulse items-center justify-center rounded-full border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none\"\n >\n <div className=\"size-7\" />\n </div>\n </div>\n )\n}\n\n// --- PowerDevice --------------------------------------------------------\n\nexport interface PowerDeviceProps {\n id?: string\n watts: number\n icon: ReactNode\n className?: string\n}\n\nexport function PowerDevice({\n id: idProp,\n watts,\n icon,\n className,\n}: PowerDeviceProps): ReactElement {\n const autoId = useId()\n const id = idProp ?? autoId\n const { register, unregister, loading } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (!element) return\n register({ id, kind: 'device', watts, element })\n }, [element, register, id, watts])\n\n useEffect(() => {\n return () => unregister(id)\n }, [unregister, id])\n\n if (loading) {\n return <DeviceSkeleton anchorRef={setElement} />\n }\n\n return (\n <div\n className={cn(\n 'flex flex-col items-center justify-center gap-1',\n className,\n )}\n >\n <div\n ref={setElement}\n className=\"flex aspect-square items-center justify-center rounded-full border border-gray bg-gray-a1 p-4\"\n >\n {icon}\n </div>\n <NumberFlow\n value={watts}\n suffix=\"W\"\n className=\"text-sm font-medium tabular-nums\"\n />\n </div>\n )\n}\n\nfunction DeviceSkeleton({\n anchorRef,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n}): ReactElement {\n return (\n <div className=\"flex flex-col items-center justify-center gap-1\">\n <div\n ref={anchorRef}\n className=\"flex aspect-square animate-pulse items-center justify-center rounded-full border border-gray bg-base-gray-soft p-4 motion-reduce:animate-none\"\n >\n <div className=\"size-5\" />\n </div>\n <Skeleton className=\"h-3.5 w-10\" />\n </div>\n )\n}\n\n// --- PowerNetworkHome ---------------------------------------------------\n\nexport interface PowerNetworkHomeProps {\n /** Override total power. Defaults to the sum of registered source watts. */\n watts?: number\n /** Self-sufficiency ratio in 0..1. Renders a green ring when provided. */\n autarky?: number\n className?: string\n}\n\nexport function PowerNetworkHome({\n watts,\n autarky,\n className,\n}: PowerNetworkHomeProps): ReactElement {\n const autoId = useId()\n const { register, unregister, loading, totalSourceWatts } = usePowerNetwork()\n const [element, setElement] = useState<HTMLDivElement | null>(null)\n const effectiveWatts = watts ?? totalSourceWatts\n\n useEffect(() => {\n if (!element) return\n register({ id: autoId, kind: 'home', watts: effectiveWatts, element })\n }, [element, register, autoId, effectiveWatts])\n\n useEffect(() => {\n return () => unregister(autoId)\n }, [unregister, autoId])\n\n if (loading) {\n return <HomeSkeleton anchorRef={setElement} className={className} />\n }\n\n return (\n <HomeSection\n anchorRef={setElement}\n autarky={autarky ?? null}\n watts={effectiveWatts}\n className={className}\n />\n )\n}\n\nfunction HomeSection({\n anchorRef,\n autarky,\n className,\n watts,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n autarky: number | null\n className?: string\n watts: number\n}): ReactElement {\n const reducedMotion = useReducedMotion()\n const targetAutarky =\n autarky == null ? 0 : Math.max(0, Math.min(100, autarky * 100))\n const [animatedAutarky, setAnimatedAutarky] = useState(targetAutarky)\n const animatedAutarkyRef = useRef(animatedAutarky)\n const nodeSize = 78\n const ringStrokeWidth = 2\n const radius = (nodeSize - ringStrokeWidth) / 2\n const circumference = 2 * Math.PI * radius\n const progressLength = (animatedAutarky / 100) * circumference\n\n useEffect(() => {\n animatedAutarkyRef.current = animatedAutarky\n }, [animatedAutarky])\n\n useEffect(() => {\n if (reducedMotion) {\n animatedAutarkyRef.current = targetAutarky\n setAnimatedAutarky(targetAutarky)\n return\n }\n\n let frameId = 0\n let lastTime = performance.now()\n\n const animate = (now: number) => {\n const deltaSeconds = Math.max(0.001, (now - lastTime) / 1000)\n lastTime = now\n const smoothing = 1 - Math.exp(-7 * deltaSeconds)\n const next =\n animatedAutarkyRef.current +\n (targetAutarky - animatedAutarkyRef.current) * smoothing\n\n animatedAutarkyRef.current = next\n setAnimatedAutarky(next)\n\n if (Math.abs(targetAutarky - next) > 0.05) {\n frameId = window.requestAnimationFrame(animate)\n } else {\n animatedAutarkyRef.current = targetAutarky\n setAnimatedAutarky(targetAutarky)\n }\n }\n\n frameId = window.requestAnimationFrame(animate)\n return () => window.cancelAnimationFrame(frameId)\n }, [targetAutarky, reducedMotion])\n\n return (\n <div\n ref={anchorRef}\n className={cn('relative mx-auto size-[78px]', className)}\n >\n {autarky != null && (\n <svg\n className=\"absolute inset-0 size-full -rotate-90\"\n viewBox={`0 0 ${nodeSize} ${nodeSize}`}\n >\n <circle\n cx={nodeSize / 2}\n cy={nodeSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"var(--neutral-a5)\"\n strokeWidth={ringStrokeWidth}\n />\n {progressLength > 0 && (\n <circle\n cx={nodeSize / 2}\n cy={nodeSize / 2}\n r={radius}\n fill=\"none\"\n stroke=\"var(--green-a9)\"\n strokeLinecap=\"round\"\n strokeWidth={ringStrokeWidth}\n strokeDasharray={`${progressLength} ${circumference}`}\n />\n )}\n </svg>\n )}\n <div className=\"absolute inset-0 flex flex-col items-center justify-center rounded-full border border-gray-a5 shadow-sm shadow-gray-a1\">\n <MaterialSymbol className=\"text-[14px]\" name=\"home\" />\n <NumberFlow\n value={watts}\n suffix=\"W\"\n className=\"mt-1 text-sm leading-none font-medium\"\n />\n </div>\n </div>\n )\n}\n\nfunction HomeSkeleton({\n anchorRef,\n className,\n}: {\n anchorRef: (element: HTMLDivElement | null) => void\n className?: string\n}): ReactElement {\n return (\n <div\n ref={anchorRef}\n className={cn(\n 'mx-auto size-[78px] animate-pulse rounded-full border border-gray motion-reduce:animate-none',\n className,\n )}\n />\n )\n}\n"],"mappings":"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"}
@@ -22,7 +22,7 @@ ScheduleListEmpty.displayName = "ScheduleListEmpty";
22
22
  const ScheduleListAdd = react.forwardRef(({ children, className, icon, type = "button", ...props }, ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
23
23
  ref,
24
24
  type,
25
- className: (0, cnfast.cn)("inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-2xl border border-dashed border-gray text-sm font-medium text-gray-light transition-colors hover:border-spark-a8 hover:bg-base-accent-soft hover:text-accent focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className),
25
+ className: (0, cnfast.cn)("inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-gray text-sm font-medium text-gray-light transition-colors hover:border-spark-a8 hover:bg-base-accent-soft hover:text-accent focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className),
26
26
  ...props,
27
27
  children: [icon ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons_material_symbol.MaterialSymbol, {
28
28
  className: "text-[14px]",
@@ -46,7 +46,7 @@ const ScheduleEntry = react.forwardRef(({ children, className, disabled, onClick
46
46
  }
47
47
  } : void 0,
48
48
  "aria-disabled": disabled || void 0,
49
- className: (0, cnfast.cn)("flex items-center gap-3 rounded-2xl bg-base-gray-soft px-3 py-2.5 transition-colors", interactive && "cursor-pointer hover:bg-gray-a3 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden", disabled && "cursor-not-allowed opacity-50", className),
49
+ className: (0, cnfast.cn)("flex items-center gap-3 rounded-lg bg-base-gray-soft px-3 py-2.5 transition-colors", interactive && "cursor-pointer hover:bg-gray-a3 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden", disabled && "cursor-not-allowed opacity-50", className),
50
50
  ...props,
51
51
  children
52
52
  });
@@ -91,7 +91,7 @@ const ScheduleEntryDelete = react.forwardRef(({ "aria-label": ariaLabel = "Delet
91
91
  event.stopPropagation();
92
92
  onClick?.(event);
93
93
  },
94
- className: (0, cnfast.cn)("inline-flex size-8 shrink-0 items-center justify-center rounded-circle text-gray-light transition-colors hover:bg-base-error-soft hover:text-error focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className),
94
+ className: (0, cnfast.cn)("inline-flex size-8 shrink-0 items-center justify-center rounded-full text-gray-light transition-colors hover:bg-base-error-soft hover:text-error focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className),
95
95
  ...props,
96
96
  children: children ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons_material_symbol.MaterialSymbol, {
97
97
  className: "text-[14px]",
@@ -1 +1 @@
1
- {"version":3,"file":"schedule-list.cjs","names":["React","MaterialSymbol"],"sources":["../../src/components/schedule-list.tsx"],"sourcesContent":["import * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\nconst ScheduleList = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n 'flex flex-col gap-[calc(var(--spark-layout-gap)*2)]',\n className,\n )}\n {...props}\n />\n))\n\nScheduleList.displayName = 'ScheduleList'\n\nconst ScheduleListEmpty = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n <p\n ref={ref}\n className={cn('px-3 py-2 text-sm text-gray-light', className)}\n {...props}\n />\n))\n\nScheduleListEmpty.displayName = 'ScheduleListEmpty'\n\ninterface ScheduleListAddProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Optional icon override; defaults to a `+`. */\n icon?: React.ReactNode\n}\n\nconst ScheduleListAdd = React.forwardRef<\n HTMLButtonElement,\n ScheduleListAddProps\n>(({ children, className, icon, type = 'button', ...props }, ref) => (\n <button\n ref={ref}\n type={type}\n className={cn(\n 'inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-2xl border border-dashed border-gray text-sm font-medium text-gray-light transition-colors hover:border-spark-a8 hover:bg-base-accent-soft hover:text-accent focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {icon ?? <MaterialSymbol className=\"text-[14px]\" name=\"add\" />}\n {children}\n </button>\n))\n\nScheduleListAdd.displayName = 'ScheduleListAdd'\n\ninterface ScheduleEntryProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Make the row interactive — enables hover, role=button, keyboard activation. */\n onClick?: () => void\n disabled?: boolean\n}\n\nconst ScheduleEntry = React.forwardRef<HTMLDivElement, ScheduleEntryProps>(\n ({ children, className, disabled, onClick, ...props }, ref) => {\n const interactive = onClick !== undefined && !disabled\n // role, tab stop and key handler travel together -- the row is never\n // clickable without Enter/Space, and never focusable without a role\n const interaction = interactive\n ? {\n role: 'button' as const,\n tabIndex: 0,\n onClick,\n onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onClick()\n }\n },\n }\n : undefined\n return (\n <div\n ref={ref}\n {...interaction}\n aria-disabled={disabled || undefined}\n className={cn(\n 'flex items-center gap-3 rounded-2xl bg-base-gray-soft px-3 py-2.5 transition-colors',\n interactive &&\n 'cursor-pointer hover:bg-gray-a3 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden',\n disabled && 'cursor-not-allowed opacity-50',\n className,\n )}\n {...props}\n >\n {children}\n </div>\n )\n },\n)\n\nScheduleEntry.displayName = 'ScheduleEntry'\n\nconst ScheduleEntryContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex min-w-0 flex-1 flex-col gap-0.5', className)}\n {...props}\n />\n))\n\nScheduleEntryContent.displayName = 'ScheduleEntryContent'\n\nconst ScheduleEntryTitle = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n 'flex min-w-0 items-center gap-2 truncate text-sm font-medium text-gray',\n className,\n )}\n {...props}\n />\n))\n\nScheduleEntryTitle.displayName = 'ScheduleEntryTitle'\n\nconst ScheduleEntrySubtitle = React.forwardRef<\n HTMLSpanElement,\n React.HTMLAttributes<HTMLSpanElement>\n>(({ className, ...props }, ref) => (\n <span\n ref={ref}\n className={cn('truncate text-xs text-gray-light', className)}\n {...props}\n />\n))\n\nScheduleEntrySubtitle.displayName = 'ScheduleEntrySubtitle'\n\n/** Right-aligned action area. Stops click propagation so inner controls\n * don't fire the parent `ScheduleEntry`'s `onClick`. */\nconst ScheduleEntryActions = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, onClick, ...props }, ref) => (\n <div\n ref={ref}\n // a layout wrapper, not a control: the handler only shields the inner\n // buttons' clicks (keyboard activation included) from the parent row\n role=\"presentation\"\n onClick={(event) => {\n event.stopPropagation()\n onClick?.(event)\n }}\n className={cn('flex shrink-0 items-center gap-1.5', className)}\n {...props}\n />\n))\n\nScheduleEntryActions.displayName = 'ScheduleEntryActions'\n\ninterface ScheduleEntryDeleteProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}\n\nconst ScheduleEntryDelete = React.forwardRef<\n HTMLButtonElement,\n ScheduleEntryDeleteProps\n>(\n (\n {\n 'aria-label': ariaLabel = 'Delete schedule entry',\n children,\n className,\n onClick,\n type = 'button',\n ...props\n },\n ref,\n ) => (\n <button\n ref={ref}\n type={type}\n aria-label={ariaLabel}\n onClick={(event) => {\n event.stopPropagation()\n onClick?.(event)\n }}\n className={cn(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-circle text-gray-light transition-colors hover:bg-base-error-soft hover:text-error focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {children ?? <MaterialSymbol className=\"text-[14px]\" name=\"delete\" />}\n </button>\n ),\n)\n\nScheduleEntryDelete.displayName = 'ScheduleEntryDelete'\n\nexport {\n ScheduleEntry,\n ScheduleEntryActions,\n ScheduleEntryContent,\n ScheduleEntryDelete,\n ScheduleEntrySubtitle,\n ScheduleEntryTitle,\n ScheduleList,\n ScheduleListAdd,\n ScheduleListEmpty,\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"schedule-list.cjs","names":["React","MaterialSymbol"],"sources":["../../src/components/schedule-list.tsx"],"sourcesContent":["import * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\nconst ScheduleList = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n 'flex flex-col gap-[calc(var(--spark-layout-gap)*2)]',\n className,\n )}\n {...props}\n />\n))\n\nScheduleList.displayName = 'ScheduleList'\n\nconst ScheduleListEmpty = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n <p\n ref={ref}\n className={cn('px-3 py-2 text-sm text-gray-light', className)}\n {...props}\n />\n))\n\nScheduleListEmpty.displayName = 'ScheduleListEmpty'\n\ninterface ScheduleListAddProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Optional icon override; defaults to a `+`. */\n icon?: React.ReactNode\n}\n\nconst ScheduleListAdd = React.forwardRef<\n HTMLButtonElement,\n ScheduleListAddProps\n>(({ children, className, icon, type = 'button', ...props }, ref) => (\n <button\n ref={ref}\n type={type}\n className={cn(\n 'inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-gray text-sm font-medium text-gray-light transition-colors hover:border-spark-a8 hover:bg-base-accent-soft hover:text-accent focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {icon ?? <MaterialSymbol className=\"text-[14px]\" name=\"add\" />}\n {children}\n </button>\n))\n\nScheduleListAdd.displayName = 'ScheduleListAdd'\n\ninterface ScheduleEntryProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Make the row interactive — enables hover, role=button, keyboard activation. */\n onClick?: () => void\n disabled?: boolean\n}\n\nconst ScheduleEntry = React.forwardRef<HTMLDivElement, ScheduleEntryProps>(\n ({ children, className, disabled, onClick, ...props }, ref) => {\n const interactive = onClick !== undefined && !disabled\n // role, tab stop and key handler travel together -- the row is never\n // clickable without Enter/Space, and never focusable without a role\n const interaction = interactive\n ? {\n role: 'button' as const,\n tabIndex: 0,\n onClick,\n onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onClick()\n }\n },\n }\n : undefined\n return (\n <div\n ref={ref}\n {...interaction}\n aria-disabled={disabled || undefined}\n className={cn(\n 'flex items-center gap-3 rounded-lg bg-base-gray-soft px-3 py-2.5 transition-colors',\n interactive &&\n 'cursor-pointer hover:bg-gray-a3 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden',\n disabled && 'cursor-not-allowed opacity-50',\n className,\n )}\n {...props}\n >\n {children}\n </div>\n )\n },\n)\n\nScheduleEntry.displayName = 'ScheduleEntry'\n\nconst ScheduleEntryContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex min-w-0 flex-1 flex-col gap-0.5', className)}\n {...props}\n />\n))\n\nScheduleEntryContent.displayName = 'ScheduleEntryContent'\n\nconst ScheduleEntryTitle = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n 'flex min-w-0 items-center gap-2 truncate text-sm font-medium text-gray',\n className,\n )}\n {...props}\n />\n))\n\nScheduleEntryTitle.displayName = 'ScheduleEntryTitle'\n\nconst ScheduleEntrySubtitle = React.forwardRef<\n HTMLSpanElement,\n React.HTMLAttributes<HTMLSpanElement>\n>(({ className, ...props }, ref) => (\n <span\n ref={ref}\n className={cn('truncate text-xs text-gray-light', className)}\n {...props}\n />\n))\n\nScheduleEntrySubtitle.displayName = 'ScheduleEntrySubtitle'\n\n/** Right-aligned action area. Stops click propagation so inner controls\n * don't fire the parent `ScheduleEntry`'s `onClick`. */\nconst ScheduleEntryActions = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, onClick, ...props }, ref) => (\n <div\n ref={ref}\n // a layout wrapper, not a control: the handler only shields the inner\n // buttons' clicks (keyboard activation included) from the parent row\n role=\"presentation\"\n onClick={(event) => {\n event.stopPropagation()\n onClick?.(event)\n }}\n className={cn('flex shrink-0 items-center gap-1.5', className)}\n {...props}\n />\n))\n\nScheduleEntryActions.displayName = 'ScheduleEntryActions'\n\ninterface ScheduleEntryDeleteProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}\n\nconst ScheduleEntryDelete = React.forwardRef<\n HTMLButtonElement,\n ScheduleEntryDeleteProps\n>(\n (\n {\n 'aria-label': ariaLabel = 'Delete schedule entry',\n children,\n className,\n onClick,\n type = 'button',\n ...props\n },\n ref,\n ) => (\n <button\n ref={ref}\n type={type}\n aria-label={ariaLabel}\n onClick={(event) => {\n event.stopPropagation()\n onClick?.(event)\n }}\n className={cn(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-full text-gray-light transition-colors hover:bg-base-error-soft hover:text-error focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {children ?? <MaterialSymbol className=\"text-[14px]\" name=\"delete\" />}\n </button>\n ),\n)\n\nScheduleEntryDelete.displayName = 'ScheduleEntryDelete'\n\nexport {\n ScheduleEntry,\n ScheduleEntryActions,\n ScheduleEntryContent,\n ScheduleEntryDelete,\n ScheduleEntrySubtitle,\n ScheduleEntryTitle,\n ScheduleList,\n ScheduleListAdd,\n ScheduleListEmpty,\n}\n"],"mappings":"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"}
@@ -19,7 +19,7 @@ ScheduleListEmpty.displayName = "ScheduleListEmpty";
19
19
  const ScheduleListAdd = React$1.forwardRef(({ children, className, icon, type = "button", ...props }, ref) => /* @__PURE__ */ jsxs("button", {
20
20
  ref,
21
21
  type,
22
- className: cn("inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-2xl border border-dashed border-gray text-sm font-medium text-gray-light transition-colors hover:border-spark-a8 hover:bg-base-accent-soft hover:text-accent focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className),
22
+ className: cn("inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-gray text-sm font-medium text-gray-light transition-colors hover:border-spark-a8 hover:bg-base-accent-soft hover:text-accent focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className),
23
23
  ...props,
24
24
  children: [icon ?? /* @__PURE__ */ jsx(MaterialSymbol, {
25
25
  className: "text-[14px]",
@@ -43,7 +43,7 @@ const ScheduleEntry = React$1.forwardRef(({ children, className, disabled, onCli
43
43
  }
44
44
  } : void 0,
45
45
  "aria-disabled": disabled || void 0,
46
- className: cn("flex items-center gap-3 rounded-2xl bg-base-gray-soft px-3 py-2.5 transition-colors", interactive && "cursor-pointer hover:bg-gray-a3 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden", disabled && "cursor-not-allowed opacity-50", className),
46
+ className: cn("flex items-center gap-3 rounded-lg bg-base-gray-soft px-3 py-2.5 transition-colors", interactive && "cursor-pointer hover:bg-gray-a3 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden", disabled && "cursor-not-allowed opacity-50", className),
47
47
  ...props,
48
48
  children
49
49
  });
@@ -88,7 +88,7 @@ const ScheduleEntryDelete = React$1.forwardRef(({ "aria-label": ariaLabel = "Del
88
88
  event.stopPropagation();
89
89
  onClick?.(event);
90
90
  },
91
- className: cn("inline-flex size-8 shrink-0 items-center justify-center rounded-circle text-gray-light transition-colors hover:bg-base-error-soft hover:text-error focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className),
91
+ className: cn("inline-flex size-8 shrink-0 items-center justify-center rounded-full text-gray-light transition-colors hover:bg-base-error-soft hover:text-error focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className),
92
92
  ...props,
93
93
  children: children ?? /* @__PURE__ */ jsx(MaterialSymbol, {
94
94
  className: "text-[14px]",
@@ -1 +1 @@
1
- {"version":3,"file":"schedule-list.js","names":["React"],"sources":["../../src/components/schedule-list.tsx"],"sourcesContent":["import * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\nconst ScheduleList = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n 'flex flex-col gap-[calc(var(--spark-layout-gap)*2)]',\n className,\n )}\n {...props}\n />\n))\n\nScheduleList.displayName = 'ScheduleList'\n\nconst ScheduleListEmpty = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n <p\n ref={ref}\n className={cn('px-3 py-2 text-sm text-gray-light', className)}\n {...props}\n />\n))\n\nScheduleListEmpty.displayName = 'ScheduleListEmpty'\n\ninterface ScheduleListAddProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Optional icon override; defaults to a `+`. */\n icon?: React.ReactNode\n}\n\nconst ScheduleListAdd = React.forwardRef<\n HTMLButtonElement,\n ScheduleListAddProps\n>(({ children, className, icon, type = 'button', ...props }, ref) => (\n <button\n ref={ref}\n type={type}\n className={cn(\n 'inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-2xl border border-dashed border-gray text-sm font-medium text-gray-light transition-colors hover:border-spark-a8 hover:bg-base-accent-soft hover:text-accent focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {icon ?? <MaterialSymbol className=\"text-[14px]\" name=\"add\" />}\n {children}\n </button>\n))\n\nScheduleListAdd.displayName = 'ScheduleListAdd'\n\ninterface ScheduleEntryProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Make the row interactive — enables hover, role=button, keyboard activation. */\n onClick?: () => void\n disabled?: boolean\n}\n\nconst ScheduleEntry = React.forwardRef<HTMLDivElement, ScheduleEntryProps>(\n ({ children, className, disabled, onClick, ...props }, ref) => {\n const interactive = onClick !== undefined && !disabled\n // role, tab stop and key handler travel together -- the row is never\n // clickable without Enter/Space, and never focusable without a role\n const interaction = interactive\n ? {\n role: 'button' as const,\n tabIndex: 0,\n onClick,\n onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onClick()\n }\n },\n }\n : undefined\n return (\n <div\n ref={ref}\n {...interaction}\n aria-disabled={disabled || undefined}\n className={cn(\n 'flex items-center gap-3 rounded-2xl bg-base-gray-soft px-3 py-2.5 transition-colors',\n interactive &&\n 'cursor-pointer hover:bg-gray-a3 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden',\n disabled && 'cursor-not-allowed opacity-50',\n className,\n )}\n {...props}\n >\n {children}\n </div>\n )\n },\n)\n\nScheduleEntry.displayName = 'ScheduleEntry'\n\nconst ScheduleEntryContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex min-w-0 flex-1 flex-col gap-0.5', className)}\n {...props}\n />\n))\n\nScheduleEntryContent.displayName = 'ScheduleEntryContent'\n\nconst ScheduleEntryTitle = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n 'flex min-w-0 items-center gap-2 truncate text-sm font-medium text-gray',\n className,\n )}\n {...props}\n />\n))\n\nScheduleEntryTitle.displayName = 'ScheduleEntryTitle'\n\nconst ScheduleEntrySubtitle = React.forwardRef<\n HTMLSpanElement,\n React.HTMLAttributes<HTMLSpanElement>\n>(({ className, ...props }, ref) => (\n <span\n ref={ref}\n className={cn('truncate text-xs text-gray-light', className)}\n {...props}\n />\n))\n\nScheduleEntrySubtitle.displayName = 'ScheduleEntrySubtitle'\n\n/** Right-aligned action area. Stops click propagation so inner controls\n * don't fire the parent `ScheduleEntry`'s `onClick`. */\nconst ScheduleEntryActions = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, onClick, ...props }, ref) => (\n <div\n ref={ref}\n // a layout wrapper, not a control: the handler only shields the inner\n // buttons' clicks (keyboard activation included) from the parent row\n role=\"presentation\"\n onClick={(event) => {\n event.stopPropagation()\n onClick?.(event)\n }}\n className={cn('flex shrink-0 items-center gap-1.5', className)}\n {...props}\n />\n))\n\nScheduleEntryActions.displayName = 'ScheduleEntryActions'\n\ninterface ScheduleEntryDeleteProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}\n\nconst ScheduleEntryDelete = React.forwardRef<\n HTMLButtonElement,\n ScheduleEntryDeleteProps\n>(\n (\n {\n 'aria-label': ariaLabel = 'Delete schedule entry',\n children,\n className,\n onClick,\n type = 'button',\n ...props\n },\n ref,\n ) => (\n <button\n ref={ref}\n type={type}\n aria-label={ariaLabel}\n onClick={(event) => {\n event.stopPropagation()\n onClick?.(event)\n }}\n className={cn(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-circle text-gray-light transition-colors hover:bg-base-error-soft hover:text-error focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {children ?? <MaterialSymbol className=\"text-[14px]\" name=\"delete\" />}\n </button>\n ),\n)\n\nScheduleEntryDelete.displayName = 'ScheduleEntryDelete'\n\nexport {\n ScheduleEntry,\n ScheduleEntryActions,\n ScheduleEntryContent,\n ScheduleEntryDelete,\n ScheduleEntrySubtitle,\n ScheduleEntryTitle,\n ScheduleList,\n ScheduleListAdd,\n ScheduleListEmpty,\n}\n"],"mappings":";;;;;;AAIA,MAAM,eAAeA,QAAM,YAGxB,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,OAAD;CACO;CACL,WAAW,GACT,uDACA,SACF;CACA,GAAI;AACL,CAAA,CACF;AAED,aAAa,cAAc;AAE3B,MAAM,oBAAoBA,QAAM,YAG7B,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,KAAD;CACO;CACL,WAAW,GAAG,qCAAqC,SAAS;CAC5D,GAAI;AACL,CAAA,CACF;AAED,kBAAkB,cAAc;AAOhC,MAAM,kBAAkBA,QAAM,YAG3B,EAAE,UAAU,WAAW,MAAM,OAAO,UAAU,GAAG,SAAS,QAC3D,qBAAC,UAAD;CACO;CACC;CACN,WAAW,GACT,kWACA,SACF;CACA,GAAI;WAPN,CASG,QAAQ,oBAAC,gBAAD;EAAgB,WAAU;EAAc,MAAK;CAAO,CAAA,GAC5D,QACK;EACT;AAED,gBAAgB,cAAc;AAQ9B,MAAM,gBAAgBA,QAAM,YACzB,EAAE,UAAU,WAAW,UAAU,SAAS,GAAG,SAAS,QAAQ;CAC7D,MAAM,cAAc,YAAY,KAAA,KAAa,CAAC;CAgB9C,OACE,oBAAC,OAAD;EACO;EACL,GAhBgB,cAChB;GACE,MAAM;GACN,UAAU;GACV;GACA,YAAY,UAA+C;IACzD,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;KAC9C,MAAM,eAAe;KACrB,QAAQ;IACV;GACF;EACF,IACA,KAAA;EAKA,iBAAe,YAAY,KAAA;EAC3B,WAAW,GACT,uFACA,eACE,iHACF,YAAY,iCACZ,SACF;EACA,GAAI;EAEH;CACE,CAAA;AAET,CACF;AAEA,cAAc,cAAc;AAE5B,MAAM,uBAAuBA,QAAM,YAGhC,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,OAAD;CACO;CACL,WAAW,GAAG,wCAAwC,SAAS;CAC/D,GAAI;AACL,CAAA,CACF;AAED,qBAAqB,cAAc;AAEnC,MAAM,qBAAqBA,QAAM,YAG9B,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,OAAD;CACO;CACL,WAAW,GACT,0EACA,SACF;CACA,GAAI;AACL,CAAA,CACF;AAED,mBAAmB,cAAc;AAEjC,MAAM,wBAAwBA,QAAM,YAGjC,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,QAAD;CACO;CACL,WAAW,GAAG,oCAAoC,SAAS;CAC3D,GAAI;AACL,CAAA,CACF;AAED,sBAAsB,cAAc;;;AAIpC,MAAM,uBAAuBA,QAAM,YAGhC,EAAE,WAAW,SAAS,GAAG,SAAS,QACnC,oBAAC,OAAD;CACO;CAGL,MAAK;CACL,UAAU,UAAU;EAClB,MAAM,gBAAgB;EACtB,UAAU,KAAK;CACjB;CACA,WAAW,GAAG,sCAAsC,SAAS;CAC7D,GAAI;AACL,CAAA,CACF;AAED,qBAAqB,cAAc;AAInC,MAAM,sBAAsBA,QAAM,YAK9B,EACE,cAAc,YAAY,yBAC1B,UACA,WACA,SACA,OAAO,UACP,GAAG,SAEL,QAEA,oBAAC,UAAD;CACO;CACC;CACN,cAAY;CACZ,UAAU,UAAU;EAClB,MAAM,gBAAgB;EACtB,UAAU,KAAK;CACjB;CACA,WAAW,GACT,oRACA,SACF;CACA,GAAI;WAEH,YAAY,oBAAC,gBAAD;EAAgB,WAAU;EAAc,MAAK;CAAU,CAAA;AAC9D,CAAA,CAEZ;AAEA,oBAAoB,cAAc"}
1
+ {"version":3,"file":"schedule-list.js","names":["React"],"sources":["../../src/components/schedule-list.tsx"],"sourcesContent":["import * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\nconst ScheduleList = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n 'flex flex-col gap-[calc(var(--spark-layout-gap)*2)]',\n className,\n )}\n {...props}\n />\n))\n\nScheduleList.displayName = 'ScheduleList'\n\nconst ScheduleListEmpty = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n <p\n ref={ref}\n className={cn('px-3 py-2 text-sm text-gray-light', className)}\n {...props}\n />\n))\n\nScheduleListEmpty.displayName = 'ScheduleListEmpty'\n\ninterface ScheduleListAddProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Optional icon override; defaults to a `+`. */\n icon?: React.ReactNode\n}\n\nconst ScheduleListAdd = React.forwardRef<\n HTMLButtonElement,\n ScheduleListAddProps\n>(({ children, className, icon, type = 'button', ...props }, ref) => (\n <button\n ref={ref}\n type={type}\n className={cn(\n 'inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-gray text-sm font-medium text-gray-light transition-colors hover:border-spark-a8 hover:bg-base-accent-soft hover:text-accent focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {icon ?? <MaterialSymbol className=\"text-[14px]\" name=\"add\" />}\n {children}\n </button>\n))\n\nScheduleListAdd.displayName = 'ScheduleListAdd'\n\ninterface ScheduleEntryProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Make the row interactive — enables hover, role=button, keyboard activation. */\n onClick?: () => void\n disabled?: boolean\n}\n\nconst ScheduleEntry = React.forwardRef<HTMLDivElement, ScheduleEntryProps>(\n ({ children, className, disabled, onClick, ...props }, ref) => {\n const interactive = onClick !== undefined && !disabled\n // role, tab stop and key handler travel together -- the row is never\n // clickable without Enter/Space, and never focusable without a role\n const interaction = interactive\n ? {\n role: 'button' as const,\n tabIndex: 0,\n onClick,\n onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onClick()\n }\n },\n }\n : undefined\n return (\n <div\n ref={ref}\n {...interaction}\n aria-disabled={disabled || undefined}\n className={cn(\n 'flex items-center gap-3 rounded-lg bg-base-gray-soft px-3 py-2.5 transition-colors',\n interactive &&\n 'cursor-pointer hover:bg-gray-a3 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden',\n disabled && 'cursor-not-allowed opacity-50',\n className,\n )}\n {...props}\n >\n {children}\n </div>\n )\n },\n)\n\nScheduleEntry.displayName = 'ScheduleEntry'\n\nconst ScheduleEntryContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex min-w-0 flex-1 flex-col gap-0.5', className)}\n {...props}\n />\n))\n\nScheduleEntryContent.displayName = 'ScheduleEntryContent'\n\nconst ScheduleEntryTitle = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n 'flex min-w-0 items-center gap-2 truncate text-sm font-medium text-gray',\n className,\n )}\n {...props}\n />\n))\n\nScheduleEntryTitle.displayName = 'ScheduleEntryTitle'\n\nconst ScheduleEntrySubtitle = React.forwardRef<\n HTMLSpanElement,\n React.HTMLAttributes<HTMLSpanElement>\n>(({ className, ...props }, ref) => (\n <span\n ref={ref}\n className={cn('truncate text-xs text-gray-light', className)}\n {...props}\n />\n))\n\nScheduleEntrySubtitle.displayName = 'ScheduleEntrySubtitle'\n\n/** Right-aligned action area. Stops click propagation so inner controls\n * don't fire the parent `ScheduleEntry`'s `onClick`. */\nconst ScheduleEntryActions = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, onClick, ...props }, ref) => (\n <div\n ref={ref}\n // a layout wrapper, not a control: the handler only shields the inner\n // buttons' clicks (keyboard activation included) from the parent row\n role=\"presentation\"\n onClick={(event) => {\n event.stopPropagation()\n onClick?.(event)\n }}\n className={cn('flex shrink-0 items-center gap-1.5', className)}\n {...props}\n />\n))\n\nScheduleEntryActions.displayName = 'ScheduleEntryActions'\n\ninterface ScheduleEntryDeleteProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}\n\nconst ScheduleEntryDelete = React.forwardRef<\n HTMLButtonElement,\n ScheduleEntryDeleteProps\n>(\n (\n {\n 'aria-label': ariaLabel = 'Delete schedule entry',\n children,\n className,\n onClick,\n type = 'button',\n ...props\n },\n ref,\n ) => (\n <button\n ref={ref}\n type={type}\n aria-label={ariaLabel}\n onClick={(event) => {\n event.stopPropagation()\n onClick?.(event)\n }}\n className={cn(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-full text-gray-light transition-colors hover:bg-base-error-soft hover:text-error focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n >\n {children ?? <MaterialSymbol className=\"text-[14px]\" name=\"delete\" />}\n </button>\n ),\n)\n\nScheduleEntryDelete.displayName = 'ScheduleEntryDelete'\n\nexport {\n ScheduleEntry,\n ScheduleEntryActions,\n ScheduleEntryContent,\n ScheduleEntryDelete,\n ScheduleEntrySubtitle,\n ScheduleEntryTitle,\n ScheduleList,\n ScheduleListAdd,\n ScheduleListEmpty,\n}\n"],"mappings":";;;;;;AAIA,MAAM,eAAeA,QAAM,YAGxB,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,OAAD;CACO;CACL,WAAW,GACT,uDACA,SACF;CACA,GAAI;AACL,CAAA,CACF;AAED,aAAa,cAAc;AAE3B,MAAM,oBAAoBA,QAAM,YAG7B,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,KAAD;CACO;CACL,WAAW,GAAG,qCAAqC,SAAS;CAC5D,GAAI;AACL,CAAA,CACF;AAED,kBAAkB,cAAc;AAOhC,MAAM,kBAAkBA,QAAM,YAG3B,EAAE,UAAU,WAAW,MAAM,OAAO,UAAU,GAAG,SAAS,QAC3D,qBAAC,UAAD;CACO;CACC;CACN,WAAW,GACT,iWACA,SACF;CACA,GAAI;WAPN,CASG,QAAQ,oBAAC,gBAAD;EAAgB,WAAU;EAAc,MAAK;CAAO,CAAA,GAC5D,QACK;EACT;AAED,gBAAgB,cAAc;AAQ9B,MAAM,gBAAgBA,QAAM,YACzB,EAAE,UAAU,WAAW,UAAU,SAAS,GAAG,SAAS,QAAQ;CAC7D,MAAM,cAAc,YAAY,KAAA,KAAa,CAAC;CAgB9C,OACE,oBAAC,OAAD;EACO;EACL,GAhBgB,cAChB;GACE,MAAM;GACN,UAAU;GACV;GACA,YAAY,UAA+C;IACzD,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;KAC9C,MAAM,eAAe;KACrB,QAAQ;IACV;GACF;EACF,IACA,KAAA;EAKA,iBAAe,YAAY,KAAA;EAC3B,WAAW,GACT,sFACA,eACE,iHACF,YAAY,iCACZ,SACF;EACA,GAAI;EAEH;CACE,CAAA;AAET,CACF;AAEA,cAAc,cAAc;AAE5B,MAAM,uBAAuBA,QAAM,YAGhC,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,OAAD;CACO;CACL,WAAW,GAAG,wCAAwC,SAAS;CAC/D,GAAI;AACL,CAAA,CACF;AAED,qBAAqB,cAAc;AAEnC,MAAM,qBAAqBA,QAAM,YAG9B,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,OAAD;CACO;CACL,WAAW,GACT,0EACA,SACF;CACA,GAAI;AACL,CAAA,CACF;AAED,mBAAmB,cAAc;AAEjC,MAAM,wBAAwBA,QAAM,YAGjC,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,QAAD;CACO;CACL,WAAW,GAAG,oCAAoC,SAAS;CAC3D,GAAI;AACL,CAAA,CACF;AAED,sBAAsB,cAAc;;;AAIpC,MAAM,uBAAuBA,QAAM,YAGhC,EAAE,WAAW,SAAS,GAAG,SAAS,QACnC,oBAAC,OAAD;CACO;CAGL,MAAK;CACL,UAAU,UAAU;EAClB,MAAM,gBAAgB;EACtB,UAAU,KAAK;CACjB;CACA,WAAW,GAAG,sCAAsC,SAAS;CAC7D,GAAI;AACL,CAAA,CACF;AAED,qBAAqB,cAAc;AAInC,MAAM,sBAAsBA,QAAM,YAK9B,EACE,cAAc,YAAY,yBAC1B,UACA,WACA,SACA,OAAO,UACP,GAAG,SAEL,QAEA,oBAAC,UAAD;CACO;CACC;CACN,cAAY;CACZ,UAAU,UAAU;EAClB,MAAM,gBAAgB;EACtB,UAAU,KAAK;CACjB;CACA,WAAW,GACT,kRACA,SACF;CACA,GAAI;WAEH,YAAY,oBAAC,gBAAD;EAAgB,WAAU;EAAc,MAAK;CAAU,CAAA;AAC9D,CAAA,CAEZ;AAEA,oBAAoB,cAAc"}