@kolkrabbi/kol-component 0.33.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +2 -2
  2. package/src/atoms/Badge.jsx +6 -2
  3. package/src/atoms/ColorSwatch.jsx +1 -1
  4. package/src/atoms/Image.jsx +1 -1
  5. package/src/graphics/Graphic.jsx +1 -1
  6. package/src/index.js +16 -17
  7. package/src/molecules/BentoCard.jsx +1 -1
  8. package/src/molecules/ColorInputRow.jsx +1 -1
  9. package/src/molecules/Dropdown.jsx +1 -1
  10. package/src/{atoms → molecules}/EmptyState.jsx +6 -2
  11. package/src/molecules/FieldRow.jsx +1 -1
  12. package/src/molecules/MenuItem.jsx +4 -2
  13. package/src/molecules/ShapeDropdown.jsx +6 -2
  14. package/src/molecules/SplitToolButton.jsx +3 -2
  15. package/src/organisms/FeaturedCarousel.jsx +1 -1
  16. package/src/organisms/MediaLibrary.jsx +1 -1
  17. package/src/organisms/MediaViewer.jsx +1 -1
  18. package/src/organisms/RecordManager.jsx +1 -1
  19. package/src/atoms/InteractiveImage.jsx +0 -152
  20. /package/src/{atoms → molecules}/PaletteHarmonyWheel.jsx +0 -0
  21. /package/src/{atoms → utilities}/AsciiCursor.jsx +0 -0
  22. /package/src/{atoms → utilities}/AssetGrid.jsx +0 -0
  23. /package/src/{atoms → utilities}/AssetPlaceholder.jsx +0 -0
  24. /package/src/{molecules → utilities}/ButtonGroup.jsx +0 -0
  25. /package/src/{organisms → utilities}/EditorShell.jsx +0 -0
  26. /package/src/{molecules → utilities}/ErrorBoundary.jsx +0 -0
  27. /package/src/{atoms → utilities}/ExitPreview.jsx +0 -0
  28. /package/src/{molecules → utilities}/FullscreenOverlay.jsx +0 -0
  29. /package/src/{molecules → utilities}/LoaderOverlay.jsx +0 -0
  30. /package/src/{atoms → utilities}/OverlayGlassPanel.jsx +0 -0
  31. /package/src/{atoms → utilities}/Popover.jsx +0 -0
  32. /package/src/{atoms → utilities}/ProsePreview.jsx +0 -0
  33. /package/src/{atoms → utilities}/TiltCard.jsx +0 -0
  34. /package/src/{atoms → utilities}/TransparentX.jsx +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.33.1",
3
+ "version": "0.34.0",
4
4
  "description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  "@floating-ui/react": "^0.27.19",
25
25
  "embla-carousel-react": "^8.6.0",
26
26
  "react-syntax-highlighter": "^16.1.1",
27
- "@kolkrabbi/kol-icons": "^0.13.0"
27
+ "@kolkrabbi/kol-icons": "^0.14.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "framer-motion": "^12.0.0",
@@ -7,14 +7,18 @@
7
7
 
8
8
  import { Icon } from '@kolkrabbi/kol-icons'
9
9
 
10
+ /* Tone names mirror StatusChip's --ui-* ladder (error/warning/info/success —
11
+ * siblings, one language). `destructive` and `critical` are legacy aliases
12
+ * of `error`; canonical name in new call sites is `error`. */
10
13
  const VARIANT_MAP = {
11
14
  default: 'kol-badge-default',
12
15
  secondary: 'kol-badge-secondary',
13
- destructive: 'kol-badge-destructive',
16
+ error: 'kol-badge-error',
17
+ destructive: 'kol-badge-error',
18
+ critical: 'kol-badge-error',
14
19
  outline: 'kol-badge-outline',
15
20
  success: 'kol-badge-success',
16
21
  warning: 'kol-badge-warning',
17
- critical: 'kol-badge-critical',
18
22
  info: 'kol-badge-info'
19
23
  }
