@intlayer/design-system 9.3.2 → 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/api/hooks/auth.mjs +2 -0
- package/dist/esm/api/hooks/auth.mjs.map +1 -1
- package/dist/esm/api/index.mjs +2 -2
- package/dist/esm/api/useAuth/useOAuth2.mjs +1 -1
- package/dist/esm/api/useAuth/useSession.mjs +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/Browser/Browser.mjs +1 -1
- package/dist/esm/components/Browser/Browser.mjs.map +1 -1
- package/dist/esm/components/ContentEditor/ContentEditorTextArea.mjs +1 -1
- package/dist/esm/components/CopyButton/index.mjs +2 -1
- package/dist/esm/components/CopyButton/index.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/ContentEditor.mjs +4 -5
- package/dist/esm/components/DictionaryFieldEditor/ContentEditor.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs +6 -10
- package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryCreationForm/DictionaryCreationForm.mjs +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.mjs +6 -6
- package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryFieldEditor.mjs +3 -7
- package/dist/esm/components/DictionaryFieldEditor/DictionaryFieldEditor.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/NavigationView/NavigationViewNode.mjs +1 -1
- package/dist/esm/components/DictionaryFieldEditor/SaveForm/SaveForm.mjs +2 -2
- package/dist/esm/components/DictionaryFieldEditor/StructureView/StructureView.mjs +1 -1
- package/dist/esm/components/ExpandCollapse/ExpandCollapse.mjs +19 -12
- 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/HTMLRender/HTMLRender.mjs +2 -2
- package/dist/esm/components/HTMLRender/HTMLRender.mjs.map +1 -1
- package/dist/esm/components/IDE/Code.mjs +3 -3
- package/dist/esm/components/IDE/Code.mjs.map +1 -1
- package/dist/esm/components/IDE/CodeBlockClient.mjs +16 -4
- package/dist/esm/components/IDE/CodeBlockClient.mjs.map +1 -1
- package/dist/esm/components/IDE/CodeBlockHighlight.mjs +14 -5
- package/dist/esm/components/IDE/CodeBlockHighlight.mjs.map +1 -1
- package/dist/esm/components/IDE/CodeBlockServer.mjs +5 -3
- package/dist/esm/components/IDE/CodeBlockServer.mjs.map +1 -1
- package/dist/esm/components/IDE/CodeBlockShiki.mjs +18 -18
- package/dist/esm/components/IDE/CodeBlockShiki.mjs.map +1 -1
- package/dist/esm/components/IDE/IDE.mjs +2 -5
- package/dist/esm/components/IDE/IDE.mjs.map +1 -1
- package/dist/esm/components/IDE/MarkDownRender.mjs +1 -2
- package/dist/esm/components/IDE/MarkDownRender.mjs.map +1 -1
- package/dist/esm/components/IDE/index.mjs +2 -2
- package/dist/esm/components/IDE/shikiThemes.mjs +18 -0
- package/dist/esm/components/IDE/shikiThemes.mjs.map +1 -0
- 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/MarkDownRender/MarkDownIframe.mjs +105 -4
- package/dist/esm/components/MarkDownRender/MarkDownIframe.mjs.map +1 -1
- package/dist/esm/components/MarkDownRender/MarkDownRender.mjs +16 -16
- package/dist/esm/components/MarkDownRender/MarkDownRender.mjs.map +1 -1
- package/dist/esm/components/MaxHeightSmoother/index.mjs +1 -1
- package/dist/esm/components/MaxHeightSmoother/index.mjs.map +1 -1
- package/dist/esm/components/MaxWidthSmoother/index.mjs +1 -1
- package/dist/esm/components/MaxWidthSmoother/index.mjs.map +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 +103 -17
- package/dist/esm/components/Pattern/GridDistortionPattern.mjs.map +1 -1
- package/dist/esm/components/Popover/dynamic.mjs +73 -28
- package/dist/esm/components/Popover/dynamic.mjs.map +1 -1
- package/dist/esm/components/Popover/index.mjs +2 -2
- package/dist/esm/components/Popover/static.mjs +47 -18
- package/dist/esm/components/Popover/static.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 +38 -17
- package/dist/esm/components/Tab/Tab.mjs.map +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/TextArea/AutoSizeTextArea.mjs +3 -2
- package/dist/esm/components/TextArea/AutoSizeTextArea.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/components/index.mjs +3 -3
- package/dist/esm/hooks/index.mjs +9 -9
- package/dist/esm/hooks/useDevice.mjs +16 -6
- package/dist/esm/hooks/useDevice.mjs.map +1 -1
- package/dist/esm/hooks/useItemSelector.mjs +110 -112
- 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/libs/auth.mjs +16 -3
- package/dist/esm/libs/auth.mjs.map +1 -1
- 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/structured-data/buildCreativeWorkJsonLd.mjs +1 -1
- package/dist/esm/structured-data/buildCreativeWorkJsonLd.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/api/hooks/auth.d.ts.map +1 -1
- package/dist/types/components/Browser/Browser.d.ts +7 -1
- package/dist/types/components/Browser/Browser.d.ts.map +1 -1
- package/dist/types/components/CopyButton/index.d.ts +9 -3
- package/dist/types/components/CopyButton/index.d.ts.map +1 -1
- package/dist/types/components/DictionaryFieldEditor/ContentEditor.d.ts +0 -1
- package/dist/types/components/DictionaryFieldEditor/ContentEditor.d.ts.map +1 -1
- package/dist/types/components/DictionaryFieldEditor/ContentEditorView/TextEditor.d.ts +0 -1
- package/dist/types/components/DictionaryFieldEditor/ContentEditorView/TextEditor.d.ts.map +1 -1
- package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.d.ts +0 -1
- package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.d.ts.map +1 -1
- package/dist/types/components/ExpandCollapse/ExpandCollapse.d.ts.map +1 -1
- package/dist/types/components/HTMLRender/HTMLRender.d.ts +1 -2
- package/dist/types/components/HTMLRender/HTMLRender.d.ts.map +1 -1
- package/dist/types/components/IDE/Code.d.ts +6 -1
- package/dist/types/components/IDE/Code.d.ts.map +1 -1
- package/dist/types/components/IDE/CodeBlockClient.d.ts +18 -2
- package/dist/types/components/IDE/CodeBlockClient.d.ts.map +1 -1
- package/dist/types/components/IDE/CodeBlockHighlight.d.ts +3 -2
- package/dist/types/components/IDE/CodeBlockHighlight.d.ts.map +1 -1
- package/dist/types/components/IDE/CodeBlockServer.d.ts +0 -1
- package/dist/types/components/IDE/CodeBlockServer.d.ts.map +1 -1
- package/dist/types/components/IDE/CodeBlockShiki.d.ts +0 -1
- package/dist/types/components/IDE/CodeBlockShiki.d.ts.map +1 -1
- package/dist/types/components/IDE/IDE.d.ts +0 -1
- package/dist/types/components/IDE/IDE.d.ts.map +1 -1
- package/dist/types/components/IDE/MarkDownRender.d.ts +0 -1
- package/dist/types/components/IDE/MarkDownRender.d.ts.map +1 -1
- package/dist/types/components/IDE/index.d.ts +2 -2
- package/dist/types/components/IDE/shikiThemes.d.ts +18 -0
- package/dist/types/components/IDE/shikiThemes.d.ts.map +1 -0
- package/dist/types/components/MarkDownRender/MarkDownIframe.d.ts.map +1 -1
- package/dist/types/components/MarkDownRender/MarkDownRender.d.ts +1 -2
- package/dist/types/components/MarkDownRender/MarkDownRender.d.ts.map +1 -1
- package/dist/types/components/Pattern/GridDistortionPattern.d.ts.map +1 -1
- package/dist/types/components/Popover/dynamic.d.ts.map +1 -1
- package/dist/types/components/Popover/index.d.ts +2 -2
- package/dist/types/components/Popover/static.d.ts +6 -1
- package/dist/types/components/Popover/static.d.ts.map +1 -1
- package/dist/types/components/Tab/Tab.d.ts +2 -2
- package/dist/types/components/Tab/Tab.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/components/index.d.ts +3 -3
- 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/libs/auth.d.ts +3 -0
- package/dist/types/libs/auth.d.ts.map +1 -1
- 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/structured-data/buildCreativeWorkJsonLd.d.ts +9 -3
- package/dist/types/structured-data/buildCreativeWorkJsonLd.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 +46 -41
- package/tailwind.css +27 -1
- 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":"index.mjs","names":[],"sources":["../../../../src/components/MaxHeightSmoother/index.tsx"],"sourcesContent":["import { cn } from '@utils/cn';\nimport type { CSSProperties, FC, HTMLAttributes, ReactNode } from 'react';\n\n/**\n * Props for the MaxHeightSmoother component\n */\ninterface MaxHeightSmootherProps extends HTMLAttributes<HTMLDivElement> {\n /** Content to render within the smoother container */\n children: ReactNode;\n /** Controls collapse state. When true, content is collapsed; when false, expanded; when undefined, relies on hover/focus behavior */\n isHidden?: boolean;\n /** Enable expand-on-hover behavior */\n isOverable?: boolean;\n /** Enable expand-on-focus behavior for accessibility and keyboard navigation */\n isFocusable?: boolean;\n /** Minimum height in pixels for the collapsed state */\n minHeight?: number;\n}\n\n/**\n * MaxHeightSmoother Component\n *\n * A sophisticated container component that provides smooth height transitions\n * for collapsible content. Implemented entirely in CSS — no client-side\n * JavaScript, hooks, or event listeners — making it compatible with React\n * Server Components and Next.js App Router without `'use client'`.\n *\n * @component\n * @example\n * Basic controlled usage:\n * ```tsx\n * const [isCollapsed, setIsCollapsed] = useState(true);\n *\n * <MaxHeightSmoother isHidden={isCollapsed}>\n * <div>Your collapsible content here</div>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Hover-triggered expansion:\n * ```tsx\n * <MaxHeightSmoother isOverable>\n * <p>This content expands when you hover over the container.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Focus-triggered expansion (keyboard accessible):\n * ```tsx\n * <MaxHeightSmoother isFocusable>\n * <p>Tab to focus this container to expand the content.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * With minimum height for preview:\n * ```tsx\n * <MaxHeightSmoother isOverable minHeight={100} className=\"border rounded-lg p-4\">\n * <h3>Article Preview</h3>\n * <p>This shows a preview of the content. Hover to see all.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Combined hover and focus:\n * ```tsx\n * <MaxHeightSmoother isOverable isFocusable minHeight={80}>\n * <h4>Interactive Card</h4>\n * <p>Expands on both hover and keyboard focus.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * ## Animation Strategies\n *\n * ### Strategy A — `grid-template-rows` (`minHeight === 0`)\n * Transitions between `grid-rows-[0fr]` and `grid-rows-[1fr]`.\n * Clean and performant; no fixed height ceiling required.\n * Requires `min-h-0` on the inner wrapper so the grid track can fully collapse.\n *\n * ### Strategy B — `max-height` via CSS custom properties (`minHeight > 0`)\n * When a visible preview floor is required, `grid-template-rows` produces a\n * \"dead time\" artifact: the track silently grows from 0 → minHeight before\n * anything visible happens, resulting in a perceived snap.\n *\n * `max-height` sidesteps this entirely — the transition starts from the\n * already-visible floor value, producing a single continuous expansion.\n *\n * The collapsed floor (`--mhs-collapsed`) and expanded ceiling\n * (`--mhs-expanded`) are injected as CSS variables via inline style,\n * NOT as `max-height` directly. This lets Tailwind own `max-height`\n * entirely, so `:hover` and `:focus-within` can override it freely\n * without fighting inline-style specificity.\n *\n * Both variables are consumer-overridable via CSS:\n * ```css\n * .my-container {\n * --mhs-collapsed: 80px;\n * --mhs-expanded: 1200px;\n * }\n * ```\n *\n * Trade-off: easing is applied over [minHeight → 3000px], not the real\n * content height. For typical content this is imperceptible. If pixel-perfect\n * easing is critical, use a JS-measured height with a controlled `isHidden`\n * prop and a `style={{ maxHeight }}` override instead.\n *\n * ## Interaction Modes\n *\n * 1. **Controlled** — Pass `isHidden` for external state control (e.g. accordion)\n * 2. **Hover** — Set `isOverable` for mouse hover expansion\n * 3. **Focus** — Set `isFocusable` for keyboard focus expansion\n * 4. **Combined** — Use `isOverable` and `isFocusable` together\n *\n * ## Accessibility\n *\n * - `role=\"button\"` and `tabIndex={0}` when `isFocusable` is true\n * - `aria-expanded` reflects the current disclosure state\n * - Focus expansion via `:focus-within` CSS pseudo-class\n * - `motion-reduce:transition-none` respects `prefers-reduced-motion`\n *\n * Note: because this component relies on CSS pseudo-classes for interaction,\n * keyboard toggle (Enter / Space) is not supported in uncontrolled mode.\n * For full keyboard toggle behaviour, manage `isHidden` externally and\n * wire an `onClick` / `onKeyDown` handler on the parent.\n *\n * ## Performance\n *\n * - Pure CSS animations — no JavaScript timers or DOM measurements\n * - No `'use client'` directive required\n * - Compatible with React Server Components and Next.js App Router\n * - GPU-accelerated transitions\n *\n * @param props - Component props extending HTML div attributes\n * @param props.children - Content to render within the container\n * @param props.isHidden - Controlled collapse state (`true` = collapsed, `false` = expanded)\n * @param props.isOverable - Enable hover-to-expand behaviour\n * @param props.isFocusable - Enable focus-to-expand behaviour with keyboard navigation\n * @param props.minHeight - Minimum height in pixels for the collapsed state (default: 0)\n * @param props.className - Additional CSS classes\n * @param props.style - Inline styles (CSS variable overrides will be merged)\n *\n * @returns A smooth height-transitioning container with configurable interaction modes\n */\nexport const MaxHeightSmoother: FC<MaxHeightSmootherProps> = ({\n children,\n isHidden,\n className = '',\n isOverable = false,\n isFocusable = false,\n minHeight = 0,\n ...props\n}) => (\n <div\n aria-hidden={isFocusable ? isHidden : undefined}\n tabIndex={isFocusable ? 0 : undefined}\n role={isFocusable ? 'button' : 'none'}\n className={cn(\n 'group/height-smoother relative grid w-full grid-rows-[0fr] overflow-hidden transition-
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/components/MaxHeightSmoother/index.tsx"],"sourcesContent":["import { cn } from '@utils/cn';\nimport type { CSSProperties, FC, HTMLAttributes, ReactNode } from 'react';\n\n/**\n * Props for the MaxHeightSmoother component\n */\ninterface MaxHeightSmootherProps extends HTMLAttributes<HTMLDivElement> {\n /** Content to render within the smoother container */\n children: ReactNode;\n /** Controls collapse state. When true, content is collapsed; when false, expanded; when undefined, relies on hover/focus behavior */\n isHidden?: boolean;\n /** Enable expand-on-hover behavior */\n isOverable?: boolean;\n /** Enable expand-on-focus behavior for accessibility and keyboard navigation */\n isFocusable?: boolean;\n /** Minimum height in pixels for the collapsed state */\n minHeight?: number;\n}\n\n/**\n * MaxHeightSmoother Component\n *\n * A sophisticated container component that provides smooth height transitions\n * for collapsible content. Implemented entirely in CSS — no client-side\n * JavaScript, hooks, or event listeners — making it compatible with React\n * Server Components and Next.js App Router without `'use client'`.\n *\n * @component\n * @example\n * Basic controlled usage:\n * ```tsx\n * const [isCollapsed, setIsCollapsed] = useState(true);\n *\n * <MaxHeightSmoother isHidden={isCollapsed}>\n * <div>Your collapsible content here</div>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Hover-triggered expansion:\n * ```tsx\n * <MaxHeightSmoother isOverable>\n * <p>This content expands when you hover over the container.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Focus-triggered expansion (keyboard accessible):\n * ```tsx\n * <MaxHeightSmoother isFocusable>\n * <p>Tab to focus this container to expand the content.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * With minimum height for preview:\n * ```tsx\n * <MaxHeightSmoother isOverable minHeight={100} className=\"border rounded-lg p-4\">\n * <h3>Article Preview</h3>\n * <p>This shows a preview of the content. Hover to see all.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Combined hover and focus:\n * ```tsx\n * <MaxHeightSmoother isOverable isFocusable minHeight={80}>\n * <h4>Interactive Card</h4>\n * <p>Expands on both hover and keyboard focus.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * ## Animation Strategies\n *\n * ### Strategy A — `grid-template-rows` (`minHeight === 0`)\n * Transitions between `grid-rows-[0fr]` and `grid-rows-[1fr]`.\n * Clean and performant; no fixed height ceiling required.\n * Requires `min-h-0` on the inner wrapper so the grid track can fully collapse.\n *\n * ### Strategy B — `max-height` via CSS custom properties (`minHeight > 0`)\n * When a visible preview floor is required, `grid-template-rows` produces a\n * \"dead time\" artifact: the track silently grows from 0 → minHeight before\n * anything visible happens, resulting in a perceived snap.\n *\n * `max-height` sidesteps this entirely — the transition starts from the\n * already-visible floor value, producing a single continuous expansion.\n *\n * The collapsed floor (`--mhs-collapsed`) and expanded ceiling\n * (`--mhs-expanded`) are injected as CSS variables via inline style,\n * NOT as `max-height` directly. This lets Tailwind own `max-height`\n * entirely, so `:hover` and `:focus-within` can override it freely\n * without fighting inline-style specificity.\n *\n * Both variables are consumer-overridable via CSS:\n * ```css\n * .my-container {\n * --mhs-collapsed: 80px;\n * --mhs-expanded: 1200px;\n * }\n * ```\n *\n * Trade-off: easing is applied over [minHeight → 3000px], not the real\n * content height. For typical content this is imperceptible. If pixel-perfect\n * easing is critical, use a JS-measured height with a controlled `isHidden`\n * prop and a `style={{ maxHeight }}` override instead.\n *\n * ## Interaction Modes\n *\n * 1. **Controlled** — Pass `isHidden` for external state control (e.g. accordion)\n * 2. **Hover** — Set `isOverable` for mouse hover expansion\n * 3. **Focus** — Set `isFocusable` for keyboard focus expansion\n * 4. **Combined** — Use `isOverable` and `isFocusable` together\n *\n * ## Accessibility\n *\n * - `role=\"button\"` and `tabIndex={0}` when `isFocusable` is true\n * - `aria-expanded` reflects the current disclosure state\n * - Focus expansion via `:focus-within` CSS pseudo-class\n * - `motion-reduce:transition-none` respects `prefers-reduced-motion`\n *\n * Note: because this component relies on CSS pseudo-classes for interaction,\n * keyboard toggle (Enter / Space) is not supported in uncontrolled mode.\n * For full keyboard toggle behaviour, manage `isHidden` externally and\n * wire an `onClick` / `onKeyDown` handler on the parent.\n *\n * ## Performance\n *\n * - Pure CSS animations — no JavaScript timers or DOM measurements\n * - No `'use client'` directive required\n * - Compatible with React Server Components and Next.js App Router\n * - GPU-accelerated transitions\n *\n * @param props - Component props extending HTML div attributes\n * @param props.children - Content to render within the container\n * @param props.isHidden - Controlled collapse state (`true` = collapsed, `false` = expanded)\n * @param props.isOverable - Enable hover-to-expand behaviour\n * @param props.isFocusable - Enable focus-to-expand behaviour with keyboard navigation\n * @param props.minHeight - Minimum height in pixels for the collapsed state (default: 0)\n * @param props.className - Additional CSS classes\n * @param props.style - Inline styles (CSS variable overrides will be merged)\n *\n * @returns A smooth height-transitioning container with configurable interaction modes\n */\nexport const MaxHeightSmoother: FC<MaxHeightSmootherProps> = ({\n children,\n isHidden,\n className = '',\n isOverable = false,\n isFocusable = false,\n minHeight = 0,\n ...props\n}) => (\n <div\n aria-hidden={isFocusable ? isHidden : undefined}\n tabIndex={isFocusable ? 0 : undefined}\n role={isFocusable ? 'button' : 'none'}\n className={cn(\n 'group/height-smoother relative grid w-full grid-rows-[0fr] overflow-hidden transition-[grid-template-rows] duration-700 ease-in-out',\n typeof isHidden !== 'undefined' &&\n !isHidden &&\n 'grid-rows-[1fr] overflow-x-auto',\n isOverable && 'hover:grid-rows-[1fr] hover:overflow-x-auto',\n isFocusable &&\n 'focus-within:grid-rows-[1fr] focus-within:overflow-x-auto focus:grid-rows-[1fr] focus:overflow-x-auto',\n className\n )}\n {...props}\n >\n <div\n style={{\n minHeight: `${minHeight}px`,\n }}\n className={cn(\n isOverable && 'group-hover/height-smoother:visible',\n isFocusable && 'group-focus/height-smoother:visible',\n className\n )}\n >\n {children}\n </div>\n </div>\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+IA,MAAa,qBAAiD,EAC5D,UACA,UACA,YAAY,IACZ,aAAa,OACb,cAAc,OACd,YAAY,GACZ,GAAG,YAEH,oBAAC,OAAD;CACE,eAAa,cAAc,WAAW;CACtC,UAAU,cAAc,IAAI;CAC5B,MAAM,cAAc,WAAW;CAC/B,WAAW,GACT,uIACA,OAAO,aAAa,eAClB,CAAC,YACD,mCACF,cAAc,+CACd,eACE,yGACF,SACF;CACA,GAAI;WAEJ,oBAAC,OAAD;EACE,OAAO,EACL,WAAW,GAAG,UAAU,IAC1B;EACA,WAAW,GACT,cAAc,uCACd,eAAe,uCACf,SACF;EAEC;CACE;AACF"}
|
|
@@ -5,7 +5,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
5
5
|
const MaxWidthSmoother = ({ children, isHidden, minWidth = 0, align = "left", isDisabled = false, className, ...props }) => {
|
|
6
6
|
const isCurrentlyHidden = isHidden && !isDisabled;
|
|
7
7
|
return /* @__PURE__ */ jsx("div", {
|
|
8
|
-
className: cn("relative grid h-full grid-cols-[0fr] overflow-x-hidden overflow-y-hidden transition-
|
|
8
|
+
className: cn("relative grid h-full grid-cols-[0fr] overflow-x-hidden overflow-y-hidden transition-[grid-template-columns] duration-500 ease-in-out", isCurrentlyHidden ? "" : "grid-cols-[1fr]", className),
|
|
9
9
|
"aria-hidden": isCurrentlyHidden,
|
|
10
10
|
inert: isCurrentlyHidden ? true : void 0,
|
|
11
11
|
...props,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/components/MaxWidthSmoother/index.tsx"],"sourcesContent":["import { cn } from '@utils/cn';\nimport type { HTMLAttributes } from 'react';\n\ntype Align = 'left' | 'right';\n\ntype MaxWidthSmootherProps = HTMLAttributes<HTMLDivElement> & {\n isHidden: boolean;\n minWidth?: number;\n align?: Align;\n isDisabled?: boolean;\n};\n\nexport const MaxWidthSmoother = ({\n children,\n isHidden,\n minWidth = 0,\n align = 'left',\n isDisabled = false,\n className,\n ...props\n}: MaxWidthSmootherProps) => {\n const isCurrentlyHidden = isHidden && !isDisabled;\n\n return (\n <div\n className={cn(\n 'relative grid h-full grid-cols-[0fr] overflow-x-hidden overflow-y-hidden transition-
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/components/MaxWidthSmoother/index.tsx"],"sourcesContent":["import { cn } from '@utils/cn';\nimport type { HTMLAttributes } from 'react';\n\ntype Align = 'left' | 'right';\n\ntype MaxWidthSmootherProps = HTMLAttributes<HTMLDivElement> & {\n isHidden: boolean;\n minWidth?: number;\n align?: Align;\n isDisabled?: boolean;\n};\n\nexport const MaxWidthSmoother = ({\n children,\n isHidden,\n minWidth = 0,\n align = 'left',\n isDisabled = false,\n className,\n ...props\n}: MaxWidthSmootherProps) => {\n const isCurrentlyHidden = isHidden && !isDisabled;\n\n return (\n <div\n className={cn(\n 'relative grid h-full grid-cols-[0fr] overflow-x-hidden overflow-y-hidden transition-[grid-template-columns] duration-500 ease-in-out',\n isCurrentlyHidden ? '' : 'grid-cols-[1fr]',\n className\n )}\n aria-hidden={isCurrentlyHidden}\n inert={isCurrentlyHidden ? true : undefined}\n {...props}\n >\n <div\n style={{\n minWidth: `${minWidth}px`,\n }}\n className={cn(align === 'right' && 'ml-auto')}\n >\n {children}\n </div>\n </div>\n );\n};\n"],"mappings":";;;;AAYA,MAAa,oBAAoB,EAC/B,UACA,UACA,WAAW,GACX,QAAQ,QACR,aAAa,OACb,WACA,GAAG,YACwB;CAC3B,MAAM,oBAAoB,YAAY,CAAC;CAEvC,OACE,oBAAC,OAAD;EACE,WAAW,GACT,wIACA,oBAAoB,KAAK,mBACzB,SACF;EACA,eAAa;EACb,OAAO,oBAAoB,OAAO;EAClC,GAAI;YAEJ,oBAAC,OAAD;GACE,OAAO,EACL,UAAU,GAAG,SAAS,IACxB;GACA,WAAW,GAAG,UAAU,WAAW,SAAS;GAE3C;EACE;CACF;AAET"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { cn } from "../../utils/cn.mjs";
|
|
4
|
-
import { useGetElementOrWindow } from "../../hooks/useGetElementOrWindow.mjs";
|
|
5
|
-
import { useScrollBlockage } from "../../hooks/useScrollBlockage/index.mjs";
|
|
6
4
|
import { Button } from "../Button/Button.mjs";
|
|
7
5
|
import { Container } from "../Container/index.mjs";
|
|
6
|
+
import { useGetElementOrWindow } from "../../hooks/useGetElementOrWindow.mjs";
|
|
7
|
+
import { useScrollBlockage } from "../../hooks/useScrollBlockage/index.mjs";
|
|
8
8
|
import { H3 } from "../Headers/index.mjs";
|
|
9
9
|
import { useEffect } from "react";
|
|
10
10
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { cn } from "../../utils/cn.mjs";
|
|
4
|
+
import { MaxHeightSmoother } from "../MaxHeightSmoother/index.mjs";
|
|
4
5
|
import { useScrollBlockage } from "../../hooks/useScrollBlockage/index.mjs";
|
|
5
6
|
import { useScrollDetection } from "../../hooks/useScrollDetection.mjs";
|
|
6
|
-
import { MaxHeightSmoother } from "../MaxHeightSmoother/index.mjs";
|
|
7
7
|
import { Burger } from "./Burger.mjs";
|
|
8
8
|
import { useRef, useState } from "react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { cn } from "../../utils/cn.mjs";
|
|
4
|
-
import { useItemSelector } from "../../hooks/useItemSelector.mjs";
|
|
5
4
|
import { Button } from "../Button/Button.mjs";
|
|
5
|
+
import { useItemSelector } from "../../hooks/useItemSelector.mjs";
|
|
6
6
|
import { useEffect, useRef } from "react";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { cva } from "class-variance-authority";
|
|
@@ -74,16 +74,16 @@ const Pagination = ({ currentPage, totalPages, onPageChange, showFirstLast = fal
|
|
|
74
74
|
const isLastPage = currentPage === totalPages;
|
|
75
75
|
const optionsRefs = useRef([]);
|
|
76
76
|
const indicatorRef = useRef(null);
|
|
77
|
-
const { choiceIndicatorPosition,
|
|
77
|
+
const { choiceIndicatorPosition, scheduleMeasurement } = useItemSelector(optionsRefs, {
|
|
78
78
|
selector,
|
|
79
79
|
isHoverable: true
|
|
80
80
|
});
|
|
81
81
|
useEffect(() => {
|
|
82
82
|
const timer = setTimeout(() => {
|
|
83
|
-
|
|
83
|
+
scheduleMeasurement();
|
|
84
84
|
}, 300);
|
|
85
85
|
return () => clearTimeout(timer);
|
|
86
|
-
}, [currentPage,
|
|
86
|
+
}, [currentPage, scheduleMeasurement]);
|
|
87
87
|
if (totalPages <= 1) return null;
|
|
88
88
|
const handlePageChange = (page) => {
|
|
89
89
|
if (!disabled && page >= 1 && page <= totalPages && page !== currentPage) onPageChange(page);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.mjs","names":[],"sources":["../../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client';\n\nimport { useItemSelector } from '@hooks/useItemSelector';\nimport { cn } from '@utils/cn';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';\nimport {\n type ComponentProps,\n type FC,\n type HTMLAttributes,\n useEffect,\n useRef,\n} from 'react';\nimport { useIntlayer } from 'react-intlayer';\nimport { Button } from '../Button';\n\nexport const paginationVariants = cva(\n 'flex items-center justify-center gap-1',\n {\n variants: {\n size: {\n sm: 'gap-1',\n md: 'gap-2',\n lg: 'gap-3',\n },\n color: {\n text: 'background-text',\n primary: 'background-primary',\n secondary: 'background-secondary',\n neutral: 'background-neutral',\n },\n variant: {\n default: '',\n bordered: 'rounded-lg border border-border p-2',\n ghost: 'bg-transparent',\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'default',\n },\n }\n);\n\nexport type PaginationSize = 'sm' | 'md' | 'lg';\n\nexport type PaginationVariant = 'default' | 'bordered' | 'ghost';\n\nexport type PaginationProps = HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof paginationVariants> & {\n currentPage: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n showFirstLast?: boolean;\n showPrevNext?: boolean;\n maxVisiblePages?: number;\n disabled?: boolean;\n };\n\nconst generatePageNumbers = (\n currentPage: number,\n totalPages: number,\n maxVisiblePages: number\n): (number | 'ellipsis')[] => {\n if (totalPages <= maxVisiblePages) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n\n const pages: (number | 'ellipsis')[] = [];\n const halfVisible = Math.floor(maxVisiblePages / 2);\n\n pages.push(1);\n\n if (currentPage <= halfVisible + 2) {\n for (let i = 2; i <= Math.min(maxVisiblePages - 1, totalPages - 1); i++) {\n pages.push(i);\n }\n if (totalPages > maxVisiblePages) {\n pages.push('ellipsis');\n }\n if (totalPages > 1) {\n pages.push(totalPages);\n }\n } else if (currentPage >= totalPages - halfVisible - 1) {\n if (totalPages > maxVisiblePages) {\n pages.push('ellipsis');\n }\n for (\n let i = Math.max(2, totalPages - maxVisiblePages + 2);\n i <= totalPages;\n i++\n ) {\n pages.push(i);\n }\n } else {\n pages.push('ellipsis');\n const start = currentPage - halfVisible;\n const end = currentPage + halfVisible;\n for (let i = start; i <= end; i++) {\n pages.push(i);\n }\n pages.push('ellipsis');\n pages.push(totalPages);\n }\n\n return pages;\n};\n\nconst selector = (option: HTMLElement) =>\n option?.getAttribute('aria-current') === 'true';\n\nconst getButtonSize = (size?: PaginationSize | `${PaginationSize}` | null) => {\n if (size === 'sm') {\n return 'icon-sm';\n } else if (size === 'lg') {\n return 'icon-lg';\n } else {\n return 'icon-md';\n }\n};\n\nconst InputIndicator: FC<ComponentProps<'div'>> = (props) => (\n <div\n className=\"absolute top-0 z-0 h-full w-auto rounded-xl bg-text/20 ring-4 ring-text/10 transition-[left,width] duration-300 ease-in-out [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-2xl motion-reduce:transition-none\"\n {...props}\n />\n);\n\nexport const Pagination: FC<PaginationProps> = ({\n currentPage,\n totalPages,\n onPageChange,\n showFirstLast = false,\n showPrevNext = true,\n maxVisiblePages = 5,\n disabled = false,\n size = 'md',\n variant = 'default',\n color = 'text',\n className,\n ...props\n}) => {\n const { goToNextPage, goToPreviousPage } = useIntlayer('pagination');\n\n const pageNumbers = generatePageNumbers(\n currentPage,\n totalPages,\n maxVisiblePages\n );\n\n const buttonSize = getButtonSize(size);\n const isFirstPage = currentPage === 1;\n const isLastPage = currentPage === totalPages;\n\n const optionsRefs = useRef<HTMLElement[]>([]);\n const indicatorRef = useRef<HTMLDivElement | null>(null);\n const { choiceIndicatorPosition, calculatePosition } = useItemSelector(\n optionsRefs,\n {\n selector,\n isHoverable: true,\n }\n );\n\n useEffect(() => {\n const timer = setTimeout(() => {\n calculatePosition();\n }, 300);\n\n return () => clearTimeout(timer);\n }, [currentPage, calculatePosition]);\n\n if (totalPages <= 1) return null;\n\n const handlePageChange = (page: number) => {\n if (!disabled && page >= 1 && page <= totalPages && page !== currentPage) {\n onPageChange(page);\n }\n };\n\n return (\n <div\n className={cn(paginationVariants({ size, variant }), className)}\n {...props}\n >\n <div className=\"relative flex items-center gap-1\">\n {choiceIndicatorPosition && (\n <InputIndicator style={choiceIndicatorPosition} ref={indicatorRef} />\n )}\n\n {showPrevNext && (\n <Button\n variant=\"outline\"\n size={buttonSize}\n color=\"text\"\n onClick={() => handlePageChange(currentPage - 1)}\n disabled={disabled || isFirstPage}\n label={goToPreviousPage.value}\n Icon={ChevronLeft}\n ref={(el) => {\n if (el) optionsRefs.current[0] = el;\n }}\n className=\"min-w-0 px-2\"\n />\n )}\n\n <div className=\"flex items-center gap-1 max-md:gap-0.5\">\n {pageNumbers.map((page, index) => {\n if (page === 'ellipsis') {\n const isFirstEllipsis = index === pageNumbers.indexOf('ellipsis');\n const ellipsisKey = isFirstEllipsis\n ? 'ellipsis-start'\n : 'ellipsis-end';\n return (\n <div\n key={ellipsisKey}\n className=\"flex h-8 min-w-8 items-center justify-center px-1\"\n >\n <MoreHorizontal className=\"h-4 w-4 text-muted-foreground\" />\n </div>\n );\n }\n\n const isActive = page === currentPage;\n // Calculate ref index: offset by 1 if showPrevNext, then count only non-ellipsis items\n const refIndex =\n (showPrevNext ? 1 : 0) +\n pageNumbers.slice(0, index).filter((p) => p !== 'ellipsis')\n .length;\n\n return (\n <Button\n key={page}\n variant={isActive ? 'default' : 'outline'}\n size={buttonSize}\n color=\"text\"\n onClick={() => handlePageChange(page)}\n disabled={disabled}\n label={`Go to page ${page}`}\n aria-current={isActive ? 'true' : 'false'}\n ref={(el) => {\n if (el) optionsRefs.current[refIndex] = el;\n }}\n className={cn(\n 'flex aspect-square h-8 w-8 min-w-0 items-center justify-center p-0 text-sm',\n size === 'sm' && 'h-6 w-6 text-xs',\n size === 'lg' && 'size-10 text-base',\n isActive && 'font-semibold'\n )}\n >\n {page}\n </Button>\n );\n })}\n </div>\n\n {showPrevNext && (\n <Button\n variant=\"outline\"\n size={buttonSize}\n color=\"text\"\n onClick={() => handlePageChange(currentPage + 1)}\n disabled={disabled || isLastPage}\n label={goToNextPage.value}\n Icon={ChevronRight}\n ref={(el) => {\n const lastRefIndex =\n (showPrevNext ? 1 : 0) +\n pageNumbers.filter((p) => p !== 'ellipsis').length;\n if (el) optionsRefs.current[lastRefIndex] = el;\n }}\n className=\"min-w-0 px-2\"\n />\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Pagination.mjs","names":[],"sources":["../../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client';\n\nimport { useItemSelector } from '@hooks/useItemSelector';\nimport { cn } from '@utils/cn';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';\nimport {\n type ComponentProps,\n type FC,\n type HTMLAttributes,\n useEffect,\n useRef,\n} from 'react';\nimport { useIntlayer } from 'react-intlayer';\nimport { Button } from '../Button';\n\nexport const paginationVariants = cva(\n 'flex items-center justify-center gap-1',\n {\n variants: {\n size: {\n sm: 'gap-1',\n md: 'gap-2',\n lg: 'gap-3',\n },\n color: {\n text: 'background-text',\n primary: 'background-primary',\n secondary: 'background-secondary',\n neutral: 'background-neutral',\n },\n variant: {\n default: '',\n bordered: 'rounded-lg border border-border p-2',\n ghost: 'bg-transparent',\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'default',\n },\n }\n);\n\nexport type PaginationSize = 'sm' | 'md' | 'lg';\n\nexport type PaginationVariant = 'default' | 'bordered' | 'ghost';\n\nexport type PaginationProps = HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof paginationVariants> & {\n currentPage: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n showFirstLast?: boolean;\n showPrevNext?: boolean;\n maxVisiblePages?: number;\n disabled?: boolean;\n };\n\nconst generatePageNumbers = (\n currentPage: number,\n totalPages: number,\n maxVisiblePages: number\n): (number | 'ellipsis')[] => {\n if (totalPages <= maxVisiblePages) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n\n const pages: (number | 'ellipsis')[] = [];\n const halfVisible = Math.floor(maxVisiblePages / 2);\n\n pages.push(1);\n\n if (currentPage <= halfVisible + 2) {\n for (let i = 2; i <= Math.min(maxVisiblePages - 1, totalPages - 1); i++) {\n pages.push(i);\n }\n if (totalPages > maxVisiblePages) {\n pages.push('ellipsis');\n }\n if (totalPages > 1) {\n pages.push(totalPages);\n }\n } else if (currentPage >= totalPages - halfVisible - 1) {\n if (totalPages > maxVisiblePages) {\n pages.push('ellipsis');\n }\n for (\n let i = Math.max(2, totalPages - maxVisiblePages + 2);\n i <= totalPages;\n i++\n ) {\n pages.push(i);\n }\n } else {\n pages.push('ellipsis');\n const start = currentPage - halfVisible;\n const end = currentPage + halfVisible;\n for (let i = start; i <= end; i++) {\n pages.push(i);\n }\n pages.push('ellipsis');\n pages.push(totalPages);\n }\n\n return pages;\n};\n\nconst selector = (option: HTMLElement) =>\n option?.getAttribute('aria-current') === 'true';\n\nconst getButtonSize = (size?: PaginationSize | `${PaginationSize}` | null) => {\n if (size === 'sm') {\n return 'icon-sm';\n } else if (size === 'lg') {\n return 'icon-lg';\n } else {\n return 'icon-md';\n }\n};\n\nconst InputIndicator: FC<ComponentProps<'div'>> = (props) => (\n <div\n className=\"absolute top-0 z-0 h-full w-auto rounded-xl bg-text/20 ring-4 ring-text/10 transition-[left,width] duration-300 ease-in-out [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-2xl motion-reduce:transition-none\"\n {...props}\n />\n);\n\nexport const Pagination: FC<PaginationProps> = ({\n currentPage,\n totalPages,\n onPageChange,\n showFirstLast = false,\n showPrevNext = true,\n maxVisiblePages = 5,\n disabled = false,\n size = 'md',\n variant = 'default',\n color = 'text',\n className,\n ...props\n}) => {\n const { goToNextPage, goToPreviousPage } = useIntlayer('pagination');\n\n const pageNumbers = generatePageNumbers(\n currentPage,\n totalPages,\n maxVisiblePages\n );\n\n const buttonSize = getButtonSize(size);\n const isFirstPage = currentPage === 1;\n const isLastPage = currentPage === totalPages;\n\n const optionsRefs = useRef<HTMLElement[]>([]);\n const indicatorRef = useRef<HTMLDivElement | null>(null);\n const { choiceIndicatorPosition, scheduleMeasurement } = useItemSelector(\n optionsRefs,\n {\n selector,\n isHoverable: true,\n }\n );\n\n useEffect(() => {\n const timer = setTimeout(() => {\n scheduleMeasurement();\n }, 300);\n\n return () => clearTimeout(timer);\n }, [currentPage, scheduleMeasurement]);\n\n if (totalPages <= 1) return null;\n\n const handlePageChange = (page: number) => {\n if (!disabled && page >= 1 && page <= totalPages && page !== currentPage) {\n onPageChange(page);\n }\n };\n\n return (\n <div\n className={cn(paginationVariants({ size, variant }), className)}\n {...props}\n >\n <div className=\"relative flex items-center gap-1\">\n {choiceIndicatorPosition && (\n <InputIndicator style={choiceIndicatorPosition} ref={indicatorRef} />\n )}\n\n {showPrevNext && (\n <Button\n variant=\"outline\"\n size={buttonSize}\n color=\"text\"\n onClick={() => handlePageChange(currentPage - 1)}\n disabled={disabled || isFirstPage}\n label={goToPreviousPage.value}\n Icon={ChevronLeft}\n ref={(el) => {\n if (el) optionsRefs.current[0] = el;\n }}\n className=\"min-w-0 px-2\"\n />\n )}\n\n <div className=\"flex items-center gap-1 max-md:gap-0.5\">\n {pageNumbers.map((page, index) => {\n if (page === 'ellipsis') {\n const isFirstEllipsis = index === pageNumbers.indexOf('ellipsis');\n const ellipsisKey = isFirstEllipsis\n ? 'ellipsis-start'\n : 'ellipsis-end';\n return (\n <div\n key={ellipsisKey}\n className=\"flex h-8 min-w-8 items-center justify-center px-1\"\n >\n <MoreHorizontal className=\"h-4 w-4 text-muted-foreground\" />\n </div>\n );\n }\n\n const isActive = page === currentPage;\n // Calculate ref index: offset by 1 if showPrevNext, then count only non-ellipsis items\n const refIndex =\n (showPrevNext ? 1 : 0) +\n pageNumbers.slice(0, index).filter((p) => p !== 'ellipsis')\n .length;\n\n return (\n <Button\n key={page}\n variant={isActive ? 'default' : 'outline'}\n size={buttonSize}\n color=\"text\"\n onClick={() => handlePageChange(page)}\n disabled={disabled}\n label={`Go to page ${page}`}\n aria-current={isActive ? 'true' : 'false'}\n ref={(el) => {\n if (el) optionsRefs.current[refIndex] = el;\n }}\n className={cn(\n 'flex aspect-square h-8 w-8 min-w-0 items-center justify-center p-0 text-sm',\n size === 'sm' && 'h-6 w-6 text-xs',\n size === 'lg' && 'size-10 text-base',\n isActive && 'font-semibold'\n )}\n >\n {page}\n </Button>\n );\n })}\n </div>\n\n {showPrevNext && (\n <Button\n variant=\"outline\"\n size={buttonSize}\n color=\"text\"\n onClick={() => handlePageChange(currentPage + 1)}\n disabled={disabled || isLastPage}\n label={goToNextPage.value}\n Icon={ChevronRight}\n ref={(el) => {\n const lastRefIndex =\n (showPrevNext ? 1 : 0) +\n pageNumbers.filter((p) => p !== 'ellipsis').length;\n if (el) optionsRefs.current[lastRefIndex] = el;\n }}\n className=\"min-w-0 px-2\"\n />\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useTheme } from "../../providers/ThemeProvider.mjs";
|
|
3
4
|
import { cn } from "../../utils/cn.mjs";
|
|
4
|
-
import { useIsDarkMode } from "../../hooks/useIsDarkMode.mjs";
|
|
5
5
|
import { useEffect, useRef } from "react";
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
7
|
|
|
@@ -105,31 +105,56 @@ const buildSegmentAttrs = (startX, startY, endX, endY, mouseX, mouseY, mouseActi
|
|
|
105
105
|
strokeWidth
|
|
106
106
|
};
|
|
107
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* Advances every point one step toward its target and reports how far the
|
|
110
|
+
* furthest one travelled.
|
|
111
|
+
*
|
|
112
|
+
* The caller uses that distance to decide whether another frame is worth
|
|
113
|
+
* running: the relaxation is exponential, so once it drops below
|
|
114
|
+
* {@link SETTLE_THRESHOLD_PX} the grid is visually at rest and redrawing it
|
|
115
|
+
* only burns the main thread.
|
|
116
|
+
*
|
|
117
|
+
* @returns Largest per-axis distance any point moved this step, in pixels.
|
|
118
|
+
*/
|
|
108
119
|
const updateGridPoints = (grid, mouseX, mouseY, mouseActive, radius, strength) => {
|
|
109
120
|
const rows = grid.length;
|
|
110
121
|
const columns = grid[0].length;
|
|
122
|
+
let largestMovement = 0;
|
|
111
123
|
for (let rowIndex = 0; rowIndex < rows; rowIndex++) for (let columnIndex = 0; columnIndex < columns; columnIndex++) {
|
|
112
124
|
const point = grid[rowIndex][columnIndex];
|
|
125
|
+
const previousX = point.x;
|
|
126
|
+
const previousY = point.y;
|
|
113
127
|
if (!mouseActive) {
|
|
114
128
|
point.x += (point.baseX - point.x) * .08;
|
|
115
129
|
point.y += (point.baseY - point.y) * .08;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const deltaX = mouseX - point.baseX;
|
|
119
|
-
const deltaY = mouseY - point.baseY;
|
|
120
|
-
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
121
|
-
if (distance < radius && distance > 0) {
|
|
122
|
-
const displacement = (Math.cos(distance / radius * Math.PI) + 1) / 2 * strength * radius * .3;
|
|
123
|
-
point.x += (point.baseX + deltaX / distance * displacement - point.x) * .12;
|
|
124
|
-
point.y += (point.baseY + deltaY / distance * displacement - point.y) * .12;
|
|
125
130
|
} else {
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
const deltaX = mouseX - point.baseX;
|
|
132
|
+
const deltaY = mouseY - point.baseY;
|
|
133
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
134
|
+
if (distance < radius && distance > 0) {
|
|
135
|
+
const displacement = (Math.cos(distance / radius * Math.PI) + 1) / 2 * strength * radius * .3;
|
|
136
|
+
point.x += (point.baseX + deltaX / distance * displacement - point.x) * .12;
|
|
137
|
+
point.y += (point.baseY + deltaY / distance * displacement - point.y) * .12;
|
|
138
|
+
} else {
|
|
139
|
+
point.x += (point.baseX - point.x) * .08;
|
|
140
|
+
point.y += (point.baseY - point.y) * .08;
|
|
141
|
+
}
|
|
128
142
|
}
|
|
143
|
+
const movement = Math.max(Math.abs(point.x - previousX), Math.abs(point.y - previousY));
|
|
144
|
+
if (movement > largestMovement) largestMovement = movement;
|
|
129
145
|
}
|
|
146
|
+
return largestMovement;
|
|
130
147
|
};
|
|
148
|
+
/**
|
|
149
|
+
* Per-frame movement below which the grid counts as at rest, in pixels.
|
|
150
|
+
*
|
|
151
|
+
* Well under a device pixel, so the frame that crosses it is the last one that
|
|
152
|
+
* could have changed anything on screen.
|
|
153
|
+
*/
|
|
154
|
+
const SETTLE_THRESHOLD_PX = .05;
|
|
131
155
|
const GridDistortionPattern = ({ width = 70, height = 70, radius = 280, strength = .12, lineColor, highlightColor, className, ...props }) => {
|
|
132
|
-
const
|
|
156
|
+
const { resolvedTheme } = useTheme();
|
|
157
|
+
const theme = resolvedTheme === "dark" ? DARK_THEME : LIGHT_THEME;
|
|
133
158
|
const resolvedLineColor = lineColor ?? theme.lineColor;
|
|
134
159
|
const resolvedHighlightColor = highlightColor ?? theme.highlightColor;
|
|
135
160
|
const svgRef = useRef(null);
|
|
@@ -139,6 +164,19 @@ const GridDistortionPattern = ({ width = 70, height = 70, radius = 280, strength
|
|
|
139
164
|
active: false
|
|
140
165
|
});
|
|
141
166
|
const points = useRef([]);
|
|
167
|
+
/**
|
|
168
|
+
* Bumped on every pointer move. The loop records the value it rendered so it
|
|
169
|
+
* can tell "the pointer is somewhere over the page but has not moved" — which
|
|
170
|
+
* looks identical frame to frame — from a genuine change.
|
|
171
|
+
*/
|
|
172
|
+
const pointerMoveCount = useRef(0);
|
|
173
|
+
/**
|
|
174
|
+
* Restarts the animation loop after it has parked itself.
|
|
175
|
+
*
|
|
176
|
+
* Assigned by the animation effect; the resize, colour and pointer effects
|
|
177
|
+
* call it so a change they make is picked up even when the grid was at rest.
|
|
178
|
+
*/
|
|
179
|
+
const wakeLoop = useRef(() => void 0);
|
|
142
180
|
const radiusRef = useRef(radius);
|
|
143
181
|
const strengthRef = useRef(strength);
|
|
144
182
|
const lineRgbRef = useRef(FALLBACK_RGB);
|
|
@@ -154,6 +192,7 @@ const GridDistortionPattern = ({ width = 70, height = 70, radius = 280, strength
|
|
|
154
192
|
if (!svg) return;
|
|
155
193
|
lineRgbRef.current = resolveColorToRgb(resolvedLineColor, svg);
|
|
156
194
|
highlightRgbRef.current = resolveColorToRgb(resolvedHighlightColor, svg);
|
|
195
|
+
wakeLoop.current();
|
|
157
196
|
}, [resolvedLineColor, resolvedHighlightColor]);
|
|
158
197
|
useEffect(() => {
|
|
159
198
|
const svg = svgRef.current;
|
|
@@ -175,6 +214,7 @@ const GridDistortionPattern = ({ width = 70, height = 70, radius = 280, strength
|
|
|
175
214
|
newGrid.push(row);
|
|
176
215
|
}
|
|
177
216
|
points.current = newGrid;
|
|
217
|
+
wakeLoop.current();
|
|
178
218
|
};
|
|
179
219
|
resize();
|
|
180
220
|
const resizeObserver = new ResizeObserver(resize);
|
|
@@ -191,9 +231,13 @@ const GridDistortionPattern = ({ width = 70, height = 70, radius = 280, strength
|
|
|
191
231
|
y: event.clientY - rect.top,
|
|
192
232
|
active: true
|
|
193
233
|
};
|
|
234
|
+
pointerMoveCount.current++;
|
|
235
|
+
wakeLoop.current();
|
|
194
236
|
};
|
|
195
237
|
const handlePointerLeave = () => {
|
|
196
238
|
mouse.current.active = false;
|
|
239
|
+
pointerMoveCount.current++;
|
|
240
|
+
wakeLoop.current();
|
|
197
241
|
};
|
|
198
242
|
window.addEventListener("pointermove", handlePointerMove, { passive: true });
|
|
199
243
|
window.addEventListener("pointerleave", handlePointerLeave, { passive: true });
|
|
@@ -202,11 +246,39 @@ const GridDistortionPattern = ({ width = 70, height = 70, radius = 280, strength
|
|
|
202
246
|
window.removeEventListener("pointerleave", handlePointerLeave);
|
|
203
247
|
};
|
|
204
248
|
}, []);
|
|
249
|
+
/**
|
|
250
|
+
* Runs the grid only while it has something to show.
|
|
251
|
+
*
|
|
252
|
+
* Each frame rewrites four attributes on every segment — on a full-viewport
|
|
253
|
+
* grid that is close to nine thousand `setAttribute` calls. Left running
|
|
254
|
+
* unconditionally it saturated the main thread of any page embedding the
|
|
255
|
+
* pattern, which starved React's transition lanes and made every other
|
|
256
|
+
* scroll-driven animation on the page advance in visible steps.
|
|
257
|
+
*
|
|
258
|
+
* So the loop parks itself whenever the grid is at rest and the pointer has
|
|
259
|
+
* not moved, and while the SVG is scrolled out of view. Anything that can
|
|
260
|
+
* change the picture — a pointer move, a resize, a colour change, the element
|
|
261
|
+
* coming back on screen — wakes it through {@link wakeLoop}.
|
|
262
|
+
*/
|
|
205
263
|
useEffect(() => {
|
|
206
264
|
const svg = svgRef.current;
|
|
207
265
|
if (!svg) return;
|
|
208
266
|
const pathPool = [];
|
|
209
267
|
let frame = 0;
|
|
268
|
+
let isRunning = false;
|
|
269
|
+
let isOnScreen = true;
|
|
270
|
+
/** Value of `pointerMoveCount` the last rendered frame was drawn for. */
|
|
271
|
+
let renderedPointerMoveCount = -1;
|
|
272
|
+
const start = () => {
|
|
273
|
+
if (isRunning || !isOnScreen) return;
|
|
274
|
+
isRunning = true;
|
|
275
|
+
frame = requestAnimationFrame(tick);
|
|
276
|
+
};
|
|
277
|
+
const stop = () => {
|
|
278
|
+
if (!isRunning) return;
|
|
279
|
+
isRunning = false;
|
|
280
|
+
cancelAnimationFrame(frame);
|
|
281
|
+
};
|
|
210
282
|
const ensurePathCount = (count) => {
|
|
211
283
|
while (pathPool.length < count) {
|
|
212
284
|
const el = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
@@ -219,9 +291,10 @@ const GridDistortionPattern = ({ width = 70, height = 70, radius = 280, strength
|
|
|
219
291
|
const tick = () => {
|
|
220
292
|
const grid = points.current;
|
|
221
293
|
if (!grid.length) {
|
|
222
|
-
|
|
294
|
+
isRunning = false;
|
|
223
295
|
return;
|
|
224
296
|
}
|
|
297
|
+
const pointerMoveCountAtFrameStart = pointerMoveCount.current;
|
|
225
298
|
const { x: mouseX, y: mouseY, active: mouseActive } = mouse.current;
|
|
226
299
|
const currentRadius = radiusRef.current;
|
|
227
300
|
const currentStrength = strengthRef.current;
|
|
@@ -229,7 +302,11 @@ const GridDistortionPattern = ({ width = 70, height = 70, radius = 280, strength
|
|
|
229
302
|
const highlightRgb = highlightRgbRef.current;
|
|
230
303
|
const baseOpacity = baseOpacityRef.current;
|
|
231
304
|
const highlightOpacityBoost = highlightOpacityBoostRef.current;
|
|
232
|
-
updateGridPoints(grid, mouseX, mouseY, mouseActive, currentRadius, currentStrength)
|
|
305
|
+
if (updateGridPoints(grid, mouseX, mouseY, mouseActive, currentRadius, currentStrength) < SETTLE_THRESHOLD_PX && renderedPointerMoveCount === pointerMoveCountAtFrameStart) {
|
|
306
|
+
isRunning = false;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
renderedPointerMoveCount = pointerMoveCountAtFrameStart;
|
|
233
310
|
const rows = grid.length;
|
|
234
311
|
const columns = grid[0].length;
|
|
235
312
|
const totalSegments = rows * (columns - 1) + columns * (rows - 1);
|
|
@@ -257,9 +334,18 @@ const GridDistortionPattern = ({ width = 70, height = 70, radius = 280, strength
|
|
|
257
334
|
}
|
|
258
335
|
frame = requestAnimationFrame(tick);
|
|
259
336
|
};
|
|
260
|
-
|
|
337
|
+
wakeLoop.current = start;
|
|
338
|
+
const visibilityObserver = new IntersectionObserver(([entry]) => {
|
|
339
|
+
isOnScreen = entry?.isIntersecting ?? true;
|
|
340
|
+
if (isOnScreen) start();
|
|
341
|
+
else stop();
|
|
342
|
+
});
|
|
343
|
+
visibilityObserver.observe(svg);
|
|
344
|
+
start();
|
|
261
345
|
return () => {
|
|
262
|
-
|
|
346
|
+
wakeLoop.current = () => void 0;
|
|
347
|
+
visibilityObserver.disconnect();
|
|
348
|
+
stop();
|
|
263
349
|
for (const el of pathPool) el.remove();
|
|
264
350
|
};
|
|
265
351
|
}, []);
|
|
@@ -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\nconst updateGridPoints = (\n grid: Point[][],\n mouseX: number,\n mouseY: number,\n mouseActive: boolean,\n radius: number,\n strength: number\n): void => {\n const rows = grid.length;\n const columns = grid[0]!.length;\n\n for (let rowIndex = 0; rowIndex < rows; rowIndex++) {\n for (let columnIndex = 0; columnIndex < columns; columnIndex++) {\n const point = grid[rowIndex]![columnIndex]!;\n if (!mouseActive) {\n point.x += (point.baseX - point.x) * 0.08;\n point.y += (point.baseY - point.y) * 0.08;\n continue;\n }\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};\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 // 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 }, [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 };\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 };\n const handlePointerLeave = () => {\n mouse.current.active = false;\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 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\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 frame = requestAnimationFrame(tick);\n return;\n }\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 updateGridPoints(\n grid,\n mouseX,\n mouseY,\n mouseActive,\n currentRadius,\n currentStrength\n );\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 tick();\n\n return () => {\n cancelAnimationFrame(frame);\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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"}
|