@intlayer/design-system 9.3.3 → 9.4.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/esm/api/hooks/analytics.mjs +6 -5
- package/dist/esm/api/hooks/analytics.mjs.map +1 -1
- package/dist/esm/components/Accordion/Accordion.mjs +2 -2
- package/dist/esm/components/Accordion/Accordion.mjs.map +1 -1
- package/dist/esm/components/ExpandCollapse/ExpandCollapse.mjs +1 -1
- package/dist/esm/components/ExpandCollapse/ExpandCollapse.mjs.map +1 -1
- package/dist/esm/components/Form/elements/OTPElement.mjs +4 -4
- package/dist/esm/components/Form/elements/OTPElement.mjs.map +1 -1
- package/dist/esm/components/Link/Link.mjs +10 -5
- package/dist/esm/components/Link/Link.mjs.map +1 -1
- package/dist/esm/components/LocaleSwitcherContentDropDown/LocaleSwitcherContent.mjs +1 -1
- package/dist/esm/components/Modal/Modal.mjs +2 -2
- package/dist/esm/components/Navbar/MobileNavbar.mjs +1 -1
- package/dist/esm/components/Pagination/Pagination.mjs +4 -4
- package/dist/esm/components/Pagination/Pagination.mjs.map +1 -1
- package/dist/esm/components/Pattern/GridDistortionPattern.mjs +3 -2
- package/dist/esm/components/Pattern/GridDistortionPattern.mjs.map +1 -1
- package/dist/esm/components/Popover/dynamic.mjs +68 -24
- package/dist/esm/components/Popover/dynamic.mjs.map +1 -1
- package/dist/esm/components/RightDrawer/RightDrawer.mjs +5 -4
- package/dist/esm/components/RightDrawer/RightDrawer.mjs.map +1 -1
- package/dist/esm/components/SwitchSelector/useSwitchSelector.mjs +2 -2
- package/dist/esm/components/SwitchSelector/useSwitchSelector.mjs.map +1 -1
- package/dist/esm/components/Tab/Tab.mjs +1 -1
- package/dist/esm/components/TabSelector/TabSelector.mjs +2 -2
- package/dist/esm/components/TabSelector/TabSelector.mjs.map +1 -1
- package/dist/esm/components/ThemeSwitcherDropDown/DesktopThemeSwitcher.mjs +10 -18
- package/dist/esm/components/ThemeSwitcherDropDown/DesktopThemeSwitcher.mjs.map +1 -1
- package/dist/esm/components/ThemeSwitcherDropDown/MobileThemeSwitcher.mjs +14 -13
- package/dist/esm/components/ThemeSwitcherDropDown/MobileThemeSwitcher.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +9 -9
- package/dist/esm/hooks/useItemSelector.mjs +12 -9
- package/dist/esm/hooks/useItemSelector.mjs.map +1 -1
- package/dist/esm/hooks/useScrollY.mjs +64 -45
- package/dist/esm/hooks/useScrollY.mjs.map +1 -1
- package/dist/esm/hooks/useStickToBottom.mjs +69 -0
- package/dist/esm/hooks/useStickToBottom.mjs.map +1 -0
- package/dist/esm/providers/ThemeProvider.mjs +143 -0
- package/dist/esm/providers/ThemeProvider.mjs.map +1 -0
- package/dist/esm/providers/index.mjs +2 -1
- package/dist/esm/routes.mjs +2 -1
- package/dist/esm/routes.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +3 -1
- package/dist/esm/utils/observeScrollExtent.mjs +39 -0
- package/dist/esm/utils/observeScrollExtent.mjs.map +1 -0
- package/dist/esm/utils/scheduleFrameTask.mjs +44 -0
- package/dist/esm/utils/scheduleFrameTask.mjs.map +1 -0
- package/dist/types/api/hooks/analytics.d.ts +5 -3
- package/dist/types/api/hooks/analytics.d.ts.map +1 -1
- package/dist/types/components/Popover/dynamic.d.ts.map +1 -1
- package/dist/types/components/ThemeSwitcherDropDown/DesktopThemeSwitcher.d.ts +1 -1
- package/dist/types/components/ThemeSwitcherDropDown/DesktopThemeSwitcher.d.ts.map +1 -1
- package/dist/types/components/ThemeSwitcherDropDown/MobileThemeSwitcher.d.ts +1 -1
- package/dist/types/components/ThemeSwitcherDropDown/MobileThemeSwitcher.d.ts.map +1 -1
- package/dist/types/components/ThemeSwitcherDropDown/types.d.ts +3 -1
- package/dist/types/components/ThemeSwitcherDropDown/types.d.ts.map +1 -1
- package/dist/types/hooks/index.d.ts +2 -2
- package/dist/types/hooks/useItemSelector.d.ts +2 -0
- package/dist/types/hooks/useItemSelector.d.ts.map +1 -1
- package/dist/types/hooks/useScrollY.d.ts +10 -0
- package/dist/types/hooks/useScrollY.d.ts.map +1 -1
- package/dist/types/hooks/useStickToBottom.d.ts +41 -0
- package/dist/types/hooks/useStickToBottom.d.ts.map +1 -0
- package/dist/types/providers/ThemeProvider.d.ts +41 -0
- package/dist/types/providers/ThemeProvider.d.ts.map +1 -0
- package/dist/types/providers/index.d.ts +2 -1
- package/dist/types/routes.d.ts +2 -1
- package/dist/types/routes.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +3 -1
- package/dist/types/utils/observeScrollExtent.d.ts +22 -0
- package/dist/types/utils/observeScrollExtent.d.ts.map +1 -0
- package/dist/types/utils/scheduleFrameTask.d.ts +25 -0
- package/dist/types/utils/scheduleFrameTask.d.ts.map +1 -0
- package/package.json +18 -18
- package/dist/esm/hooks/useIsDarkMode.mjs +0 -17
- package/dist/esm/hooks/useIsDarkMode.mjs.map +0 -1
- package/dist/types/hooks/useIsDarkMode.d.ts +0 -5
- package/dist/types/hooks/useIsDarkMode.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GridDistortionPattern.mjs","names":[],"sources":["../../../../src/components/Pattern/GridDistortionPattern.tsx"],"sourcesContent":["'use client';\n\nimport { cn } from '@utils/cn';\nimport { type FC, type SVGProps, useEffect, useRef } from 'react';\nimport { useIsDarkMode } from '../../hooks/useIsDarkMode';\n\n// ---------------------------------------------------------------------------\n// Theme configuration — tweak these to adjust the grid appearance globally\n// ---------------------------------------------------------------------------\n\ntype GridTheme = {\n /** Resting line color, used far from the pointer. */\n lineColor: string;\n /**\n * Color the lines fade toward as the pointer approaches. To avoid lines\n * appearing to \"dim into the background\" near the pointer, this should be\n * brighter (higher luminance) than {@link GridTheme.lineColor} in dark mode\n * and clearly contrasting in light mode.\n */\n highlightColor: string;\n /** Stroke opacity far from the pointer — keeps the whole grid visible. */\n baseOpacity: number;\n /** Extra opacity added at the pointer's center on top of {@link GridTheme.baseOpacity}. */\n highlightOpacityBoost: number;\n};\n\n/**\n * Light mode: a visible (but subtle) gray line on white. The base opacity is\n * raised and the highlight boost lowered so the grid no longer fades to nothing\n * far from the pointer while spiking to a harsh accent underneath it.\n */\nconst LIGHT_THEME: GridTheme = {\n lineColor: '#a8a8a8',\n highlightColor: '#969696',\n baseOpacity: 0.3,\n highlightOpacityBoost: 0.3,\n};\n\n/**\n * Dark mode: a mid-gray line that brightens toward a light purple near the\n * pointer (brighter than the resting line, so the hovered segment lights up\n * instead of darkening into the black background).\n */\nconst DARK_THEME: GridTheme = {\n lineColor: '#4d4d4d',\n highlightColor: '#5c5c5c',\n baseOpacity: 0.2,\n highlightOpacityBoost: 0.2,\n};\n\n/**\n * Falloff exponent for the highlight. A gentler curve (2 = quadratic) spreads\n * the highlight over a wider area than a cubic one, smoothing the transition\n * between the resting line and the accent under the pointer.\n */\nconst HIGHLIGHT_FALLOFF_EXPONENT = 2;\n\n// ---------------------------------------------------------------------------\n// Pure helpers — defined once at module level, never recreated\n// ---------------------------------------------------------------------------\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const numericValue = parseInt(hex.replace('#', ''), 16);\n return [\n (numericValue >> 16) & 255,\n (numericValue >> 8) & 255,\n numericValue & 255,\n ];\n};\n\n/** Neutral gray used when a color string can't be parsed into RGB. */\nconst FALLBACK_RGB: [number, number, number] = [136, 136, 136];\n\n/**\n * Resolves any CSS color string — `#rrggbb`, `rgb()/hsl()`, a named color, or a\n * `var(--token)` reference — into an `[r, g, b]` triple.\n *\n * 6-digit hex is parsed directly. Everything else is resolved by the browser\n * via a throwaway probe element mounted inside `contextElement`, so custom\n * properties resolve against the correct cascade. Falls back to a neutral gray\n * when the value can't be parsed, so the grid never emits invalid\n * `rgb(NaN, …)` strokes (which browsers render as solid black).\n */\nconst resolveColorToRgb = (\n color: string,\n contextElement: Element\n): [number, number, number] => {\n const trimmed = color.trim();\n\n // Fast path: 6-digit hex needs no DOM access.\n if (/^#[0-9a-f]{6}$/i.test(trimmed)) {\n return hexToRgb(trimmed);\n }\n\n const probe = document.createElement('span');\n probe.style.color = trimmed;\n probe.style.display = 'none';\n contextElement.appendChild(probe);\n const computedColor = getComputedStyle(probe).color;\n probe.remove();\n\n const channels = computedColor.match(/-?\\d+(?:\\.\\d+)?/g);\n if (channels && channels.length >= 3) {\n return [Number(channels[0]), Number(channels[1]), Number(channels[2])];\n }\n\n return FALLBACK_RGB;\n};\n\ntype Point = { baseX: number; baseY: number; x: number; y: number };\n\nconst buildSegmentAttrs = (\n startX: number,\n startY: number,\n endX: number,\n endY: number,\n mouseX: number,\n mouseY: number,\n mouseActive: boolean,\n radius: number,\n strength: number,\n lineRgb: [number, number, number],\n highlightRgb: [number, number, number],\n baseOpacity: number,\n highlightOpacityBoost: number\n): {\n d: string;\n stroke: string;\n strokeOpacity: number;\n strokeWidth: number;\n} => {\n let red: number;\n let green: number;\n let blue: number;\n let opacity: number;\n let strokeWidth: number;\n\n if (!mouseActive) {\n [red, green, blue] = lineRgb;\n opacity = baseOpacity;\n strokeWidth = 0.8;\n } else {\n const midX = (startX + endX) / 2;\n const midY = (startY + endY) / 2;\n const deltaX = mouseX - midX;\n const deltaY = mouseY - midY;\n const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);\n const distanceRatio = Math.max(0, 1 - distance / radius);\n const easing = distanceRatio ** HIGHLIGHT_FALLOFF_EXPONENT;\n red = Math.round(lineRgb[0] + (highlightRgb[0] - lineRgb[0]) * easing);\n green = Math.round(lineRgb[1] + (highlightRgb[1] - lineRgb[1]) * easing);\n blue = Math.round(lineRgb[2] + (highlightRgb[2] - lineRgb[2]) * easing);\n opacity = baseOpacity + easing * highlightOpacityBoost;\n strokeWidth = 0.8 + easing * 1.2;\n void strength; // currently unused in segment coloring, kept for API compatibility\n }\n\n return {\n d: `M ${startX} ${startY} L ${endX} ${endY}`,\n stroke: `rgb(${red},${green},${blue})`,\n strokeOpacity: opacity,\n strokeWidth,\n };\n};\n\n/**\n * Advances every point one step toward its target and reports how far the\n * furthest one travelled.\n *\n * The caller uses that distance to decide whether another frame is worth\n * running: the relaxation is exponential, so once it drops below\n * {@link SETTLE_THRESHOLD_PX} the grid is visually at rest and redrawing it\n * only burns the main thread.\n *\n * @returns Largest per-axis distance any point moved this step, in pixels.\n */\nconst updateGridPoints = (\n grid: Point[][],\n mouseX: number,\n mouseY: number,\n mouseActive: boolean,\n radius: number,\n strength: number\n): number => {\n const rows = grid.length;\n const columns = grid[0]!.length;\n let largestMovement = 0;\n\n for (let rowIndex = 0; rowIndex < rows; rowIndex++) {\n for (let columnIndex = 0; columnIndex < columns; columnIndex++) {\n const point = grid[rowIndex]![columnIndex]!;\n const previousX = point.x;\n const previousY = point.y;\n\n if (!mouseActive) {\n point.x += (point.baseX - point.x) * 0.08;\n point.y += (point.baseY - point.y) * 0.08;\n } else {\n const deltaX = mouseX - point.baseX;\n const deltaY = mouseY - point.baseY;\n const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);\n\n if (distance < radius && distance > 0) {\n const force = (Math.cos((distance / radius) * Math.PI) + 1) / 2;\n const displacement = force * strength * radius * 0.3;\n point.x +=\n (point.baseX + (deltaX / distance) * displacement - point.x) * 0.12;\n point.y +=\n (point.baseY + (deltaY / distance) * displacement - point.y) * 0.12;\n } else {\n point.x += (point.baseX - point.x) * 0.08;\n point.y += (point.baseY - point.y) * 0.08;\n }\n }\n\n const movement = Math.max(\n Math.abs(point.x - previousX),\n Math.abs(point.y - previousY)\n );\n if (movement > largestMovement) largestMovement = movement;\n }\n }\n\n return largestMovement;\n};\n\n/**\n * Per-frame movement below which the grid counts as at rest, in pixels.\n *\n * Well under a device pixel, so the frame that crosses it is the last one that\n * could have changed anything on screen.\n */\nconst SETTLE_THRESHOLD_PX = 0.05;\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\ntype GridDistortionPatternProps = {\n width?: number;\n height?: number;\n radius?: number;\n strength?: number;\n lineColor?: string;\n highlightColor?: string;\n} & SVGProps<SVGSVGElement>;\n\nexport const GridDistortionPattern: FC<GridDistortionPatternProps> = ({\n width = 70,\n height = 70,\n radius = 280,\n strength = 0.12,\n lineColor,\n highlightColor,\n className,\n ...props\n}) => {\n const isDarkMode = useIsDarkMode();\n const theme = isDarkMode ? DARK_THEME : LIGHT_THEME;\n\n // Explicit props win; otherwise fall back to the active theme's defaults.\n const resolvedLineColor = lineColor ?? theme.lineColor;\n const resolvedHighlightColor = highlightColor ?? theme.highlightColor;\n\n const svgRef = useRef<SVGSVGElement>(null);\n const mouse = useRef({ x: -9999, y: -9999, active: false });\n const points = useRef<Point[][]>([]);\n /**\n * Bumped on every pointer move. The loop records the value it rendered so it\n * can tell \"the pointer is somewhere over the page but has not moved\" — which\n * looks identical frame to frame — from a genuine change.\n */\n const pointerMoveCount = useRef(0);\n /**\n * Restarts the animation loop after it has parked itself.\n *\n * Assigned by the animation effect; the resize, colour and pointer effects\n * call it so a change they make is picked up even when the grid was at rest.\n */\n const wakeLoop = useRef<() => void>(() => undefined);\n\n // Stable refs so the animation loop always sees the latest prop values\n // without needing to restart itself.\n const radiusRef = useRef(radius);\n const strengthRef = useRef(strength);\n // Initialized to safe placeholders; the real (possibly CSS-variable-based)\n // colors are resolved against the DOM in the effect below.\n const lineRgbRef = useRef<[number, number, number]>(FALLBACK_RGB);\n const highlightRgbRef = useRef<[number, number, number]>(FALLBACK_RGB);\n const baseOpacityRef = useRef(theme.baseOpacity);\n const highlightOpacityBoostRef = useRef(theme.highlightOpacityBoost);\n\n // Keep numeric refs in sync with props on each render (cheap assignments).\n radiusRef.current = radius;\n strengthRef.current = strength;\n baseOpacityRef.current = theme.baseOpacity;\n highlightOpacityBoostRef.current = theme.highlightOpacityBoost;\n\n // Resolve colors once mounted — getComputedStyle is required so that\n // `var(--token)` highlight/line colors resolve against the live cascade.\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n lineRgbRef.current = resolveColorToRgb(resolvedLineColor, svg);\n highlightRgbRef.current = resolveColorToRgb(resolvedHighlightColor, svg);\n wakeLoop.current();\n }, [resolvedLineColor, resolvedHighlightColor]);\n\n // ---------------------------------------------------------------------------\n // Resize — rebuild the point grid and resize the SVG viewBox\n // ---------------------------------------------------------------------------\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n\n const resize = () => {\n const rect = svg.getBoundingClientRect();\n svg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`);\n\n const columns = Math.ceil(rect.width / width) + 2;\n const rows = Math.ceil(rect.height / height) + 2;\n const newGrid: Point[][] = [];\n\n for (let rowIndex = -1; rowIndex <= rows; rowIndex++) {\n const row: Point[] = [];\n for (let columnIndex = -1; columnIndex <= columns; columnIndex++) {\n row.push({\n baseX: columnIndex * width,\n baseY: rowIndex * height,\n x: columnIndex * width,\n y: rowIndex * height,\n });\n }\n newGrid.push(row);\n }\n points.current = newGrid;\n wakeLoop.current();\n };\n\n resize();\n const resizeObserver = new ResizeObserver(resize);\n resizeObserver.observe(svg);\n return () => resizeObserver.disconnect();\n }, [width, height]);\n\n // ---------------------------------------------------------------------------\n // Pointer tracking — passive listeners, no React state involved\n // ---------------------------------------------------------------------------\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = svg.getBoundingClientRect();\n mouse.current = {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n active: true,\n };\n pointerMoveCount.current++;\n wakeLoop.current();\n };\n const handlePointerLeave = () => {\n mouse.current.active = false;\n pointerMoveCount.current++;\n wakeLoop.current();\n };\n\n window.addEventListener('pointermove', handlePointerMove, {\n passive: true,\n });\n window.addEventListener('pointerleave', handlePointerLeave, {\n passive: true,\n });\n return () => {\n window.removeEventListener('pointermove', handlePointerMove);\n window.removeEventListener('pointerleave', handlePointerLeave);\n };\n }, []);\n\n // ---------------------------------------------------------------------------\n // Animation loop — writes directly to SVG DOM, no React state/re-renders\n // ---------------------------------------------------------------------------\n /**\n * Runs the grid only while it has something to show.\n *\n * Each frame rewrites four attributes on every segment — on a full-viewport\n * grid that is close to nine thousand `setAttribute` calls. Left running\n * unconditionally it saturated the main thread of any page embedding the\n * pattern, which starved React's transition lanes and made every other\n * scroll-driven animation on the page advance in visible steps.\n *\n * So the loop parks itself whenever the grid is at rest and the pointer has\n * not moved, and while the SVG is scrolled out of view. Anything that can\n * change the picture — a pointer move, a resize, a colour change, the element\n * coming back on screen — wakes it through {@link wakeLoop}.\n */\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n\n // We maintain a pool of <path> elements and reuse them each frame\n // to avoid GC pressure from creating/deleting DOM nodes every tick.\n const pathPool: SVGPathElement[] = [];\n let frame = 0;\n let isRunning = false;\n let isOnScreen = true;\n /** Value of `pointerMoveCount` the last rendered frame was drawn for. */\n let renderedPointerMoveCount = -1;\n\n const start = () => {\n if (isRunning || !isOnScreen) return;\n isRunning = true;\n frame = requestAnimationFrame(tick);\n };\n\n const stop = () => {\n if (!isRunning) return;\n isRunning = false;\n cancelAnimationFrame(frame);\n };\n\n const ensurePathCount = (count: number) => {\n while (pathPool.length < count) {\n const el = document.createElementNS(\n 'http://www.w3.org/2000/svg',\n 'path'\n );\n el.setAttribute('fill', 'none');\n svg.appendChild(el);\n pathPool.push(el);\n }\n // Hide surplus paths\n for (let i = count; i < pathPool.length; i++) {\n pathPool[i]!.setAttribute('d', '');\n }\n };\n\n const tick = () => {\n const grid = points.current;\n if (!grid.length) {\n // No grid yet — the resize effect will wake us once it has built one.\n isRunning = false;\n return;\n }\n\n const pointerMoveCountAtFrameStart = pointerMoveCount.current;\n\n const { x: mouseX, y: mouseY, active: mouseActive } = mouse.current;\n const currentRadius = radiusRef.current;\n const currentStrength = strengthRef.current;\n const lineRgb = lineRgbRef.current;\n const highlightRgb = highlightRgbRef.current;\n const baseOpacity = baseOpacityRef.current;\n const highlightOpacityBoost = highlightOpacityBoostRef.current;\n\n // Update point positions\n const largestMovement = updateGridPoints(\n grid,\n mouseX,\n mouseY,\n mouseActive,\n currentRadius,\n currentStrength\n );\n\n // Nothing moved and the pointer is where it already was, so this frame\n // would repaint the picture already on screen.\n if (\n largestMovement < SETTLE_THRESHOLD_PX &&\n renderedPointerMoveCount === pointerMoveCountAtFrameStart\n ) {\n isRunning = false;\n return;\n }\n renderedPointerMoveCount = pointerMoveCountAtFrameStart;\n\n const rows = grid.length;\n const columns = grid[0]!.length;\n\n // Count segments up front so we can size the pool\n const horizontalCount = rows * (columns - 1);\n const verticalCount = columns * (rows - 1);\n const totalSegments = horizontalCount + verticalCount;\n ensurePathCount(totalSegments);\n\n let pathIndex = 0;\n\n // Horizontal segments\n for (let rowIndex = 0; rowIndex < rows; rowIndex++) {\n for (let columnIndex = 0; columnIndex < columns - 1; columnIndex++) {\n const p0 = grid[rowIndex]![columnIndex]!;\n const p1 = grid[rowIndex]![columnIndex + 1]!;\n const attrs = buildSegmentAttrs(\n p0.x,\n p0.y,\n p1.x,\n p1.y,\n mouseX,\n mouseY,\n mouseActive,\n currentRadius,\n currentStrength,\n lineRgb,\n highlightRgb,\n baseOpacity,\n highlightOpacityBoost\n );\n const el = pathPool[pathIndex++]!;\n el.setAttribute('d', attrs.d);\n el.setAttribute('stroke', attrs.stroke);\n el.setAttribute('stroke-opacity', String(attrs.strokeOpacity));\n el.setAttribute('stroke-width', String(attrs.strokeWidth));\n }\n }\n\n // Vertical segments\n for (let columnIndex = 0; columnIndex < columns; columnIndex++) {\n for (let rowIndex = 0; rowIndex < rows - 1; rowIndex++) {\n const p0 = grid[rowIndex]![columnIndex]!;\n const p1 = grid[rowIndex + 1]![columnIndex]!;\n const attrs = buildSegmentAttrs(\n p0.x,\n p0.y,\n p1.x,\n p1.y,\n mouseX,\n mouseY,\n mouseActive,\n currentRadius,\n currentStrength,\n lineRgb,\n highlightRgb,\n baseOpacity,\n highlightOpacityBoost\n );\n const el = pathPool[pathIndex++]!;\n el.setAttribute('d', attrs.d);\n el.setAttribute('stroke', attrs.stroke);\n el.setAttribute('stroke-opacity', String(attrs.strokeOpacity));\n el.setAttribute('stroke-width', String(attrs.strokeWidth));\n }\n }\n\n frame = requestAnimationFrame(tick);\n };\n\n wakeLoop.current = start;\n\n // Off-screen the grid cannot be seen, so neither the relaxation nor the\n // redraw is worth a frame.\n const visibilityObserver = new IntersectionObserver(([entry]) => {\n isOnScreen = entry?.isIntersecting ?? true;\n if (isOnScreen) start();\n else stop();\n });\n visibilityObserver.observe(svg);\n\n start();\n\n return () => {\n wakeLoop.current = () => undefined;\n visibilityObserver.disconnect();\n stop();\n // Clean up path elements we injected\n for (const el of pathPool) {\n el.remove();\n }\n };\n }, []); // no dependencies — reads everything via refs\n\n return (\n <svg\n ref={svgRef}\n aria-hidden=\"true\"\n className={cn(\n 'pointer-events-none absolute inset-0 size-full',\n className\n )}\n {...props}\n />\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GridDistortionPattern.mjs","names":[],"sources":["../../../../src/components/Pattern/GridDistortionPattern.tsx"],"sourcesContent":["'use client';\n\nimport { useTheme } from '@providers/ThemeProvider';\nimport { cn } from '@utils/cn';\nimport { type FC, type SVGProps, useEffect, useRef } from 'react';\n\n// ---------------------------------------------------------------------------\n// Theme configuration — tweak these to adjust the grid appearance globally\n// ---------------------------------------------------------------------------\n\ntype GridTheme = {\n /** Resting line color, used far from the pointer. */\n lineColor: string;\n /**\n * Color the lines fade toward as the pointer approaches. To avoid lines\n * appearing to \"dim into the background\" near the pointer, this should be\n * brighter (higher luminance) than {@link GridTheme.lineColor} in dark mode\n * and clearly contrasting in light mode.\n */\n highlightColor: string;\n /** Stroke opacity far from the pointer — keeps the whole grid visible. */\n baseOpacity: number;\n /** Extra opacity added at the pointer's center on top of {@link GridTheme.baseOpacity}. */\n highlightOpacityBoost: number;\n};\n\n/**\n * Light mode: a visible (but subtle) gray line on white. The base opacity is\n * raised and the highlight boost lowered so the grid no longer fades to nothing\n * far from the pointer while spiking to a harsh accent underneath it.\n */\nconst LIGHT_THEME: GridTheme = {\n lineColor: '#a8a8a8',\n highlightColor: '#969696',\n baseOpacity: 0.3,\n highlightOpacityBoost: 0.3,\n};\n\n/**\n * Dark mode: a mid-gray line that brightens toward a light purple near the\n * pointer (brighter than the resting line, so the hovered segment lights up\n * instead of darkening into the black background).\n */\nconst DARK_THEME: GridTheme = {\n lineColor: '#4d4d4d',\n highlightColor: '#5c5c5c',\n baseOpacity: 0.2,\n highlightOpacityBoost: 0.2,\n};\n\n/**\n * Falloff exponent for the highlight. A gentler curve (2 = quadratic) spreads\n * the highlight over a wider area than a cubic one, smoothing the transition\n * between the resting line and the accent under the pointer.\n */\nconst HIGHLIGHT_FALLOFF_EXPONENT = 2;\n\n// ---------------------------------------------------------------------------\n// Pure helpers — defined once at module level, never recreated\n// ---------------------------------------------------------------------------\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const numericValue = parseInt(hex.replace('#', ''), 16);\n return [\n (numericValue >> 16) & 255,\n (numericValue >> 8) & 255,\n numericValue & 255,\n ];\n};\n\n/** Neutral gray used when a color string can't be parsed into RGB. */\nconst FALLBACK_RGB: [number, number, number] = [136, 136, 136];\n\n/**\n * Resolves any CSS color string — `#rrggbb`, `rgb()/hsl()`, a named color, or a\n * `var(--token)` reference — into an `[r, g, b]` triple.\n *\n * 6-digit hex is parsed directly. Everything else is resolved by the browser\n * via a throwaway probe element mounted inside `contextElement`, so custom\n * properties resolve against the correct cascade. Falls back to a neutral gray\n * when the value can't be parsed, so the grid never emits invalid\n * `rgb(NaN, …)` strokes (which browsers render as solid black).\n */\nconst resolveColorToRgb = (\n color: string,\n contextElement: Element\n): [number, number, number] => {\n const trimmed = color.trim();\n\n // Fast path: 6-digit hex needs no DOM access.\n if (/^#[0-9a-f]{6}$/i.test(trimmed)) {\n return hexToRgb(trimmed);\n }\n\n const probe = document.createElement('span');\n probe.style.color = trimmed;\n probe.style.display = 'none';\n contextElement.appendChild(probe);\n const computedColor = getComputedStyle(probe).color;\n probe.remove();\n\n const channels = computedColor.match(/-?\\d+(?:\\.\\d+)?/g);\n if (channels && channels.length >= 3) {\n return [Number(channels[0]), Number(channels[1]), Number(channels[2])];\n }\n\n return FALLBACK_RGB;\n};\n\ntype Point = { baseX: number; baseY: number; x: number; y: number };\n\nconst buildSegmentAttrs = (\n startX: number,\n startY: number,\n endX: number,\n endY: number,\n mouseX: number,\n mouseY: number,\n mouseActive: boolean,\n radius: number,\n strength: number,\n lineRgb: [number, number, number],\n highlightRgb: [number, number, number],\n baseOpacity: number,\n highlightOpacityBoost: number\n): {\n d: string;\n stroke: string;\n strokeOpacity: number;\n strokeWidth: number;\n} => {\n let red: number;\n let green: number;\n let blue: number;\n let opacity: number;\n let strokeWidth: number;\n\n if (!mouseActive) {\n [red, green, blue] = lineRgb;\n opacity = baseOpacity;\n strokeWidth = 0.8;\n } else {\n const midX = (startX + endX) / 2;\n const midY = (startY + endY) / 2;\n const deltaX = mouseX - midX;\n const deltaY = mouseY - midY;\n const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);\n const distanceRatio = Math.max(0, 1 - distance / radius);\n const easing = distanceRatio ** HIGHLIGHT_FALLOFF_EXPONENT;\n red = Math.round(lineRgb[0] + (highlightRgb[0] - lineRgb[0]) * easing);\n green = Math.round(lineRgb[1] + (highlightRgb[1] - lineRgb[1]) * easing);\n blue = Math.round(lineRgb[2] + (highlightRgb[2] - lineRgb[2]) * easing);\n opacity = baseOpacity + easing * highlightOpacityBoost;\n strokeWidth = 0.8 + easing * 1.2;\n void strength; // currently unused in segment coloring, kept for API compatibility\n }\n\n return {\n d: `M ${startX} ${startY} L ${endX} ${endY}`,\n stroke: `rgb(${red},${green},${blue})`,\n strokeOpacity: opacity,\n strokeWidth,\n };\n};\n\n/**\n * Advances every point one step toward its target and reports how far the\n * furthest one travelled.\n *\n * The caller uses that distance to decide whether another frame is worth\n * running: the relaxation is exponential, so once it drops below\n * {@link SETTLE_THRESHOLD_PX} the grid is visually at rest and redrawing it\n * only burns the main thread.\n *\n * @returns Largest per-axis distance any point moved this step, in pixels.\n */\nconst updateGridPoints = (\n grid: Point[][],\n mouseX: number,\n mouseY: number,\n mouseActive: boolean,\n radius: number,\n strength: number\n): number => {\n const rows = grid.length;\n const columns = grid[0]!.length;\n let largestMovement = 0;\n\n for (let rowIndex = 0; rowIndex < rows; rowIndex++) {\n for (let columnIndex = 0; columnIndex < columns; columnIndex++) {\n const point = grid[rowIndex]![columnIndex]!;\n const previousX = point.x;\n const previousY = point.y;\n\n if (!mouseActive) {\n point.x += (point.baseX - point.x) * 0.08;\n point.y += (point.baseY - point.y) * 0.08;\n } else {\n const deltaX = mouseX - point.baseX;\n const deltaY = mouseY - point.baseY;\n const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);\n\n if (distance < radius && distance > 0) {\n const force = (Math.cos((distance / radius) * Math.PI) + 1) / 2;\n const displacement = force * strength * radius * 0.3;\n point.x +=\n (point.baseX + (deltaX / distance) * displacement - point.x) * 0.12;\n point.y +=\n (point.baseY + (deltaY / distance) * displacement - point.y) * 0.12;\n } else {\n point.x += (point.baseX - point.x) * 0.08;\n point.y += (point.baseY - point.y) * 0.08;\n }\n }\n\n const movement = Math.max(\n Math.abs(point.x - previousX),\n Math.abs(point.y - previousY)\n );\n if (movement > largestMovement) largestMovement = movement;\n }\n }\n\n return largestMovement;\n};\n\n/**\n * Per-frame movement below which the grid counts as at rest, in pixels.\n *\n * Well under a device pixel, so the frame that crosses it is the last one that\n * could have changed anything on screen.\n */\nconst SETTLE_THRESHOLD_PX = 0.05;\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\ntype GridDistortionPatternProps = {\n width?: number;\n height?: number;\n radius?: number;\n strength?: number;\n lineColor?: string;\n highlightColor?: string;\n} & SVGProps<SVGSVGElement>;\n\nexport const GridDistortionPattern: FC<GridDistortionPatternProps> = ({\n width = 70,\n height = 70,\n radius = 280,\n strength = 0.12,\n lineColor,\n highlightColor,\n className,\n ...props\n}) => {\n const { resolvedTheme } = useTheme();\n const theme = resolvedTheme === 'dark' ? DARK_THEME : LIGHT_THEME;\n\n // Explicit props win; otherwise fall back to the active theme's defaults.\n const resolvedLineColor = lineColor ?? theme.lineColor;\n const resolvedHighlightColor = highlightColor ?? theme.highlightColor;\n\n const svgRef = useRef<SVGSVGElement>(null);\n const mouse = useRef({ x: -9999, y: -9999, active: false });\n const points = useRef<Point[][]>([]);\n /**\n * Bumped on every pointer move. The loop records the value it rendered so it\n * can tell \"the pointer is somewhere over the page but has not moved\" — which\n * looks identical frame to frame — from a genuine change.\n */\n const pointerMoveCount = useRef(0);\n /**\n * Restarts the animation loop after it has parked itself.\n *\n * Assigned by the animation effect; the resize, colour and pointer effects\n * call it so a change they make is picked up even when the grid was at rest.\n */\n const wakeLoop = useRef<() => void>(() => undefined);\n\n // Stable refs so the animation loop always sees the latest prop values\n // without needing to restart itself.\n const radiusRef = useRef(radius);\n const strengthRef = useRef(strength);\n // Initialized to safe placeholders; the real (possibly CSS-variable-based)\n // colors are resolved against the DOM in the effect below.\n const lineRgbRef = useRef<[number, number, number]>(FALLBACK_RGB);\n const highlightRgbRef = useRef<[number, number, number]>(FALLBACK_RGB);\n const baseOpacityRef = useRef(theme.baseOpacity);\n const highlightOpacityBoostRef = useRef(theme.highlightOpacityBoost);\n\n // Keep numeric refs in sync with props on each render (cheap assignments).\n radiusRef.current = radius;\n strengthRef.current = strength;\n baseOpacityRef.current = theme.baseOpacity;\n highlightOpacityBoostRef.current = theme.highlightOpacityBoost;\n\n // Resolve colors once mounted — getComputedStyle is required so that\n // `var(--token)` highlight/line colors resolve against the live cascade.\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n lineRgbRef.current = resolveColorToRgb(resolvedLineColor, svg);\n highlightRgbRef.current = resolveColorToRgb(resolvedHighlightColor, svg);\n wakeLoop.current();\n }, [resolvedLineColor, resolvedHighlightColor]);\n\n // ---------------------------------------------------------------------------\n // Resize — rebuild the point grid and resize the SVG viewBox\n // ---------------------------------------------------------------------------\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n\n const resize = () => {\n const rect = svg.getBoundingClientRect();\n svg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`);\n\n const columns = Math.ceil(rect.width / width) + 2;\n const rows = Math.ceil(rect.height / height) + 2;\n const newGrid: Point[][] = [];\n\n for (let rowIndex = -1; rowIndex <= rows; rowIndex++) {\n const row: Point[] = [];\n for (let columnIndex = -1; columnIndex <= columns; columnIndex++) {\n row.push({\n baseX: columnIndex * width,\n baseY: rowIndex * height,\n x: columnIndex * width,\n y: rowIndex * height,\n });\n }\n newGrid.push(row);\n }\n points.current = newGrid;\n wakeLoop.current();\n };\n\n resize();\n const resizeObserver = new ResizeObserver(resize);\n resizeObserver.observe(svg);\n return () => resizeObserver.disconnect();\n }, [width, height]);\n\n // ---------------------------------------------------------------------------\n // Pointer tracking — passive listeners, no React state involved\n // ---------------------------------------------------------------------------\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = svg.getBoundingClientRect();\n mouse.current = {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n active: true,\n };\n pointerMoveCount.current++;\n wakeLoop.current();\n };\n const handlePointerLeave = () => {\n mouse.current.active = false;\n pointerMoveCount.current++;\n wakeLoop.current();\n };\n\n window.addEventListener('pointermove', handlePointerMove, {\n passive: true,\n });\n window.addEventListener('pointerleave', handlePointerLeave, {\n passive: true,\n });\n return () => {\n window.removeEventListener('pointermove', handlePointerMove);\n window.removeEventListener('pointerleave', handlePointerLeave);\n };\n }, []);\n\n // ---------------------------------------------------------------------------\n // Animation loop — writes directly to SVG DOM, no React state/re-renders\n // ---------------------------------------------------------------------------\n /**\n * Runs the grid only while it has something to show.\n *\n * Each frame rewrites four attributes on every segment — on a full-viewport\n * grid that is close to nine thousand `setAttribute` calls. Left running\n * unconditionally it saturated the main thread of any page embedding the\n * pattern, which starved React's transition lanes and made every other\n * scroll-driven animation on the page advance in visible steps.\n *\n * So the loop parks itself whenever the grid is at rest and the pointer has\n * not moved, and while the SVG is scrolled out of view. Anything that can\n * change the picture — a pointer move, a resize, a colour change, the element\n * coming back on screen — wakes it through {@link wakeLoop}.\n */\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n\n // We maintain a pool of <path> elements and reuse them each frame\n // to avoid GC pressure from creating/deleting DOM nodes every tick.\n const pathPool: SVGPathElement[] = [];\n let frame = 0;\n let isRunning = false;\n let isOnScreen = true;\n /** Value of `pointerMoveCount` the last rendered frame was drawn for. */\n let renderedPointerMoveCount = -1;\n\n const start = () => {\n if (isRunning || !isOnScreen) return;\n isRunning = true;\n frame = requestAnimationFrame(tick);\n };\n\n const stop = () => {\n if (!isRunning) return;\n isRunning = false;\n cancelAnimationFrame(frame);\n };\n\n const ensurePathCount = (count: number) => {\n while (pathPool.length < count) {\n const el = document.createElementNS(\n 'http://www.w3.org/2000/svg',\n 'path'\n );\n el.setAttribute('fill', 'none');\n svg.appendChild(el);\n pathPool.push(el);\n }\n // Hide surplus paths\n for (let i = count; i < pathPool.length; i++) {\n pathPool[i]!.setAttribute('d', '');\n }\n };\n\n const tick = () => {\n const grid = points.current;\n if (!grid.length) {\n // No grid yet — the resize effect will wake us once it has built one.\n isRunning = false;\n return;\n }\n\n const pointerMoveCountAtFrameStart = pointerMoveCount.current;\n\n const { x: mouseX, y: mouseY, active: mouseActive } = mouse.current;\n const currentRadius = radiusRef.current;\n const currentStrength = strengthRef.current;\n const lineRgb = lineRgbRef.current;\n const highlightRgb = highlightRgbRef.current;\n const baseOpacity = baseOpacityRef.current;\n const highlightOpacityBoost = highlightOpacityBoostRef.current;\n\n // Update point positions\n const largestMovement = updateGridPoints(\n grid,\n mouseX,\n mouseY,\n mouseActive,\n currentRadius,\n currentStrength\n );\n\n // Nothing moved and the pointer is where it already was, so this frame\n // would repaint the picture already on screen.\n if (\n largestMovement < SETTLE_THRESHOLD_PX &&\n renderedPointerMoveCount === pointerMoveCountAtFrameStart\n ) {\n isRunning = false;\n return;\n }\n renderedPointerMoveCount = pointerMoveCountAtFrameStart;\n\n const rows = grid.length;\n const columns = grid[0]!.length;\n\n // Count segments up front so we can size the pool\n const horizontalCount = rows * (columns - 1);\n const verticalCount = columns * (rows - 1);\n const totalSegments = horizontalCount + verticalCount;\n ensurePathCount(totalSegments);\n\n let pathIndex = 0;\n\n // Horizontal segments\n for (let rowIndex = 0; rowIndex < rows; rowIndex++) {\n for (let columnIndex = 0; columnIndex < columns - 1; columnIndex++) {\n const p0 = grid[rowIndex]![columnIndex]!;\n const p1 = grid[rowIndex]![columnIndex + 1]!;\n const attrs = buildSegmentAttrs(\n p0.x,\n p0.y,\n p1.x,\n p1.y,\n mouseX,\n mouseY,\n mouseActive,\n currentRadius,\n currentStrength,\n lineRgb,\n highlightRgb,\n baseOpacity,\n highlightOpacityBoost\n );\n const el = pathPool[pathIndex++]!;\n el.setAttribute('d', attrs.d);\n el.setAttribute('stroke', attrs.stroke);\n el.setAttribute('stroke-opacity', String(attrs.strokeOpacity));\n el.setAttribute('stroke-width', String(attrs.strokeWidth));\n }\n }\n\n // Vertical segments\n for (let columnIndex = 0; columnIndex < columns; columnIndex++) {\n for (let rowIndex = 0; rowIndex < rows - 1; rowIndex++) {\n const p0 = grid[rowIndex]![columnIndex]!;\n const p1 = grid[rowIndex + 1]![columnIndex]!;\n const attrs = buildSegmentAttrs(\n p0.x,\n p0.y,\n p1.x,\n p1.y,\n mouseX,\n mouseY,\n mouseActive,\n currentRadius,\n currentStrength,\n lineRgb,\n highlightRgb,\n baseOpacity,\n highlightOpacityBoost\n );\n const el = pathPool[pathIndex++]!;\n el.setAttribute('d', attrs.d);\n el.setAttribute('stroke', attrs.stroke);\n el.setAttribute('stroke-opacity', String(attrs.strokeOpacity));\n el.setAttribute('stroke-width', String(attrs.strokeWidth));\n }\n }\n\n frame = requestAnimationFrame(tick);\n };\n\n wakeLoop.current = start;\n\n // Off-screen the grid cannot be seen, so neither the relaxation nor the\n // redraw is worth a frame.\n const visibilityObserver = new IntersectionObserver(([entry]) => {\n isOnScreen = entry?.isIntersecting ?? true;\n if (isOnScreen) start();\n else stop();\n });\n visibilityObserver.observe(svg);\n\n start();\n\n return () => {\n wakeLoop.current = () => undefined;\n visibilityObserver.disconnect();\n stop();\n // Clean up path elements we injected\n for (const el of pathPool) {\n el.remove();\n }\n };\n }, []); // no dependencies — reads everything via refs\n\n return (\n <svg\n ref={svgRef}\n aria-hidden=\"true\"\n className={cn(\n 'pointer-events-none absolute inset-0 size-full',\n className\n )}\n {...props}\n />\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { scheduleFrameTask } from "../../utils/scheduleFrameTask.mjs";
|
|
3
4
|
import { Detail as Detail$1, PopoverStatic, usePopoverIds } from "./static.mjs";
|
|
4
5
|
import { useLayoutEffect, useRef, useState } from "react";
|
|
5
6
|
import { jsx } from "react/jsx-runtime";
|
|
6
7
|
|
|
7
8
|
//#region src/components/Popover/dynamic.tsx
|
|
9
|
+
/** Gap kept between the trigger and the panel, mirroring the `1rem` offset of the static styles. */
|
|
10
|
+
const TRIGGER_GAP = 16;
|
|
11
|
+
/** Space kept between the panel and the edges of the viewport. */
|
|
12
|
+
const VIEWPORT_PADDING = 16;
|
|
8
13
|
/**
|
|
9
14
|
* Popover Component (Client-side)
|
|
10
15
|
*
|
|
@@ -28,7 +33,8 @@ const PopoverComponent = (props) => {
|
|
|
28
33
|
* - Adds automatic positioning adjustment based on viewport
|
|
29
34
|
* - Calculates optimal X/Y alignment to prevent overflow
|
|
30
35
|
* - Dynamically adjusts max-width based on available space
|
|
31
|
-
* -
|
|
36
|
+
* - Measures only while the panel is open, on a frame shared with the rest of
|
|
37
|
+
* the page
|
|
32
38
|
*
|
|
33
39
|
* @param props - Popover Detail component props
|
|
34
40
|
* @returns Positioned popover content with animations and accessibility
|
|
@@ -40,41 +46,45 @@ const Detail = ({ xAlign = "start", yAlign = "below", ...props }) => {
|
|
|
40
46
|
const [computedYAlign, setComputedYAlign] = useState(yAlign);
|
|
41
47
|
const [maxWidth, setMaxWidth] = useState(void 0);
|
|
42
48
|
useLayoutEffect(() => {
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
/** Cancels the measurement queued for the next frame, if any. */
|
|
50
|
+
let cancelScheduledMeasurement = null;
|
|
51
|
+
/** Cancels the read that follows the max-width write, if any. */
|
|
52
|
+
let cancelScheduledRead = null;
|
|
53
|
+
/** Whether the pointer or the focus is currently inside the trigger. */
|
|
54
|
+
let isOpen = false;
|
|
55
|
+
const measurePosition = () => {
|
|
45
56
|
const popoverElement = popoverRef.current;
|
|
46
57
|
const triggerElement = document.getElementById(triggerId);
|
|
47
|
-
if (!triggerElement) return;
|
|
58
|
+
if (!popoverElement || !triggerElement) return;
|
|
48
59
|
const triggerRect = triggerElement.getBoundingClientRect();
|
|
49
60
|
const viewportWidth = window.innerWidth;
|
|
50
61
|
const viewportHeight = window.innerHeight;
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const maxWidthFromLeft = viewportWidth - triggerRect.left - padding;
|
|
54
|
-
const maxWidthFromRight = triggerRect.right - padding;
|
|
62
|
+
const maxWidthFromLeft = viewportWidth - triggerRect.left - VIEWPORT_PADDING;
|
|
63
|
+
const maxWidthFromRight = triggerRect.right - VIEWPORT_PADDING;
|
|
55
64
|
setMaxWidth(Math.max(maxWidthFromLeft, maxWidthFromRight));
|
|
56
65
|
const wasInvisible = popoverElement.classList.contains("invisible");
|
|
57
66
|
if (wasInvisible) {
|
|
58
67
|
popoverElement.style.visibility = "hidden";
|
|
59
68
|
popoverElement.classList.remove("invisible");
|
|
60
69
|
}
|
|
61
|
-
|
|
70
|
+
cancelScheduledRead = scheduleFrameTask(() => {
|
|
71
|
+
cancelScheduledRead = null;
|
|
62
72
|
const popoverRect = popoverElement.getBoundingClientRect();
|
|
63
73
|
if (wasInvisible) {
|
|
64
74
|
popoverElement.style.visibility = "";
|
|
65
75
|
popoverElement.classList.add("invisible");
|
|
66
76
|
}
|
|
67
77
|
let newYAlign = yAlign;
|
|
68
|
-
const spaceBelow = viewportHeight - triggerRect.bottom -
|
|
69
|
-
const spaceAbove = triggerRect.top -
|
|
78
|
+
const spaceBelow = viewportHeight - triggerRect.bottom - TRIGGER_GAP;
|
|
79
|
+
const spaceAbove = triggerRect.top - TRIGGER_GAP;
|
|
70
80
|
if (yAlign === "below" && spaceBelow < popoverRect.height) {
|
|
71
81
|
if (spaceAbove >= popoverRect.height) newYAlign = "above";
|
|
72
82
|
} else if (yAlign === "above" && spaceAbove < popoverRect.height) {
|
|
73
83
|
if (spaceBelow >= popoverRect.height) newYAlign = "below";
|
|
74
84
|
}
|
|
75
85
|
let newXAlign = xAlign;
|
|
76
|
-
const spaceRight = viewportWidth - triggerRect.left -
|
|
77
|
-
const spaceLeft = triggerRect.right -
|
|
86
|
+
const spaceRight = viewportWidth - triggerRect.left - VIEWPORT_PADDING;
|
|
87
|
+
const spaceLeft = triggerRect.right - VIEWPORT_PADDING;
|
|
78
88
|
if (xAlign === "start" && spaceRight < popoverRect.width) {
|
|
79
89
|
if (spaceLeft >= popoverRect.width) newXAlign = "end";
|
|
80
90
|
} else if (xAlign === "end" && spaceLeft < popoverRect.width) {
|
|
@@ -84,27 +94,61 @@ const Detail = ({ xAlign = "start", yAlign = "below", ...props }) => {
|
|
|
84
94
|
setComputedXAlign(newXAlign);
|
|
85
95
|
});
|
|
86
96
|
};
|
|
87
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Defers the measurement to the next frame, and to a single one however
|
|
99
|
+
* many events fire.
|
|
100
|
+
*
|
|
101
|
+
* Reading `getBoundingClientRect()` straight from a scroll, resize or
|
|
102
|
+
* observer callback forces a synchronous reflow. Sharing one frame with
|
|
103
|
+
* every other measurement the page schedules keeps the reads in a single
|
|
104
|
+
* batch, so React commits them all after the last one.
|
|
105
|
+
*/
|
|
106
|
+
const scheduleMeasurement = () => {
|
|
107
|
+
if (cancelScheduledMeasurement) return;
|
|
108
|
+
cancelScheduledMeasurement = scheduleFrameTask(() => {
|
|
109
|
+
cancelScheduledMeasurement = null;
|
|
110
|
+
measurePosition();
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
const handleOpen = () => {
|
|
114
|
+
if (!isOpen) {
|
|
115
|
+
isOpen = true;
|
|
116
|
+
window.addEventListener("scroll", scheduleMeasurement, {
|
|
117
|
+
passive: true,
|
|
118
|
+
capture: true
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
scheduleMeasurement();
|
|
122
|
+
};
|
|
123
|
+
const handleClose = () => {
|
|
124
|
+
if (!isOpen) return;
|
|
125
|
+
isOpen = false;
|
|
126
|
+
window.removeEventListener("scroll", scheduleMeasurement, true);
|
|
127
|
+
};
|
|
88
128
|
const triggerElement = document.getElementById(triggerId);
|
|
89
129
|
if (triggerElement) {
|
|
90
|
-
triggerElement.addEventListener("mouseenter",
|
|
91
|
-
triggerElement.addEventListener("focusin",
|
|
130
|
+
triggerElement.addEventListener("mouseenter", handleOpen, { passive: true });
|
|
131
|
+
triggerElement.addEventListener("focusin", handleOpen, { passive: true });
|
|
132
|
+
triggerElement.addEventListener("mouseleave", handleClose, { passive: true });
|
|
133
|
+
triggerElement.addEventListener("focusout", handleClose, { passive: true });
|
|
92
134
|
}
|
|
93
135
|
const resizeObserver = new ResizeObserver(() => {
|
|
94
|
-
|
|
136
|
+
if (isOpen) scheduleMeasurement();
|
|
95
137
|
});
|
|
96
138
|
if (popoverRef.current) resizeObserver.observe(popoverRef.current);
|
|
97
|
-
window.addEventListener("resize",
|
|
98
|
-
window.addEventListener("scroll", adjustPosition, true);
|
|
139
|
+
window.addEventListener("resize", scheduleMeasurement, { passive: true });
|
|
99
140
|
return () => {
|
|
100
|
-
|
|
141
|
+
cancelScheduledMeasurement?.();
|
|
142
|
+
cancelScheduledRead?.();
|
|
101
143
|
if (triggerElement) {
|
|
102
|
-
triggerElement.removeEventListener("mouseenter",
|
|
103
|
-
triggerElement.removeEventListener("focusin",
|
|
144
|
+
triggerElement.removeEventListener("mouseenter", handleOpen);
|
|
145
|
+
triggerElement.removeEventListener("focusin", handleOpen);
|
|
146
|
+
triggerElement.removeEventListener("mouseleave", handleClose);
|
|
147
|
+
triggerElement.removeEventListener("focusout", handleClose);
|
|
104
148
|
}
|
|
105
149
|
resizeObserver.disconnect();
|
|
106
|
-
window.removeEventListener("resize",
|
|
107
|
-
window.removeEventListener("scroll",
|
|
150
|
+
window.removeEventListener("resize", scheduleMeasurement);
|
|
151
|
+
window.removeEventListener("scroll", scheduleMeasurement, true);
|
|
108
152
|
};
|
|
109
153
|
}, [
|
|
110
154
|
triggerId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic.mjs","names":["StaticDetail"],"sources":["../../../../src/components/Popover/dynamic.tsx"],"sourcesContent":["'use client';\n\nimport type { FC } from 'react';\nimport { useLayoutEffect, useRef, useState } from 'react';\nimport {\n type DetailProps,\n type PopoverProps,\n PopoverStatic,\n type PopoverType,\n Detail as StaticDetail,\n usePopoverIds,\n} from './static';\n\n/**\n * Popover Component (Client-side)\n *\n * Client-side wrapper around the static Popover component.\n * Reuses the server-side compatible implementation.\n *\n * @param props - Popover component props\n * @returns Trigger container with popover functionality\n */\nconst PopoverComponent: FC<PopoverProps> = (props) => {\n return <PopoverStatic {...props} />;\n};\n\n/**\n * Popover Detail Component (Client-side)\n *\n * Client-side wrapper around the static Detail component that adds automatic\n * positioning logic based on viewport constraints.\n *\n * Features:\n * - Reuses server-side compatible static Detail component\n * - Adds automatic positioning adjustment based on viewport\n * - Calculates optimal X/Y alignment to prevent overflow\n * - Dynamically adjusts max-width based on available space\n * - Listens to window resize and scroll events\n *\n * @param props - Popover Detail component props\n * @returns Positioned popover content with animations and accessibility\n */\nconst Detail: FC<DetailProps> = ({\n xAlign = 'start',\n yAlign = 'below',\n ...props\n}) => {\n const popoverRef = useRef<HTMLDivElement>(null);\n const { triggerId } = usePopoverIds(props.identifier);\n const [computedXAlign, setComputedXAlign] = useState(xAlign);\n const [computedYAlign, setComputedYAlign] = useState(yAlign);\n const [maxWidth, setMaxWidth] = useState<number | undefined>(undefined);\n\n useLayoutEffect(() => {\n const adjustPosition = () => {\n if (!popoverRef.current) return;\n\n const popoverElement = popoverRef.current;\n const triggerElement = document.getElementById(triggerId);\n\n if (!triggerElement) return;\n\n const triggerRect = triggerElement.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n const gap = 16; // 1rem gap\n const padding = 16; // Additional padding from viewport edges\n\n // Calculate maximum width based on viewport and trigger position\n const maxWidthFromLeft = viewportWidth - triggerRect.left - padding;\n const maxWidthFromRight = triggerRect.right - padding;\n\n // Use the larger space to ensure popover can fit\n const absoluteMaxWidth = Math.max(maxWidthFromLeft, maxWidthFromRight);\n\n setMaxWidth(absoluteMaxWidth);\n\n // Force a layout calculation by temporarily making visible if needed\n const wasInvisible = popoverElement.classList.contains('invisible');\n if (wasInvisible) {\n popoverElement.style.visibility = 'hidden';\n popoverElement.classList.remove('invisible');\n }\n\n // Small delay to ensure max-width is applied and content reflows\n requestAnimationFrame(() => {\n const popoverRect = popoverElement.getBoundingClientRect();\n\n // Restore invisible state if it was invisible\n if (wasInvisible) {\n popoverElement.style.visibility = '';\n popoverElement.classList.add('invisible');\n }\n\n // Determine optimal Y alignment\n let newYAlign = yAlign;\n const spaceBelow = viewportHeight - triggerRect.bottom - gap;\n const spaceAbove = triggerRect.top - gap;\n\n if (yAlign === 'below' && spaceBelow < popoverRect.height) {\n // Not enough space below, try above\n if (spaceAbove >= popoverRect.height) {\n newYAlign = 'above';\n }\n } else if (yAlign === 'above' && spaceAbove < popoverRect.height) {\n // Not enough space above, try below\n if (spaceBelow >= popoverRect.height) {\n newYAlign = 'below';\n }\n }\n\n // Determine optimal X alignment\n let newXAlign = xAlign;\n const spaceRight = viewportWidth - triggerRect.left - padding;\n const spaceLeft = triggerRect.right - padding;\n\n if (xAlign === 'start' && spaceRight < popoverRect.width) {\n // Not enough space on the right, try left\n if (spaceLeft >= popoverRect.width) {\n newXAlign = 'end';\n }\n } else if (xAlign === 'end' && spaceLeft < popoverRect.width) {\n // Not enough space on the left, try right\n if (spaceRight >= popoverRect.width) {\n newXAlign = 'start';\n }\n }\n\n setComputedYAlign(newYAlign);\n setComputedXAlign(newXAlign);\n });\n };\n\n // Adjust position with a slight delay to ensure DOM is ready\n const timeoutId = setTimeout(adjustPosition, 0);\n\n // Listen to mouse enter on the trigger to recalculate\n const triggerElement = document.getElementById(triggerId);\n\n if (triggerElement) {\n triggerElement.addEventListener('mouseenter', adjustPosition);\n triggerElement.addEventListener('focusin', adjustPosition);\n }\n\n // Use ResizeObserver to detect popover content size changes\n const resizeObserver = new ResizeObserver(() => {\n adjustPosition();\n });\n\n if (popoverRef.current) {\n resizeObserver.observe(popoverRef.current);\n }\n\n window.addEventListener('resize', adjustPosition);\n window.addEventListener('scroll', adjustPosition, true);\n\n return () => {\n clearTimeout(timeoutId);\n if (triggerElement) {\n triggerElement.removeEventListener('mouseenter', adjustPosition);\n triggerElement.removeEventListener('focusin', adjustPosition);\n }\n resizeObserver.disconnect();\n window.removeEventListener('resize', adjustPosition);\n window.removeEventListener('scroll', adjustPosition, true);\n };\n }, [triggerId, xAlign, yAlign]);\n\n // Use the static Detail component with computed alignment values\n return (\n <StaticDetail\n {...props}\n xAlign={computedXAlign}\n yAlign={computedYAlign}\n ref={popoverRef}\n style={{\n ...props.style,\n maxWidth: maxWidth ? `${maxWidth}px` : undefined,\n }}\n />\n );\n};\n\n// Create Popover with Detail attached\nexport const Popover: PopoverType = PopoverComponent as PopoverType;\n\nPopover.Detail = Detail;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dynamic.mjs","names":["StaticDetail"],"sources":["../../../../src/components/Popover/dynamic.tsx"],"sourcesContent":["'use client';\n\nimport { scheduleFrameTask } from '@utils/scheduleFrameTask';\nimport type { FC } from 'react';\nimport { useLayoutEffect, useRef, useState } from 'react';\nimport {\n type DetailProps,\n type PopoverProps,\n PopoverStatic,\n type PopoverType,\n Detail as StaticDetail,\n usePopoverIds,\n} from './static';\n\n/** Gap kept between the trigger and the panel, mirroring the `1rem` offset of the static styles. */\nconst TRIGGER_GAP = 16;\n\n/** Space kept between the panel and the edges of the viewport. */\nconst VIEWPORT_PADDING = 16;\n\n/**\n * Popover Component (Client-side)\n *\n * Client-side wrapper around the static Popover component.\n * Reuses the server-side compatible implementation.\n *\n * @param props - Popover component props\n * @returns Trigger container with popover functionality\n */\nconst PopoverComponent: FC<PopoverProps> = (props) => {\n return <PopoverStatic {...props} />;\n};\n\n/**\n * Popover Detail Component (Client-side)\n *\n * Client-side wrapper around the static Detail component that adds automatic\n * positioning logic based on viewport constraints.\n *\n * Features:\n * - Reuses server-side compatible static Detail component\n * - Adds automatic positioning adjustment based on viewport\n * - Calculates optimal X/Y alignment to prevent overflow\n * - Dynamically adjusts max-width based on available space\n * - Measures only while the panel is open, on a frame shared with the rest of\n * the page\n *\n * @param props - Popover Detail component props\n * @returns Positioned popover content with animations and accessibility\n */\nconst Detail: FC<DetailProps> = ({\n xAlign = 'start',\n yAlign = 'below',\n ...props\n}) => {\n const popoverRef = useRef<HTMLDivElement>(null);\n const { triggerId } = usePopoverIds(props.identifier);\n const [computedXAlign, setComputedXAlign] = useState(xAlign);\n const [computedYAlign, setComputedYAlign] = useState(yAlign);\n const [maxWidth, setMaxWidth] = useState<number | undefined>(undefined);\n\n useLayoutEffect(() => {\n /** Cancels the measurement queued for the next frame, if any. */\n let cancelScheduledMeasurement: (() => void) | null = null;\n /** Cancels the read that follows the max-width write, if any. */\n let cancelScheduledRead: (() => void) | null = null;\n /** Whether the pointer or the focus is currently inside the trigger. */\n let isOpen = false;\n\n const measurePosition = () => {\n const popoverElement = popoverRef.current;\n const triggerElement = document.getElementById(triggerId);\n\n if (!popoverElement || !triggerElement) return;\n\n const triggerRect = triggerElement.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n\n // Calculate maximum width based on viewport and trigger position, using\n // the larger of the two sides so the panel can always fit somewhere.\n const maxWidthFromLeft =\n viewportWidth - triggerRect.left - VIEWPORT_PADDING;\n const maxWidthFromRight = triggerRect.right - VIEWPORT_PADDING;\n\n setMaxWidth(Math.max(maxWidthFromLeft, maxWidthFromRight));\n\n // A panel that is still hidden has no box to measure, so it is revealed\n // to layout only — `visibility: hidden` keeps it off the screen.\n const wasInvisible = popoverElement.classList.contains('invisible');\n if (wasInvisible) {\n popoverElement.style.visibility = 'hidden';\n popoverElement.classList.remove('invisible');\n }\n\n // The max-width above still has to be committed and laid out, so the\n // panel is measured on the next frame rather than right away.\n cancelScheduledRead = scheduleFrameTask(() => {\n cancelScheduledRead = null;\n\n const popoverRect = popoverElement.getBoundingClientRect();\n\n if (wasInvisible) {\n popoverElement.style.visibility = '';\n popoverElement.classList.add('invisible');\n }\n\n // Determine optimal Y alignment\n let newYAlign = yAlign;\n const spaceBelow = viewportHeight - triggerRect.bottom - TRIGGER_GAP;\n const spaceAbove = triggerRect.top - TRIGGER_GAP;\n\n if (yAlign === 'below' && spaceBelow < popoverRect.height) {\n // Not enough space below, try above\n if (spaceAbove >= popoverRect.height) {\n newYAlign = 'above';\n }\n } else if (yAlign === 'above' && spaceAbove < popoverRect.height) {\n // Not enough space above, try below\n if (spaceBelow >= popoverRect.height) {\n newYAlign = 'below';\n }\n }\n\n // Determine optimal X alignment\n let newXAlign = xAlign;\n const spaceRight = viewportWidth - triggerRect.left - VIEWPORT_PADDING;\n const spaceLeft = triggerRect.right - VIEWPORT_PADDING;\n\n if (xAlign === 'start' && spaceRight < popoverRect.width) {\n // Not enough space on the right, try left\n if (spaceLeft >= popoverRect.width) {\n newXAlign = 'end';\n }\n } else if (xAlign === 'end' && spaceLeft < popoverRect.width) {\n // Not enough space on the left, try right\n if (spaceRight >= popoverRect.width) {\n newXAlign = 'start';\n }\n }\n\n setComputedYAlign(newYAlign);\n setComputedXAlign(newXAlign);\n });\n };\n\n /**\n * Defers the measurement to the next frame, and to a single one however\n * many events fire.\n *\n * Reading `getBoundingClientRect()` straight from a scroll, resize or\n * observer callback forces a synchronous reflow. Sharing one frame with\n * every other measurement the page schedules keeps the reads in a single\n * batch, so React commits them all after the last one.\n */\n const scheduleMeasurement = () => {\n if (cancelScheduledMeasurement) return;\n\n cancelScheduledMeasurement = scheduleFrameTask(() => {\n cancelScheduledMeasurement = null;\n measurePosition();\n });\n };\n\n /*\n * Scrolling only matters while the panel is on screen, and a closed\n * popover is re-measured when it opens — behind an 800ms CSS delay, so the\n * measurement always lands before anything is painted. Subscribing on open\n * rather than on mount keeps a page holding several popovers from running\n * one capture-phase handler per popover on every scroll event, and leaves\n * a page whose popovers are never opened with no layout work at all.\n */\n const handleOpen = () => {\n if (!isOpen) {\n isOpen = true;\n window.addEventListener('scroll', scheduleMeasurement, {\n passive: true,\n capture: true,\n });\n }\n\n scheduleMeasurement();\n };\n\n const handleClose = () => {\n if (!isOpen) return;\n\n isOpen = false;\n window.removeEventListener('scroll', scheduleMeasurement, true);\n };\n\n const triggerElement = document.getElementById(triggerId);\n\n if (triggerElement) {\n triggerElement.addEventListener('mouseenter', handleOpen, {\n passive: true,\n });\n triggerElement.addEventListener('focusin', handleOpen, { passive: true });\n triggerElement.addEventListener('mouseleave', handleClose, {\n passive: true,\n });\n triggerElement.addEventListener('focusout', handleClose, {\n passive: true,\n });\n }\n\n // The panel keeps its own size in check: content growing past the viewport\n // has to flip the alignment even when the user did nothing.\n const resizeObserver = new ResizeObserver(() => {\n if (isOpen) scheduleMeasurement();\n });\n\n if (popoverRef.current) {\n resizeObserver.observe(popoverRef.current);\n }\n\n window.addEventListener('resize', scheduleMeasurement, { passive: true });\n\n return () => {\n cancelScheduledMeasurement?.();\n cancelScheduledRead?.();\n\n if (triggerElement) {\n triggerElement.removeEventListener('mouseenter', handleOpen);\n triggerElement.removeEventListener('focusin', handleOpen);\n triggerElement.removeEventListener('mouseleave', handleClose);\n triggerElement.removeEventListener('focusout', handleClose);\n }\n\n resizeObserver.disconnect();\n window.removeEventListener('resize', scheduleMeasurement);\n window.removeEventListener('scroll', scheduleMeasurement, true);\n };\n }, [triggerId, xAlign, yAlign]);\n\n // Use the static Detail component with computed alignment values\n return (\n <StaticDetail\n {...props}\n xAlign={computedXAlign}\n yAlign={computedYAlign}\n ref={popoverRef}\n style={{\n ...props.style,\n maxWidth: maxWidth ? `${maxWidth}px` : undefined,\n }}\n />\n );\n};\n\n// Create Popover with Detail attached\nexport const Popover: PopoverType = PopoverComponent as PopoverType;\n\nPopover.Detail = Detail;\n"],"mappings":"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"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useGetElementOrWindow } from "../../hooks/useGetElementOrWindow.mjs";
|
|
4
|
-
import { useDevice } from "../../hooks/useDevice.mjs";
|
|
5
|
-
import { useScrollBlockage } from "../../hooks/useScrollBlockage/index.mjs";
|
|
6
3
|
import { Button } from "../Button/Button.mjs";
|
|
7
4
|
import { Container } from "../Container/index.mjs";
|
|
5
|
+
import { useDevice } from "../../hooks/useDevice.mjs";
|
|
8
6
|
import { KeyboardShortcut } from "../KeyboardShortcut/KeyboardShortcut.mjs";
|
|
9
7
|
import { Popover } from "../Popover/dynamic.mjs";
|
|
8
|
+
import { useGetElementOrWindow } from "../../hooks/useGetElementOrWindow.mjs";
|
|
9
|
+
import { useScrollBlockage } from "../../hooks/useScrollBlockage/index.mjs";
|
|
10
10
|
import { MaxWidthSmoother } from "../MaxWidthSmoother/index.mjs";
|
|
11
11
|
import { isElementAtTopAndNotCovered } from "./isElementAtTopAndNotCovered.mjs";
|
|
12
12
|
import { useRightDrawer } from "./useRightDrawer.mjs";
|
|
@@ -98,9 +98,10 @@ const RightDrawer = ({ title, identifier, children, header, footer, closeOnOutsi
|
|
|
98
98
|
isHidden: !isOpen,
|
|
99
99
|
align: "right",
|
|
100
100
|
children: /* @__PURE__ */ jsxs(Container, {
|
|
101
|
-
className: "relative flex h-screen w-screen flex-col text-text md:w-100",
|
|
101
|
+
className: "relative flex h-screen w-screen flex-col border-l text-text md:w-100",
|
|
102
102
|
ref: panelRef,
|
|
103
103
|
roundedSize: "none",
|
|
104
|
+
borderColor: "neutral",
|
|
104
105
|
children: [
|
|
105
106
|
/* @__PURE__ */ jsxs("div", {
|
|
106
107
|
className: "flex shrink-0 flex-col gap-3 px-6 pt-6",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RightDrawer.mjs","names":[],"sources":["../../../../src/components/RightDrawer/RightDrawer.tsx"],"sourcesContent":["'use client';\n\nimport { useGetElementOrWindow } from '@hooks/index';\nimport { useDevice } from '@hooks/useDevice';\nimport { useScrollBlockage } from '@hooks/useScrollBlockage';\nimport { ChevronLeft, X } from 'lucide-react';\nimport {\n type FC,\n type KeyboardEventHandler,\n type MouseEventHandler,\n type ReactNode,\n useEffect,\n useRef,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { useIntlayer } from 'react-intlayer';\nimport { Button } from '../Button';\nimport { Container } from '../Container';\nimport { KeyboardShortcut } from '../KeyboardShortcut';\nimport { MaxWidthSmoother } from '../MaxWidthSmoother/index';\nimport { Popover } from '../Popover';\nimport { isElementAtTopAndNotCovered } from './isElementAtTopAndNotCovered';\nimport { useRightDrawer } from './useRightDrawer';\n\n/**\n * Configuration for the back button functionality in the RightDrawer\n *\n * @interface BackButtonProps\n */\ntype BackButtonProps = {\n /** Callback function triggered when the back button is clicked */\n onBack: () => void;\n /** Optional custom text for the back buttoDefaults to \"Go back\" if not provided */\n text?: string;\n};\n\n/**\n * Props configuration for the RightDrawer component\n *\n * @interface RightDrawerProps\n */\ntype RightDrawerProps = {\n /**\n * Title displayed in the drawer header\n */\n title?: ReactNode;\n\n /**\n * Unique identifier for the drawer instancRequired for store management\n */\n identifier: string;\n\n /** The content to be displayed inside the drawer */\n children?: ReactNode;\n\n /**\n * Optional header content displayed below the title\n */\n header?: ReactNode;\n\n /**\n * Optional footer content pinned to the bottom of the drawer\n */\n footer?: ReactNode;\n\n /**\n * Whether the drawer should close when clicking outside of it\n * @default true\n */\n closeOnOutsideClick?: boolean;\n\n /**\n * Configuration for an optional back button in the drawer header\n */\n backButton?: BackButtonProps;\n\n /**\n * External control for the open statWhen provided, overrides internal store state\n */\n isOpen?: boolean;\n\n /**\n * Callback function triggered when the drawer is closed\n */\n onClose?: () => void;\n\n /**\n * Optional container to render the drawer into.\n * If not provided, it will be rendered into the body.\n */\n container?: HTMLElement;\n};\n\nexport const RightDrawer: FC<RightDrawerProps> = ({\n title,\n identifier,\n children,\n header,\n footer,\n closeOnOutsideClick = true,\n backButton,\n isOpen: isOpenProp,\n onClose,\n container,\n}) => {\n const content = useIntlayer('right-drawer');\n const { isMobile } = useDevice('md');\n const panelRef = useRef<HTMLDivElement>(null);\n const childrenContainerRef = useRef<HTMLDivElement>(null);\n const containerElement = useGetElementOrWindow(container);\n\n const {\n open: openDrawer,\n close: closeDrawer,\n isOpen: checkIsOpen,\n } = useRightDrawer();\n const storeIsOpen = checkIsOpen(identifier);\n const isOpen = storeIsOpen;\n\n useScrollBlockage({\n disableScroll: isOpen,\n key: identifier ? `right_drawer_${identifier}` : 'right_drawer',\n });\n\n // Handle Click Outside\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n try {\n if (!panelRef.current) return;\n\n const isClickAble = isOpen && closeOnOutsideClick;\n const isClickOutside =\n event.target && !panelRef.current.contains(event.target as Node);\n const isAtTopAndVisible = isElementAtTopAndNotCovered(panelRef.current);\n\n if (\n (isClickAble && isClickOutside && isAtTopAndVisible) ||\n !event.target\n ) {\n closeDrawer(identifier);\n onClose?.();\n }\n } catch (_e) {\n closeDrawer(identifier);\n onClose?.();\n }\n };\n\n window.addEventListener('mousedown', handleClickOutside);\n return () => window.removeEventListener('mousedown', handleClickOutside);\n }, [isOpen, closeDrawer, onClose, closeOnOutsideClick, identifier]);\n\n const onCloseRef = useRef(onClose);\n useEffect(() => {\n onCloseRef.current = onClose;\n }, [onClose]);\n\n useEffect(() => {\n if (isOpenProp === undefined) return;\n if (isOpenProp === storeIsOpen) return;\n\n if (isOpenProp) {\n openDrawer(identifier);\n } else {\n closeDrawer(identifier);\n onCloseRef.current?.();\n }\n }, [isOpenProp, storeIsOpen, identifier, openDrawer, closeDrawer]);\n\n const handleSpareSpaceClick: MouseEventHandler<HTMLDivElement> = (e) => {\n if (e.target !== e.currentTarget) {\n return;\n }\n if (isMobile) {\n closeDrawer(identifier);\n onClose?.();\n }\n };\n\n // Handle Keyboard on Spare Space (Linter Fix)\n const handleSpareSpaceKeyDown: KeyboardEventHandler<HTMLDivElement> = (e) => {\n if (e.target !== e.currentTarget) return;\n\n // Allow closing via Enter or Space if focused on the spare area\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n if (isMobile) {\n closeDrawer(identifier);\n onClose?.();\n }\n }\n };\n\n if (!containerElement) return <></>;\n\n return createPortal(\n <div className=\"fixed top-0 right-0 z-50 flex h-full justify-end\">\n <MaxWidthSmoother isHidden={!isOpen} align=\"right\">\n <Container\n className=\"relative flex h-screen w-screen flex-col text-text md:w-100\"\n ref={panelRef}\n roundedSize=\"none\"\n >\n {/* Header */}\n <div className=\"flex shrink-0 flex-col gap-3 px-6 pt-6\">\n <div className=\"flex justify-between gap-3\">\n <div>\n {backButton && (\n <Button\n variant=\"hoverable\"\n color=\"text\"\n label={backButton.text ?? content.goBack.value}\n onClick={backButton.onBack}\n Icon={ChevronLeft}\n >\n {backButton?.text}\n </Button>\n )}\n </div>\n <div>\n <Popover identifier=\"close-drawer\">\n <Button\n variant=\"hoverable\"\n color=\"text\"\n label=\"Close\"\n className=\"ml-auto\"\n onClick={() => {\n closeDrawer(identifier);\n onClose?.();\n }}\n Icon={X}\n size=\"icon-md\"\n />\n\n <Popover.Detail identifier=\"close-drawer\">\n <div className=\"flex items-center gap-2 p-2\">\n <span className=\"whitespace-nowrap text-neutral text-xs\">\n {content.closeDrawer}\n </span>\n <KeyboardShortcut\n shortcut=\"Escape\"\n size=\"sm\"\n onTriggered={() => {\n closeDrawer(identifier);\n onClose?.();\n }}\n />\n </div>\n </Popover.Detail>\n </Popover>\n </div>\n </div>\n {title && (\n <h2 className=\"flex items-center justify-center font-bold text-lg\">\n {title}\n </h2>\n )}\n {header}\n </div>\n\n {/* Body */}\n <div className=\"flex min-h-0 flex-1 flex-col overflow-y-auto p-2\">\n {/** biome-ignore lint/a11y/useSemanticElements: This div is used to handle the spare space click and keydown events */}\n <div\n className=\"flex flex-1 flex-col outline-none\"\n onClick={handleSpareSpaceClick}\n onKeyDown={handleSpareSpaceKeyDown}\n ref={childrenContainerRef}\n role=\"button\" // Semantically acts as a button area\n tabIndex={0} // Makes it focusable to receive key events\n >\n {children}\n </div>\n </div>\n\n {/* Footer */}\n {footer && <div className=\"shrink-0\">{footer}</div>}\n </Container>\n </MaxWidthSmoother>\n </div>,\n containerElement\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RightDrawer.mjs","names":[],"sources":["../../../../src/components/RightDrawer/RightDrawer.tsx"],"sourcesContent":["'use client';\n\nimport { useGetElementOrWindow } from '@hooks/index';\nimport { useDevice } from '@hooks/useDevice';\nimport { useScrollBlockage } from '@hooks/useScrollBlockage';\nimport { ChevronLeft, X } from 'lucide-react';\nimport {\n type FC,\n type KeyboardEventHandler,\n type MouseEventHandler,\n type ReactNode,\n useEffect,\n useRef,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { useIntlayer } from 'react-intlayer';\nimport { Button } from '../Button';\nimport { Container } from '../Container';\nimport { KeyboardShortcut } from '../KeyboardShortcut';\nimport { MaxWidthSmoother } from '../MaxWidthSmoother/index';\nimport { Popover } from '../Popover';\nimport { isElementAtTopAndNotCovered } from './isElementAtTopAndNotCovered';\nimport { useRightDrawer } from './useRightDrawer';\n\n/**\n * Configuration for the back button functionality in the RightDrawer\n *\n * @interface BackButtonProps\n */\ntype BackButtonProps = {\n /** Callback function triggered when the back button is clicked */\n onBack: () => void;\n /** Optional custom text for the back buttoDefaults to \"Go back\" if not provided */\n text?: string;\n};\n\n/**\n * Props configuration for the RightDrawer component\n *\n * @interface RightDrawerProps\n */\ntype RightDrawerProps = {\n /**\n * Title displayed in the drawer header\n */\n title?: ReactNode;\n\n /**\n * Unique identifier for the drawer instancRequired for store management\n */\n identifier: string;\n\n /** The content to be displayed inside the drawer */\n children?: ReactNode;\n\n /**\n * Optional header content displayed below the title\n */\n header?: ReactNode;\n\n /**\n * Optional footer content pinned to the bottom of the drawer\n */\n footer?: ReactNode;\n\n /**\n * Whether the drawer should close when clicking outside of it\n * @default true\n */\n closeOnOutsideClick?: boolean;\n\n /**\n * Configuration for an optional back button in the drawer header\n */\n backButton?: BackButtonProps;\n\n /**\n * External control for the open statWhen provided, overrides internal store state\n */\n isOpen?: boolean;\n\n /**\n * Callback function triggered when the drawer is closed\n */\n onClose?: () => void;\n\n /**\n * Optional container to render the drawer into.\n * If not provided, it will be rendered into the body.\n */\n container?: HTMLElement;\n};\n\nexport const RightDrawer: FC<RightDrawerProps> = ({\n title,\n identifier,\n children,\n header,\n footer,\n closeOnOutsideClick = true,\n backButton,\n isOpen: isOpenProp,\n onClose,\n container,\n}) => {\n const content = useIntlayer('right-drawer');\n const { isMobile } = useDevice('md');\n const panelRef = useRef<HTMLDivElement>(null);\n const childrenContainerRef = useRef<HTMLDivElement>(null);\n const containerElement = useGetElementOrWindow(container);\n\n const {\n open: openDrawer,\n close: closeDrawer,\n isOpen: checkIsOpen,\n } = useRightDrawer();\n const storeIsOpen = checkIsOpen(identifier);\n const isOpen = storeIsOpen;\n\n useScrollBlockage({\n disableScroll: isOpen,\n key: identifier ? `right_drawer_${identifier}` : 'right_drawer',\n });\n\n // Handle Click Outside\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n try {\n if (!panelRef.current) return;\n\n const isClickAble = isOpen && closeOnOutsideClick;\n const isClickOutside =\n event.target && !panelRef.current.contains(event.target as Node);\n const isAtTopAndVisible = isElementAtTopAndNotCovered(panelRef.current);\n\n if (\n (isClickAble && isClickOutside && isAtTopAndVisible) ||\n !event.target\n ) {\n closeDrawer(identifier);\n onClose?.();\n }\n } catch (_e) {\n closeDrawer(identifier);\n onClose?.();\n }\n };\n\n window.addEventListener('mousedown', handleClickOutside);\n return () => window.removeEventListener('mousedown', handleClickOutside);\n }, [isOpen, closeDrawer, onClose, closeOnOutsideClick, identifier]);\n\n const onCloseRef = useRef(onClose);\n useEffect(() => {\n onCloseRef.current = onClose;\n }, [onClose]);\n\n useEffect(() => {\n if (isOpenProp === undefined) return;\n if (isOpenProp === storeIsOpen) return;\n\n if (isOpenProp) {\n openDrawer(identifier);\n } else {\n closeDrawer(identifier);\n onCloseRef.current?.();\n }\n }, [isOpenProp, storeIsOpen, identifier, openDrawer, closeDrawer]);\n\n const handleSpareSpaceClick: MouseEventHandler<HTMLDivElement> = (e) => {\n if (e.target !== e.currentTarget) {\n return;\n }\n if (isMobile) {\n closeDrawer(identifier);\n onClose?.();\n }\n };\n\n // Handle Keyboard on Spare Space (Linter Fix)\n const handleSpareSpaceKeyDown: KeyboardEventHandler<HTMLDivElement> = (e) => {\n if (e.target !== e.currentTarget) return;\n\n // Allow closing via Enter or Space if focused on the spare area\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n if (isMobile) {\n closeDrawer(identifier);\n onClose?.();\n }\n }\n };\n\n if (!containerElement) return <></>;\n\n return createPortal(\n <div className=\"fixed top-0 right-0 z-50 flex h-full justify-end\">\n <MaxWidthSmoother isHidden={!isOpen} align=\"right\">\n <Container\n className=\"relative flex h-screen w-screen flex-col border-l text-text md:w-100\"\n ref={panelRef}\n roundedSize=\"none\"\n borderColor=\"neutral\"\n >\n {/* Header */}\n <div className=\"flex shrink-0 flex-col gap-3 px-6 pt-6\">\n <div className=\"flex justify-between gap-3\">\n <div>\n {backButton && (\n <Button\n variant=\"hoverable\"\n color=\"text\"\n label={backButton.text ?? content.goBack.value}\n onClick={backButton.onBack}\n Icon={ChevronLeft}\n >\n {backButton?.text}\n </Button>\n )}\n </div>\n <div>\n <Popover identifier=\"close-drawer\">\n <Button\n variant=\"hoverable\"\n color=\"text\"\n label=\"Close\"\n className=\"ml-auto\"\n onClick={() => {\n closeDrawer(identifier);\n onClose?.();\n }}\n Icon={X}\n size=\"icon-md\"\n />\n\n <Popover.Detail identifier=\"close-drawer\">\n <div className=\"flex items-center gap-2 p-2\">\n <span className=\"whitespace-nowrap text-neutral text-xs\">\n {content.closeDrawer}\n </span>\n <KeyboardShortcut\n shortcut=\"Escape\"\n size=\"sm\"\n onTriggered={() => {\n closeDrawer(identifier);\n onClose?.();\n }}\n />\n </div>\n </Popover.Detail>\n </Popover>\n </div>\n </div>\n {title && (\n <h2 className=\"flex items-center justify-center font-bold text-lg\">\n {title}\n </h2>\n )}\n {header}\n </div>\n\n {/* Body */}\n <div className=\"flex min-h-0 flex-1 flex-col overflow-y-auto p-2\">\n {/** biome-ignore lint/a11y/useSemanticElements: This div is used to handle the spare space click and keydown events */}\n <div\n className=\"flex flex-1 flex-col outline-none\"\n onClick={handleSpareSpaceClick}\n onKeyDown={handleSpareSpaceKeyDown}\n ref={childrenContainerRef}\n role=\"button\" // Semantically acts as a button area\n tabIndex={0} // Makes it focusable to receive key events\n >\n {children}\n </div>\n </div>\n\n {/* Footer */}\n {footer && <div className=\"shrink-0\">{footer}</div>}\n </Container>\n </MaxWidthSmoother>\n </div>,\n containerElement\n );\n};\n"],"mappings":"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"}
|
|
@@ -5,12 +5,12 @@ import { useEffect, useRef, useState } from "react";
|
|
|
5
5
|
|
|
6
6
|
//#region src/components/SwitchSelector/useSwitchSelector.ts
|
|
7
7
|
const useSwitchSelector = ({ choices = [], value, defaultValue, onChange, hoverable = true, disabled = false }, orientation) => {
|
|
8
|
-
const [valueState, setValue] = useState(value ?? defaultValue ?? choices[0]
|
|
8
|
+
const [valueState, setValue] = useState(value ?? defaultValue ?? choices[0].value);
|
|
9
9
|
const [hoveredIndex, setHoveredIndex] = useState(null);
|
|
10
10
|
const optionsRefs = useRef([]);
|
|
11
11
|
const indicatorRef = useRef(null);
|
|
12
12
|
const { choiceIndicatorPosition } = useItemSelector(optionsRefs, {
|
|
13
|
-
selector: (el) => el
|
|
13
|
+
selector: (el) => el?.getAttribute("data-indicator") === "true",
|
|
14
14
|
isHoverable: false,
|
|
15
15
|
orientation
|
|
16
16
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSwitchSelector.mjs","names":[],"sources":["../../../../src/components/SwitchSelector/useSwitchSelector.ts"],"sourcesContent":["'use client';\n\nimport {\n type ItemSelectorOrientation,\n useItemSelector,\n} from '@hooks/useItemSelector';\nimport { useEffect, useRef, useState } from 'react';\nimport type { SwitchSelectorBaseProps } from './SwitchSelector';\n\nexport const useSwitchSelector = <T>(\n {\n choices = [],\n value,\n defaultValue,\n onChange,\n hoverable = true,\n disabled = false,\n }: SwitchSelectorBaseProps<T>,\n orientation: ItemSelectorOrientation\n) => {\n const [valueState, setValue] = useState<T>(\n value ?? defaultValue ?? choices[0]
|
|
1
|
+
{"version":3,"file":"useSwitchSelector.mjs","names":[],"sources":["../../../../src/components/SwitchSelector/useSwitchSelector.ts"],"sourcesContent":["'use client';\n\nimport {\n type ItemSelectorOrientation,\n useItemSelector,\n} from '@hooks/useItemSelector';\nimport { useEffect, useRef, useState } from 'react';\nimport type { SwitchSelectorBaseProps } from './SwitchSelector';\n\nexport const useSwitchSelector = <T>(\n {\n choices = [],\n value,\n defaultValue,\n onChange,\n hoverable = true,\n disabled = false,\n }: SwitchSelectorBaseProps<T>,\n orientation: ItemSelectorOrientation\n) => {\n const [valueState, setValue] = useState<T>(\n value ?? defaultValue ?? choices[0]!.value\n );\n const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n\n const optionsRefs = useRef<HTMLButtonElement[]>([]);\n const indicatorRef = useRef<HTMLDivElement | null>(null);\n const { choiceIndicatorPosition } = useItemSelector(optionsRefs, {\n selector: (el) => el?.getAttribute('data-indicator') === 'true',\n isHoverable: false,\n orientation,\n });\n\n const selectedIndex = choices.findIndex(\n (choice) => choice.value === valueState\n );\n\n // The indicator follows hover if hoverable, otherwise the selected option\n const indicatorIndex =\n hoverable && hoveredIndex !== null ? hoveredIndex : selectedIndex;\n\n const handleChange = (newValue: T) => {\n setValue(newValue);\n onChange?.(newValue);\n };\n\n useEffect(() => {\n if (value === undefined) return;\n setValue(value);\n }, [value]);\n\n return {\n valueState,\n hoveredIndex,\n setHoveredIndex,\n optionsRefs,\n indicatorRef,\n choiceIndicatorPosition,\n selectedIndex,\n indicatorIndex,\n handleChange,\n disabled,\n };\n};\n"],"mappings":";;;;;;AASA,MAAa,qBACX,EACE,UAAU,CAAC,GACX,OACA,cACA,UACA,YAAY,MACZ,WAAW,SAEb,gBACG;CACH,MAAM,CAAC,YAAY,YAAY,SAC7B,SAAS,gBAAgB,QAAQ,EAAE,CAAE,KACvC;CACA,MAAM,CAAC,cAAc,mBAAmB,SAAwB,IAAI;CAEpE,MAAM,cAAc,OAA4B,CAAC,CAAC;CAClD,MAAM,eAAe,OAA8B,IAAI;CACvD,MAAM,EAAE,4BAA4B,gBAAgB,aAAa;EAC/D,WAAW,OAAO,IAAI,aAAa,gBAAgB,MAAM;EACzD,aAAa;EACb;CACF,CAAC;CAED,MAAM,gBAAgB,QAAQ,WAC3B,WAAW,OAAO,UAAU,UAC/B;CAGA,MAAM,iBACJ,aAAa,iBAAiB,OAAO,eAAe;CAEtD,MAAM,gBAAgB,aAAgB;EACpC,SAAS,QAAQ;EACjB,WAAW,QAAQ;CACrB;CAEA,gBAAgB;EACd,IAAI,UAAU,QAAW;EACzB,SAAS,KAAK;CAChB,GAAG,CAAC,KAAK,CAAC;CAEV,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { cn } from "../../utils/cn.mjs";
|
|
4
|
-
import { useHorizontalSwipe } from "../../hooks/useHorizontalSwipe.mjs";
|
|
5
4
|
import { TabSelector } from "../TabSelector/TabSelector.mjs";
|
|
5
|
+
import { useHorizontalSwipe } from "../../hooks/useHorizontalSwipe.mjs";
|
|
6
6
|
import { useTabContext } from "./TabContext.mjs";
|
|
7
7
|
import { Children, createContext, isValidElement, useState } from "react";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -57,12 +57,12 @@ const indicatorVariant = cva("absolute -z-1 rounded-lg duration-300 ease-in-out
|
|
|
57
57
|
const TabSelector = ({ tabs, selectedChoice, onTabClick, color = "primary", hoverable = false, orientation = "horizontal", className }) => {
|
|
58
58
|
const optionsRefs = useRef([]);
|
|
59
59
|
const indicatorRef = useRef(null);
|
|
60
|
-
const { choiceIndicatorPosition,
|
|
60
|
+
const { choiceIndicatorPosition, scheduleMeasurement } = useItemSelector(optionsRefs, {
|
|
61
61
|
isHoverable: hoverable,
|
|
62
62
|
orientation
|
|
63
63
|
});
|
|
64
64
|
useEffect(() => {
|
|
65
|
-
|
|
65
|
+
scheduleMeasurement();
|
|
66
66
|
}, [selectedChoice]);
|
|
67
67
|
return /* @__PURE__ */ jsxs("div", {
|
|
68
68
|
className: cn(tabSelectorVariant({ color }), className),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TabSelector.mjs","names":[],"sources":["../../../../src/components/TabSelector/TabSelector.tsx"],"sourcesContent":["'use client';\n\nimport {\n type ItemSelectorOrientation,\n useItemSelector,\n} from '@hooks/useItemSelector';\nimport { cn } from '@utils/cn';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport {\n cloneElement,\n type HTMLAttributes,\n type MouseEvent,\n type ReactElement,\n useEffect,\n useRef,\n} from 'react';\n\nexport type TabSelectorColor =\n | 'primary'\n | 'secondary'\n | 'error'\n | 'neutral'\n | 'light'\n | 'dark'\n | 'text';\n\nconst tabSelectorVariant = cva(\n 'relative z-0 flex size-full flex-row items-center gap-2',\n {\n variants: {\n color: {\n primary: 'border-primary text-primary',\n secondary: 'border-secondary text-secondary',\n error: 'border-error bg-error text-error',\n neutral: 'border-neutral text-neutral',\n light: 'border-white text-white',\n dark: 'border-neutral-800 text-neutral-800',\n text: 'border-text text-text',\n },\n },\n defaultVariants: {\n color: 'primary',\n },\n }\n);\n\nconst indicatorVariant = cva(\n 'absolute -z-1 rounded-lg duration-300 ease-in-out motion-reduce:transition-none',\n {\n variants: {\n color: {\n primary: 'bg-primary/10 aria-selected:text-text',\n secondary: 'bg-secondary/10 aria-selected:text-text',\n error: 'bg-error/10 aria-selected:text-text',\n neutral: 'bg-neutral/10 aria-selected:text-white/10',\n light: 'bg-white/10 aria-selected:text-black',\n dark: 'bg-neutral-800/10 aria-selected:text-white',\n text: 'bg-text/10 aria-selected:text-text-opposite',\n },\n orientation: {\n horizontal: 'top-0 h-full w-auto transition-[left,width]',\n vertical: 'left-0 h-auto w-full transition-[top,height]',\n },\n },\n defaultVariants: {\n orientation: 'horizontal',\n },\n }\n);\n\nexport type TabSelectorItemProps = HTMLAttributes<HTMLElement> & {\n key: string | number;\n};\n\nexport type TabSelectorProps<T extends TabSelectorItemProps> = {\n tabs: ReactElement<T>[];\n selectedChoice: T['key'];\n onTabClick?: (choice: T['key']) => void;\n hoverable?: boolean;\n orientation?: ItemSelectorOrientation;\n} & HTMLAttributes<HTMLElement> &\n Omit<VariantProps<typeof tabSelectorVariant>, 'color'> & {\n color?: TabSelectorColor | `${TabSelectorColor}`;\n };\n\n/**\n *\n * Component that allows the user to select one of the provided choices.\n *\n * Example:\n * ```jsx\n * <TabSelector\n * selectedChoice=\"option1\"\n * onTabClick={(choice) => console.log(choice)}\n * hoverable={true}\n * >\n * <Button key=\"option1\"/>\n * <Button key=\"option2\"/>\n * <Button key=\"option3\"/>\n * </TabSelector>\n * ```\n */\nexport const TabSelector = <T extends TabSelectorItemProps>({\n tabs,\n selectedChoice,\n onTabClick,\n color = 'primary',\n hoverable = false,\n orientation = 'horizontal',\n className,\n}: TabSelectorProps<T>) => {\n const optionsRefs = useRef<HTMLElement[]>([]);\n const indicatorRef = useRef<HTMLDivElement | null>(null);\n const { choiceIndicatorPosition,
|
|
1
|
+
{"version":3,"file":"TabSelector.mjs","names":[],"sources":["../../../../src/components/TabSelector/TabSelector.tsx"],"sourcesContent":["'use client';\n\nimport {\n type ItemSelectorOrientation,\n useItemSelector,\n} from '@hooks/useItemSelector';\nimport { cn } from '@utils/cn';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport {\n cloneElement,\n type HTMLAttributes,\n type MouseEvent,\n type ReactElement,\n useEffect,\n useRef,\n} from 'react';\n\nexport type TabSelectorColor =\n | 'primary'\n | 'secondary'\n | 'error'\n | 'neutral'\n | 'light'\n | 'dark'\n | 'text';\n\nconst tabSelectorVariant = cva(\n 'relative z-0 flex size-full flex-row items-center gap-2',\n {\n variants: {\n color: {\n primary: 'border-primary text-primary',\n secondary: 'border-secondary text-secondary',\n error: 'border-error bg-error text-error',\n neutral: 'border-neutral text-neutral',\n light: 'border-white text-white',\n dark: 'border-neutral-800 text-neutral-800',\n text: 'border-text text-text',\n },\n },\n defaultVariants: {\n color: 'primary',\n },\n }\n);\n\nconst indicatorVariant = cva(\n 'absolute -z-1 rounded-lg duration-300 ease-in-out motion-reduce:transition-none',\n {\n variants: {\n color: {\n primary: 'bg-primary/10 aria-selected:text-text',\n secondary: 'bg-secondary/10 aria-selected:text-text',\n error: 'bg-error/10 aria-selected:text-text',\n neutral: 'bg-neutral/10 aria-selected:text-white/10',\n light: 'bg-white/10 aria-selected:text-black',\n dark: 'bg-neutral-800/10 aria-selected:text-white',\n text: 'bg-text/10 aria-selected:text-text-opposite',\n },\n orientation: {\n horizontal: 'top-0 h-full w-auto transition-[left,width]',\n vertical: 'left-0 h-auto w-full transition-[top,height]',\n },\n },\n defaultVariants: {\n orientation: 'horizontal',\n },\n }\n);\n\nexport type TabSelectorItemProps = HTMLAttributes<HTMLElement> & {\n key: string | number;\n};\n\nexport type TabSelectorProps<T extends TabSelectorItemProps> = {\n tabs: ReactElement<T>[];\n selectedChoice: T['key'];\n onTabClick?: (choice: T['key']) => void;\n hoverable?: boolean;\n orientation?: ItemSelectorOrientation;\n} & HTMLAttributes<HTMLElement> &\n Omit<VariantProps<typeof tabSelectorVariant>, 'color'> & {\n color?: TabSelectorColor | `${TabSelectorColor}`;\n };\n\n/**\n *\n * Component that allows the user to select one of the provided choices.\n *\n * Example:\n * ```jsx\n * <TabSelector\n * selectedChoice=\"option1\"\n * onTabClick={(choice) => console.log(choice)}\n * hoverable={true}\n * >\n * <Button key=\"option1\"/>\n * <Button key=\"option2\"/>\n * <Button key=\"option3\"/>\n * </TabSelector>\n * ```\n */\nexport const TabSelector = <T extends TabSelectorItemProps>({\n tabs,\n selectedChoice,\n onTabClick,\n color = 'primary',\n hoverable = false,\n orientation = 'horizontal',\n className,\n}: TabSelectorProps<T>) => {\n const optionsRefs = useRef<HTMLElement[]>([]);\n const indicatorRef = useRef<HTMLDivElement | null>(null);\n const { choiceIndicatorPosition, scheduleMeasurement } = useItemSelector(\n optionsRefs,\n { isHoverable: hoverable, orientation }\n );\n\n useEffect(() => {\n scheduleMeasurement();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [selectedChoice]);\n\n return (\n <div\n className={cn(\n tabSelectorVariant({\n color,\n }),\n className\n )}\n aria-orientation={orientation}\n aria-multiselectable=\"false\"\n role=\"tablist\"\n >\n {tabs.map((Tab, index) => {\n const key = Tab.key!;\n\n const isSelected = selectedChoice === key;\n\n return cloneElement(Tab, {\n key: key ?? index,\n role: 'tab',\n onClick: (e: MouseEvent<HTMLElement>) => {\n Tab.props?.onClick?.(e);\n onTabClick?.(key);\n },\n 'aria-selected': isSelected,\n 'data-active': isSelected as unknown as string,\n tabIndex: isSelected ? 0 : -1,\n ref: (el: HTMLElement) => {\n optionsRefs.current[index] = el!;\n },\n } as unknown as T);\n })}\n {choiceIndicatorPosition && (\n <div\n className={cn(\n indicatorVariant({\n color,\n orientation,\n })\n )}\n style={choiceIndicatorPosition}\n ref={indicatorRef}\n key={`${selectedChoice}${JSON.stringify(tabs.map((tab) => tab.key))}`}\n />\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;AA0BA,MAAM,qBAAqB,IACzB,2DACA;CACE,UAAU,EACR,OAAO;EACL,SAAS;EACT,WAAW;EACX,OAAO;EACP,SAAS;EACT,OAAO;EACP,MAAM;EACN,MAAM;CACR,EACF;CACA,iBAAiB,EACf,OAAO,UACT;AACF,CACF;AAEA,MAAM,mBAAmB,IACvB,mFACA;CACE,UAAU;EACR,OAAO;GACL,SAAS;GACT,WAAW;GACX,OAAO;GACP,SAAS;GACT,OAAO;GACP,MAAM;GACN,MAAM;EACR;EACA,aAAa;GACX,YAAY;GACZ,UAAU;EACZ;CACF;CACA,iBAAiB,EACf,aAAa,aACf;AACF,CACF;;;;;;;;;;;;;;;;;;AAkCA,MAAa,eAA+C,EAC1D,MACA,gBACA,YACA,QAAQ,WACR,YAAY,OACZ,cAAc,cACd,gBACyB;CACzB,MAAM,cAAc,OAAsB,CAAC,CAAC;CAC5C,MAAM,eAAe,OAA8B,IAAI;CACvD,MAAM,EAAE,yBAAyB,wBAAwB,gBACvD,aACA;EAAE,aAAa;EAAW;CAAY,CACxC;CAEA,gBAAgB;EACd,oBAAoB;CAEtB,GAAG,CAAC,cAAc,CAAC;CAEnB,OACE,qBAAC,OAAD;EACE,WAAW,GACT,mBAAmB,EACjB,MACF,CAAC,GACD,SACF;EACA,oBAAkB;EAClB,wBAAqB;EACrB,MAAK;YATP,CAWG,KAAK,KAAK,KAAK,UAAU;GACxB,MAAM,MAAM,IAAI;GAEhB,MAAM,aAAa,mBAAmB;GAEtC,OAAO,aAAa,KAAK;IACvB,KAAK,OAAO;IACZ,MAAM;IACN,UAAU,MAA+B;KACvC,IAAI,OAAO,UAAU,CAAC;KACtB,aAAa,GAAG;IAClB;IACA,iBAAiB;IACjB,eAAe;IACf,UAAU,aAAa,IAAI;IAC3B,MAAM,OAAoB;KACxB,YAAY,QAAQ,SAAS;IAC/B;GACF,CAAiB;EACnB,CAAC,GACA,2BACC,oBAAC,OAAD;GACE,WAAW,GACT,iBAAiB;IACf;IACA;GACF,CAAC,CACH;GACA,OAAO;GACP,KAAK;EAEN,GADM,GAAG,iBAAiB,KAAK,UAAU,KAAK,KAAK,QAAQ,IAAI,GAAG,CAAC,GACnE,CAEA;;AAET"}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { Button } from "../Button/Button.mjs";
|
|
4
4
|
import { Container } from "../Container/index.mjs";
|
|
5
5
|
import { DropDown } from "../DropDown/index.mjs";
|
|
6
|
-
import { useState } from "react";
|
|
7
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
7
|
import { CircleDashed, Moon, Sun } from "lucide-react";
|
|
9
8
|
|
|
@@ -13,20 +12,13 @@ const ButtonItem = ({ Icon, children, ...props }) => /* @__PURE__ */ jsx("div",
|
|
|
13
12
|
children: /* @__PURE__ */ jsx(Button, {
|
|
14
13
|
className: "w-full cursor-pointer rounded-lg p-1 text-left hover:bg-text/10 focus:bg-text-opposite/20 focus:outline-hidden disabled:text-white/25",
|
|
15
14
|
Icon,
|
|
16
|
-
"data-mode": "system",
|
|
17
15
|
role: "option",
|
|
18
16
|
variant: "none",
|
|
19
17
|
...props,
|
|
20
18
|
children
|
|
21
19
|
})
|
|
22
20
|
});
|
|
23
|
-
const DesktopThemeSwitcher = ({ theme, setTheme
|
|
24
|
-
const [mode, setMode] = useState(systemTheme === theme ? "system" : theme);
|
|
25
|
-
const switchMode = (mode) => {
|
|
26
|
-
if (mode === "system") setTheme(systemTheme ?? "light");
|
|
27
|
-
else setTheme(mode);
|
|
28
|
-
setMode(mode);
|
|
29
|
-
};
|
|
21
|
+
const DesktopThemeSwitcher = ({ theme, setTheme }) => {
|
|
30
22
|
const panelIdentifier = "theme-switcher";
|
|
31
23
|
return /* @__PURE__ */ jsxs(DropDown, {
|
|
32
24
|
identifier: panelIdentifier,
|
|
@@ -35,9 +27,9 @@ const DesktopThemeSwitcher = ({ theme, setTheme, systemTheme }) => {
|
|
|
35
27
|
identifier: panelIdentifier,
|
|
36
28
|
"aria-label": "Theme selector",
|
|
37
29
|
children: [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
theme === "system" && /* @__PURE__ */ jsx(CircleDashed, { "data-mode": "system" }),
|
|
31
|
+
theme === "light" && /* @__PURE__ */ jsx(Sun, { "data-mode": "light" }),
|
|
32
|
+
theme === "dark" && /* @__PURE__ */ jsx(Moon, { "data-mode": "dark" })
|
|
41
33
|
]
|
|
42
34
|
}), /* @__PURE__ */ jsx(DropDown.Panel, {
|
|
43
35
|
identifier: panelIdentifier,
|
|
@@ -49,22 +41,22 @@ const DesktopThemeSwitcher = ({ theme, setTheme, systemTheme }) => {
|
|
|
49
41
|
children: [
|
|
50
42
|
/* @__PURE__ */ jsx(ButtonItem, {
|
|
51
43
|
Icon: CircleDashed,
|
|
52
|
-
onClick: () =>
|
|
53
|
-
isActive:
|
|
44
|
+
onClick: () => setTheme("system"),
|
|
45
|
+
isActive: theme === "system",
|
|
54
46
|
label: "Restore to system mode",
|
|
55
47
|
children: "System"
|
|
56
48
|
}),
|
|
57
49
|
/* @__PURE__ */ jsx(ButtonItem, {
|
|
58
50
|
Icon: Sun,
|
|
59
|
-
onClick: () =>
|
|
60
|
-
isActive:
|
|
51
|
+
onClick: () => setTheme("light"),
|
|
52
|
+
isActive: theme === "light",
|
|
61
53
|
label: "Switch to light mode",
|
|
62
54
|
children: "Light"
|
|
63
55
|
}),
|
|
64
56
|
/* @__PURE__ */ jsx(ButtonItem, {
|
|
65
57
|
Icon: Moon,
|
|
66
|
-
onClick: () =>
|
|
67
|
-
isActive:
|
|
58
|
+
onClick: () => setTheme("dark"),
|
|
59
|
+
isActive: theme === "dark",
|
|
68
60
|
label: "Switch to dark mode",
|
|
69
61
|
children: "Dark"
|
|
70
62
|
})
|