20
24
 
@@ -34,7 +34,7 @@
34
34
  * onClick — if provided, renders as <button>; else <span>.
35
35
  * title — passes through.
36
36
  */
37
- import TransparentX from './TransparentX'
37
+ import TransparentX from '../utilities/TransparentX'
38
38
 
39
39
  const SIZE_CLASSES = {
40
40
  fill: 'w-full aspect-square',
@@ -6,7 +6,7 @@
6
6
  * visible at its intended size (aspect-ratio preserved).
7
7
  */
8
8
  import { useState } from 'react'
9
- import AssetPlaceholder from './AssetPlaceholder'
9
+ import AssetPlaceholder from '../utilities/AssetPlaceholder'
10
10
 
11
11
  export default function Image({
12
12
  src,
@@ -12,7 +12,7 @@
12
12
  * than silently empty.
13
13
  */
14
14
  import { useEffect, useState } from 'react'
15
- import AssetPlaceholder from '../atoms/AssetPlaceholder.jsx'
15
+ import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
16
16
 
17
17
  let RAW = null // category → name → raw svg, once the chunk resolves
18
18
  let loadPromise = null
package/src/index.js CHANGED
@@ -17,8 +17,8 @@
17
17
 
18
18
  // atoms
19
19
  export { default as AnimatedTitle } from './atoms/AnimatedTitle.jsx'
20
- export { default as AssetGrid } from './atoms/AssetGrid.jsx'
21
- export { default as AssetPlaceholder } from './atoms/AssetPlaceholder.jsx'
20
+ export { default as AssetGrid } from './utilities/AssetGrid.jsx'
21
+ export { default as AssetPlaceholder } from './utilities/AssetPlaceholder.jsx'
22
22
  export { default as Avatar } from './atoms/Avatar.jsx'
23
23
  export { default as Badge } from './atoms/Badge.jsx'
24
24
  export { default as Button } from './atoms/Button.jsx'
@@ -27,19 +27,19 @@ export { default as CurveOverlay } from './atoms/CurveOverlay.jsx'
27
27
  export { default as Divider } from './atoms/Divider.jsx'
28
28
  export { default as DocsToc } from './molecules/DocsToc.jsx'
29
29
  export { default as DropdownTagFilter } from './molecules/DropdownTagFilter.jsx'
30
- export { default as EmptyState } from './atoms/EmptyState.jsx'
30
+ export { default as EmptyState } from './molecules/EmptyState.jsx'
31
31
  export { default as IconFrame } from './atoms/IconFrame.jsx'
32
- export { default as ExitPreview } from './atoms/ExitPreview.jsx'
32
+ export { default as ExitPreview } from './utilities/ExitPreview.jsx'
33
33
  export { default as Figure } from './atoms/Figure.jsx'
34
- export { default as FullscreenOverlay } from './molecules/FullscreenOverlay.jsx'
34
+ export { default as FullscreenOverlay } from './utilities/FullscreenOverlay.jsx'
35
35
  export { default as HlsVideo } from './atoms/HlsVideo.jsx'
36
36
  export { default as Input } from './atoms/Input.jsx'
37
37
  export { default as Label } from './atoms/Label.jsx'
38
38
  export { default as LabeledControl } from './molecules/LabeledControl.jsx'
39
- export { default as OverlayGlassPanel } from './atoms/OverlayGlassPanel.jsx'
39
+ export { default as OverlayGlassPanel } from './utilities/OverlayGlassPanel.jsx'
40
40
  export { default as Pill } from './atoms/Pill.jsx'
41
- export { usePopover, PopoverPanel, Tooltip } from './atoms/Popover.jsx'
42
- export { default as ProsePreview } from './atoms/ProsePreview.jsx'
41
+ export { usePopover, PopoverPanel, Tooltip } from './utilities/Popover.jsx'
42
+ export { default as ProsePreview } from './utilities/ProsePreview.jsx'
43
43
  export { default as QuantityInput } from './molecules/QuantityInput.jsx'
44
44
  export { default as RotaryDial } from './atoms/RotaryDial.jsx'
45
45
  export { default as SearchInput } from './molecules/SearchInput.jsx'
@@ -52,17 +52,16 @@ export { default as Textarea } from './atoms/Textarea.jsx'
52
52
  export { default as ToggleBracket } from './atoms/ToggleBracket.jsx'
53
53
  export { default as ToggleCheckbox } from './atoms/ToggleCheckbox.jsx'
54
54
  export { default as ToggleSwitch } from './atoms/ToggleSwitch.jsx'
55
- export { default as TiltCard } from './atoms/TiltCard.jsx'
56
- export { default as TransparentX } from './atoms/TransparentX.jsx'
55
+ export { default as TiltCard } from './utilities/TiltCard.jsx'
56
+ export { default as TransparentX } from './utilities/TransparentX.jsx'
57
57
  export { default as ViewToggle } from './atoms/ViewToggle.jsx'
58
58
 
59
59
  // molecules
60
60
  export { Accordion, AccordionPanel } from './molecules/Accordion.jsx'
61
- export { default as ButtonGroup } from './molecules/ButtonGroup.jsx'
61
+ export { default as ButtonGroup } from './utilities/ButtonGroup.jsx'
62
62
  /* monorepo sets (P6–P10) — molecule members */
63
63
  export { default as AlignmentGrid } from './molecules/AlignmentGrid.jsx'
64
64
  export { default as ImageBlock } from './molecules/ImageBlock.jsx'
65
- export { default as InteractiveImage } from './atoms/InteractiveImage.jsx'
66
65
  export { default as SelectionOverlay } from './atoms/SelectionOverlay.jsx'
67
66
  export { default as VideoBlock, getEmbedUrl } from './molecules/VideoBlock.jsx'
68
67
  export { default as CardFeatureItem } from './molecules/CardFeatureItem.jsx'
@@ -79,7 +78,7 @@ export { default as MediaRow } from './molecules/MediaRow.jsx'
79
78
  export { MenuItem, MenuDropdownItem, MenuDropdownDivider, MenuDropdownNest } from './molecules/MenuItem.jsx'
80
79
  export { MenuPopover } from './molecules/MenuPopover.jsx'
81
80
  export { ModalProvider, useModal } from './molecules/Modal.jsx'
82
- export { default as PaletteHarmonyWheel } from './atoms/PaletteHarmonyWheel.jsx'
81
+ export { default as PaletteHarmonyWheel } from './molecules/PaletteHarmonyWheel.jsx'
83
82
  export { default as PropertyInput } from './molecules/PropertyInput.jsx'
84
83
  export { default as ShapeDropdown } from './molecules/ShapeDropdown.jsx'
85
84
  export { default as ShellDrawer } from './molecules/ShellDrawer.jsx'
@@ -96,20 +95,20 @@ export { default as TabsRow } from './molecules/TabsRow.jsx'
96
95
  standalone @kolkrabbi/kol-foundry package (with the type-specimen kit +
97
96
  live-font effects moved there 2026-07-09) — never re-exported here. */
98
97
  export { default as Canvas, CanvasFrame, PanViewport, CANVAS_VIRTUAL_W, DEFAULT_ASPECTS, CANVAS_DEFAULTS } from './organisms/Canvas.jsx'
99
- export { default as EditorShell } from './organisms/EditorShell.jsx'
98
+ export { default as EditorShell } from './utilities/EditorShell.jsx'
100
99
  export { default as GalleryCarousel } from './organisms/GalleryCarousel.jsx'
101
- export { default as AsciiCursor } from './atoms/AsciiCursor.jsx'
100
+ export { default as AsciiCursor } from './utilities/AsciiCursor.jsx'
102
101
  export { default as BentoCard } from './molecules/BentoCard.jsx'
103
102
  export { default as Carousel } from './molecules/Carousel.jsx'
104
103
  export { default as ContentFilters } from './organisms/ContentFilters.jsx'
105
104
  export { default as CtaGlobal } from './organisms/CtaGlobal.jsx'
106
- export { default as ErrorBoundary } from './molecules/ErrorBoundary.jsx'
105
+ export { default as ErrorBoundary } from './utilities/ErrorBoundary.jsx'
107
106
  export { default as FeatureSplit } from './organisms/FeatureSplit.jsx'
108
107
  export { default as FeaturedCarousel } from './organisms/FeaturedCarousel.jsx'
109
108
  export { default as FeaturesCardSection } from './organisms/FeaturesCardSection.jsx'
110
109
  export { default as FoundryCTA } from './organisms/FoundryCTA.jsx'
111
110
  export { default as FullBleedHero } from './organisms/FullBleedHero.jsx'
112
- export { default as LoaderOverlay } from './molecules/LoaderOverlay.jsx'
111
+ export { default as LoaderOverlay } from './utilities/LoaderOverlay.jsx'
113
112
  export { default as MediaLibrary, MediaLibraryProvider, useMediaLibrary, MediaPicker, MediaBrowser } from './organisms/MediaLibrary.jsx'
114
113
  export { default as MediaTileGallery } from './organisms/MediaTileGallery.jsx'
115
114
  export { default as MediaViewer } from './organisms/MediaViewer.jsx'
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { motion } from 'framer-motion'
3
3
  import HlsVideo from '../atoms/HlsVideo.jsx'
4
- import AssetPlaceholder from '../atoms/AssetPlaceholder.jsx'
4
+ import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
5
5
  import Button from '../atoms/Button.jsx'
6
6
  import Image from '../atoms/Image.jsx'
7
7
  import useTilt from '../hooks/useTilt.js'
@@ -2,7 +2,7 @@ import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import ColorSwatch from '../atoms/ColorSwatch'
4
4
  import Input from '../atoms/Input'
5
- import { usePopover, PopoverPanel } from '../atoms/Popover'
5
+ import { usePopover, PopoverPanel } from '../utilities/Popover'
6
6
 
7
7
  /**
8
8
  * ColorInputRow — swatch chip + `#` hex input row. The single merged form of
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import { MenuDropdownItem } from './MenuItem.jsx'
4
- import { PopoverPanel, usePopover } from '../atoms/Popover.jsx'
4
+ import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
5
5
  import { indicatorSize } from '../hooks/glyphLadders.js'
6
6
 
7
7
  /**
@@ -13,10 +13,14 @@
13
13
  export default function EmptyState({ eyebrow, title, body, footer }) {
14
14
  return (
15
15
  <div>
16
+ {/* helper (line-height 1) is single-line chrome ONLY — title and footer
17
+ * can wrap, so they ride the line-height-bearing kol-mono-* scale
18
+ * (the type-conform fault line; user, 2026-08-09). Eyebrow stays
19
+ * helper: a one-line kicker. */}
16
20
  {eyebrow && <p className="kol-helper-10 text-meta mb-1">{eyebrow}</p>}
17
- {title && <p className="kol-helper-16 text-emphasis mb-3">{title}</p>}
21
+ {title && <p className="kol-mono-16 text-emphasis mb-3">{title}</p>}
18
22
  {body && <p className="kol-sans-body-03 text-body mb-4">{body}</p>}
19
- {footer && <p className="kol-helper-12 text-meta pt-3 border-t border-fg-08">{footer}</p>}
23
+ {footer && <p className="kol-mono-12 text-meta pt-3 border-t border-fg-08">{footer}</p>}
20
24
  </div>
21
25
  )
22
26
  }
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import Input from '../atoms/Input'
4
- import { usePopover, PopoverPanel } from '../atoms/Popover'
4
+ import { usePopover, PopoverPanel } from '../utilities/Popover'
5
5
  import Dropdown from './Dropdown'
6
6
 
7
7
  /**
@@ -1,6 +1,6 @@
1
1
  import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
- import { PopoverPanel, usePopover } from '../atoms/Popover.jsx'
3
+ import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
4
4
 
5
5
  /**
6
6
  * MenuItem — top-level menu entry. Trigger button + popover panel.
@@ -54,11 +54,13 @@ export function MenuItem({
54
54
  style={{ transform: open ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 200ms' }}
55
55
  />
56
56
  </button>
57
+ {/* w-max: floats size to CONTENT, never to the containing block —
58
+ * same family law as ShapeDropdown's panel (2026-08-09 review). */}
57
59
  <PopoverPanel
58
60
  popover={popover}
59
61
  panel={false}
60
62
  focus={false}
61
- className={`bg-surface-secondary rounded ${panelClassName}`}
63
+ className={`w-max bg-surface-secondary rounded ${panelClassName}`}
62
64
  style={panelStyle}
63
65
  >
64
66
  <div
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import Button from '../atoms/Button.jsx'
4
- import { PopoverPanel, usePopover } from '../atoms/Popover.jsx'
4
+ import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
5
5
  import { MenuDropdownItem } from './MenuItem.jsx'
6
6
 
7
7
  /**
@@ -68,11 +68,15 @@ const ShapeDropdown = ({ options = [], value, onChange, onAction, className = ''
68
68
  >
69
69
  <Button variant="ghost" size="sm" quiet iconOnly="chevron-down" iconSize={10} aria-label="Variants" />
70
70
  </span>
71
+ {/* w-max: a float sizes to its CONTENT (floating-ui contract) — without
72
+ * it the absolutely-positioned panel can stretch against its containing
73
+ * block and ignore both its rows and its trigger (2026-08-09 review,
74
+ * the shape menu spanning the whole card). */}
71
75
  <PopoverPanel
72
76
  popover={popover}
73
77
  panel={false}
74
78
  focus={false}
75
- className="bg-surface-secondary border border-fg-08 rounded shadow-lg"
79
+ className="w-max bg-surface-secondary border border-fg-08 rounded shadow-lg"
76
80
  >
77
81
  {options.map((option) => (
78
82
  <MenuDropdownItem
@@ -1,6 +1,6 @@
1
1
  import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
- import { PopoverPanel, usePopover } from '../atoms/Popover.jsx'
3
+ import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
4
4
 
5
5
  /**
6
6
  * SplitToolButton — single-trigger split tool button + variant menu (the
@@ -100,11 +100,12 @@ const SplitToolButton = ({
100
100
  {triggerVariant && <Icon name={triggerVariant.icon} size={14} />}
101
101
  <FoldIndicator />
102
102
  </button>
103
+ {/* w-max — floats size to content, the menu-family law (2026-08-09). */}
103
104
  <PopoverPanel
104
105
  popover={popover}
105
106
  panel={false}
106
107
  focus={false}
107
- className="bg-surface-secondary border border-fg-08 rounded shadow-lg"
108
+ className="w-max bg-surface-secondary border border-fg-08 rounded shadow-lg"
108
109
  >
109
110
  {variants.map((variant) => {
110
111
  const isActive = active && variant.id === value
@@ -2,7 +2,7 @@ import { isValidElement, useCallback, useEffect, useState } from 'react'
2
2
  import useEmblaCarousel from 'embla-carousel-react'
3
3
  import Image from '../atoms/Image.jsx'
4
4
  import HlsVideo from '../atoms/HlsVideo.jsx'
5
- import OverlayGlassPanel from '../atoms/OverlayGlassPanel.jsx'
5
+ import OverlayGlassPanel from '../utilities/OverlayGlassPanel.jsx'
6
6
 
7
7
  /**
8
8
  * Per-slide media layer: a `{ src, kind }` descriptor becomes a cover-fit
@@ -2,7 +2,7 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import Button from '../atoms/Button.jsx'
4
4
  import SegmentedToggle from '../atoms/SegmentedToggle.jsx'
5
- import FullscreenOverlay from '../molecules/FullscreenOverlay.jsx'
5
+ import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
6
6
  import MediaCard from '../molecules/MediaCard.jsx'
7
7
  import MediaRow from '../molecules/MediaRow.jsx'
8
8
  import ContentFilters from './ContentFilters.jsx'
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import useEmblaCarousel from 'embla-carousel-react'
3
3
  import { Icon } from '@kolkrabbi/kol-icons'
4
- import FullscreenOverlay from '../molecules/FullscreenOverlay.jsx'
4
+ import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
5
5
 
6
6
  /**
7
7
  * MediaViewer — THE fullscreen paged media viewer. One viewer for every
@@ -7,7 +7,7 @@ import ToggleCheckbox from '../atoms/ToggleCheckbox'
7
7
  import IconFrame from '../atoms/IconFrame'
8
8
  import ShellDrawer from '../molecules/ShellDrawer'
9
9
  import FieldRow, { StatusChip } from '../molecules/FieldRow'
10
- import { Tooltip } from '../atoms/Popover'
10
+ import { Tooltip } from '../utilities/Popover'
11
11
  import Table from './Table'
12
12
  import MediaLibrary from './MediaLibrary'
13
13
 
@@ -1,152 +0,0 @@
1
- import { useId, useLayoutEffect, useRef, useState } from 'react'
2
- import { motion } from 'framer-motion'
3
- import useTilt from '../hooks/useTilt.js'
4
- import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
5
- import useCoarsePointer from '../hooks/useCoarsePointer.js'
6
-
7
- /* The default mask: one closed blob authored in unit-square coordinates, so
8
- * it scales to any element size. Swap it via the `shape` prop — any path in
9
- * a 0..1 box works. */
10
- const BLOB =
11
- 'M.96.217L.855.834a.09.09 0 01-.07.072L.166.994A.09.09 0 01.06.9L.04.166A.09.09 0 01.15.073L.89.13a.09.09 0 01.07.087z'
12
-
13
- /**
14
- * InteractiveImage — an image seen through an organic blob mask that
15
- * re-centres on the cursor, floating over a blurred, scaled copy of
16
- * itself. The whole stage tilts in 3D toward the pointer.
17
- *
18
- * Composed, not forked: the tilt is the shared `useTilt` hook (springs,
19
- * not tweens — the DS has one tilt and this is it), so the motion here
20
- * matches TiltCard and BentoCard rather than introducing a second feel.
21
- * The source's gsap tweens are gone with it; nothing else needed gsap.
22
- *
23
- * Motion is gated twice — on coarse-pointer devices and under
24
- * prefers-reduced-motion the mask sits centred, the tilt never mounts,
25
- * and no listeners are attached.
26
- *
27
- * The two SVG ids are per-instance (`useId`). The monorepo source hard-coded
28
- * them at module scope, so a second instance on the same page silently
29
- * clobbered the first one's clip path and pattern — the reason this was
30
- * lobbied as a fresh effect rather than a migration.
31
- *
32
- * Purely image-driven: no tokens, no colors, nothing to theme. Size comes
33
- * from the consumer via `className` — the stage fills its box.
34
- *
35
- * @param {string} src image source, used by both the mask and the backdrop. Required
36
- * @param {string} alt accessible name for the masked image
37
- * @param {string} className classes on the root — supplies the size
38
- * @param {string} shape SVG path `d` in unit-square coords; the mask outline
39
- * @param {number} magnitude max tilt in degrees (±)
40
- * @param {number} perspective CSS transform perspective, in px
41
- * @param {number} blur backdrop blur radius, in px
42
- * @param {number} backdropScale backdrop zoom, as a multiplier — hides the blur's soft edge
43
- */
44
- export default function InteractiveImage({
45
- src,
46
- alt = '',
47
- className = '',
48
- shape = BLOB,
49
- magnitude = 10,
50
- perspective = 500,
51
- blur = 10,
52
- backdropScale = 1.1,
53
- }) {
54
- const uid = useId()
55
- const clipId = `kol-ii-clip-${uid}`
56
- const patternId = `kol-ii-pattern-${uid}`
57
-
58
- const reducedMotion = usePrefersReducedMotion()
59
- const coarse = useCoarsePointer()
60
- const still = reducedMotion || coarse
61
-
62
- const tilt = useTilt({ magnitude, perspective })
63
- const boxRef = useRef(null)
64
- const [size, setSize] = useState({ width: 0, height: 0 })
65
- /* Mask centre in element coordinates; null until the pointer arrives, which
66
- * is also the resting state — `centre` below reads it as dead centre. */
67
- const [point, setPoint] = useState(null)
68
-
69
- useLayoutEffect(() => {
70
- const el = boxRef.current
71
- if (!el) return undefined
72
- const ro = new ResizeObserver(([entry]) => {
73
- const { width, height } = entry.contentRect
74
- setSize({ width, height })
75
- })
76
- ro.observe(el)
77
- return () => ro.disconnect()
78
- }, [])
79
-
80
- const centre = point ?? { x: size.width / 2, y: size.height / 2 }
81
-
82
- /* The unit blob is scaled up to the element box, then offset so its centre
83
- * lands under the pointer rather than at the origin. */
84
- const maskTransform = `translate(${centre.x - size.width / 2} ${centre.y - size.height / 2}) scale(${size.width} ${size.height})`
85
-
86
- const handleMove = (e) => {
87
- if (still) return
88
- tilt.onMouseMove(e)
89
- const rect = boxRef.current?.getBoundingClientRect()
90
- if (!rect) return
91
- setPoint({ x: e.clientX - rect.left, y: e.clientY - rect.top })
92
- }
93
-
94
- const handleLeave = () => {
95
- if (still) return
96
- tilt.onMouseLeave()
97
- setPoint(null)
98
- }
99
-
100
- return (
101
- <div
102
- ref={boxRef}
103
- className={`relative ${className}`}
104
- onMouseMove={still ? undefined : handleMove}
105
- onMouseLeave={still ? undefined : handleLeave}
106
- >
107
- <div
108
- className="absolute inset-0 bg-cover bg-center"
109
- style={{
110
- backgroundImage: `url(${src})`,
111
- filter: `blur(${blur}px)`,
112
- transform: `scale(${backdropScale})`,
113
- }}
114
- />
115
-
116
- <motion.svg
117
- ref={tilt.ref}
118
- role="img"
119
- aria-label={alt || undefined}
120
- aria-hidden={alt ? undefined : true}
121
- className="relative w-full h-full"
122
- viewBox={`0 0 ${size.width} ${size.height}`}
123
- style={still ? undefined : tilt.style}
124
- >
125
- <defs>
126
- <clipPath id={clipId}>
127
- <path d={shape} transform={maskTransform} />
128
- </clipPath>
129
- <pattern
130
- id={patternId}
131
- patternUnits="userSpaceOnUse"
132
- width={size.width}
133
- height={size.height}
134
- >
135
- <image
136
- href={src}
137
- width="100%"
138
- height="100%"
139
- preserveAspectRatio="xMidYMid slice"
140
- />
141
- </pattern>
142
- </defs>
143
- <rect
144
- width="100%"
145
- height="100%"
146
- fill={`url(#${patternId})`}
147
- clipPath={`url(#${clipId})`}
148
- />
149
- </motion.svg>
150
- </div>
151
- )
152
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes