@kolkrabbi/kol-component 0.33.1 → 0.35.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 +12 -0
- package/package.json +8 -3
- package/src/atoms/Badge.jsx +6 -2
- package/src/atoms/ColorSwatch.jsx +20 -8
- package/src/atoms/Image.jsx +1 -1
- package/src/atoms/Input.jsx +8 -0
- package/src/atoms/Textarea.jsx +24 -4
- package/src/graphics/Graphic.jsx +1 -1
- package/src/index.js +16 -17
- package/src/molecules/BentoCard.jsx +1 -1
- package/src/molecules/ColorInputRow.jsx +5 -3
- package/src/molecules/Dropdown.jsx +1 -1
- package/src/{atoms → molecules}/EmptyState.jsx +6 -2
- package/src/molecules/FieldRow.jsx +3 -2
- package/src/molecules/MenuItem.jsx +11 -4
- package/src/molecules/Modal.jsx +25 -7
- package/src/molecules/ShapeDropdown.jsx +6 -2
- package/src/molecules/SplitToolButton.jsx +3 -2
- package/src/organisms/FeaturedCarousel.jsx +1 -1
- package/src/organisms/MediaLibrary.jsx +1 -1
- package/src/organisms/MediaViewer.jsx +1 -1
- package/src/organisms/RecordManager.jsx +2 -2
- package/src/atoms/InteractiveImage.jsx +0 -152
- /package/src/{atoms → molecules}/PaletteHarmonyWheel.jsx +0 -0
- /package/src/{atoms → utilities}/AsciiCursor.jsx +0 -0
- /package/src/{atoms → utilities}/AssetGrid.jsx +0 -0
- /package/src/{atoms → utilities}/AssetPlaceholder.jsx +0 -0
- /package/src/{molecules → utilities}/ButtonGroup.jsx +0 -0
- /package/src/{organisms → utilities}/EditorShell.jsx +0 -0
- /package/src/{molecules → utilities}/ErrorBoundary.jsx +0 -0
- /package/src/{atoms → utilities}/ExitPreview.jsx +0 -0
- /package/src/{molecules → utilities}/FullscreenOverlay.jsx +0 -0
- /package/src/{molecules → utilities}/LoaderOverlay.jsx +0 -0
- /package/src/{atoms → utilities}/OverlayGlassPanel.jsx +0 -0
- /package/src/{atoms → utilities}/Popover.jsx +0 -0
- /package/src/{atoms → utilities}/ProsePreview.jsx +0 -0
- /package/src/{atoms → utilities}/TiltCard.jsx +0 -0
- /package/src/{atoms → utilities}/TransparentX.jsx +0 -0
package/README.md
CHANGED
|
@@ -30,3 +30,15 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Atoms (Button, Input, Slider, Toggle\*, …), molecules (Dropdown, Tag, Badge, Modal, Popover, …), primitives (Accordion, Carousel, CodeBlock, Image, …), an organism (Table), graphics, and hooks (`useReveal`, `useScrollSpy`). See the [usage reference](https://github.com/Tor-Grimsson/kol-ds/tree/main/docs/usage) for real examples of each.
|
|
33
|
+
|
|
34
|
+
### Deep imports — skip the barrel, skip the peers
|
|
35
|
+
|
|
36
|
+
The barrel (`import { Button } from '@kolkrabbi/kol-component'`) statically imports the whole tree, so building against it requires every organism peer (`framer-motion`, `gsap`, `hls.js`) even if you render none of them — module resolution happens before tree-shaking. Consumers that want a slice import the file directly and only its own chain resolves:
|
|
37
|
+
|
|
38
|
+
```jsx
|
|
39
|
+
import Button from '@kolkrabbi/kol-component/atoms/Button'
|
|
40
|
+
import Modal from '@kolkrabbi/kol-component/molecules/Modal'
|
|
41
|
+
import useReveal from '@kolkrabbi/kol-component/hooks/useReveal'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Subpaths: `./atoms/*`, `./molecules/*`, `./organisms/*`, `./utilities/*` (`.jsx`) and `./hooks/*` (`.js`). Most files default-export their component; multi-part families (`Modal`, `MenuItem`, `Accordion`) export named — mirror whatever the barrel re-exports. The barrel stays for showcase-style consumers that want everything.
|
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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",
|
|
7
7
|
"main": "./src/index.js",
|
|
8
8
|
"module": "./src/index.js",
|
|
9
9
|
"exports": {
|
|
10
|
-
".": "./src/index.js"
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./atoms/*": "./src/atoms/*.jsx",
|
|
12
|
+
"./molecules/*": "./src/molecules/*.jsx",
|
|
13
|
+
"./organisms/*": "./src/organisms/*.jsx",
|
|
14
|
+
"./utilities/*": "./src/utilities/*.jsx",
|
|
15
|
+
"./hooks/*": "./src/hooks/*.js"
|
|
11
16
|
},
|
|
12
17
|
"files": [
|
|
13
18
|
"src",
|
|
@@ -24,7 +29,7 @@
|
|
|
24
29
|
"@floating-ui/react": "^0.27.19",
|
|
25
30
|
"embla-carousel-react": "^8.6.0",
|
|
26
31
|
"react-syntax-highlighter": "^16.1.1",
|
|
27
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
32
|
+
"@kolkrabbi/kol-icons": "^0.14.0"
|
|
28
33
|
},
|
|
29
34
|
"peerDependencies": {
|
|
30
35
|
"framer-motion": "^12.0.0",
|
package/src/atoms/Badge.jsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
* hex — color string, e.g. '#FF6F00'. Ignored if showTransparent.
|
|
12
12
|
* selected — adds active border + ring (border-fg-64 ring-1).
|
|
13
13
|
* size — number (px) for fixed-size, 'fill' (w-full aspect-square,
|
|
14
|
-
* for grid cells that should stay square),
|
|
15
|
-
* (w-full h-full, for grid cells that stretch to row height)
|
|
16
|
-
*
|
|
14
|
+
* for grid cells that should stay square), 'stretch'
|
|
15
|
+
* (w-full h-full, for grid cells that stretch to row height),
|
|
16
|
+
* or 'control-sm' (26px — the kol-control-sm row height, so
|
|
17
|
+
* a swatch sits flush beside sm input chrome in a paint bar;
|
|
18
|
+
* ColorSwatchFieldSizing 2026-08-12). Default 24.
|
|
17
19
|
* radius — 'sm' (4px, default) | 'tight' (2px) | 'none' | 'full' (circle).
|
|
20
|
+
* Default was 'tight' until 2026-08-12 — flipped to 'sm' per
|
|
21
|
+
* the system radius law (containers are 4px everywhere);
|
|
22
|
+
* square-corner contexts opt out via 'tight'/'none'.
|
|
18
23
|
* frame — boolean (default true). When false, no border drawn —
|
|
19
24
|
* used by tightly-packed grid layouts.
|
|
20
25
|
* variant — 'default' (border-based chrome) |
|
|
@@ -34,13 +39,19 @@
|
|
|
34
39
|
* onClick — if provided, renders as <button>; else <span>.
|
|
35
40
|
* title — passes through.
|
|
36
41
|
*/
|
|
37
|
-
import TransparentX from '
|
|
42
|
+
import TransparentX from '../utilities/TransparentX'
|
|
38
43
|
|
|
39
44
|
const SIZE_CLASSES = {
|
|
40
45
|
fill: 'w-full aspect-square',
|
|
41
46
|
stretch: 'w-full h-full',
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/* Named px sizes that track the control ladder — inline style, not a
|
|
50
|
+
* utility class (package chrome never rides arbitrary utilities). */
|
|
51
|
+
const NAMED_PX = {
|
|
52
|
+
'control-sm': 26, // kol-control-sm outer height — paint-bar swatch flush with sm input chrome
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
const RADIUS_CLASSES = {
|
|
45
56
|
none: 'rounded-none',
|
|
46
57
|
tight: 'rounded-[var(--kol-radius-xs)]',
|
|
@@ -54,7 +65,7 @@ export default function ColorSwatch({
|
|
|
54
65
|
hex,
|
|
55
66
|
selected = false,
|
|
56
67
|
size = 24,
|
|
57
|
-
radius = '
|
|
68
|
+
radius = 'sm',
|
|
58
69
|
frame = true,
|
|
59
70
|
variant = 'default',
|
|
60
71
|
hoverable = true,
|
|
@@ -66,9 +77,10 @@ export default function ColorSwatch({
|
|
|
66
77
|
...rest
|
|
67
78
|
}) {
|
|
68
79
|
const interactive = typeof onClick === 'function'
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
80
|
+
const resolvedSize = NAMED_PX[size] ?? size
|
|
81
|
+
const isNamed = typeof resolvedSize === 'string'
|
|
82
|
+
const sizeCls = isNamed ? (SIZE_CLASSES[resolvedSize] ?? '') : ''
|
|
83
|
+
const sizeStyle = isNamed ? null : { width: resolvedSize, height: resolvedSize }
|
|
72
84
|
|
|
73
85
|
const isHalo = variant === 'halo'
|
|
74
86
|
const radiusCls = RADIUS_CLASSES[radius] ?? RADIUS_CLASSES.sm
|
package/src/atoms/Image.jsx
CHANGED
package/src/atoms/Input.jsx
CHANGED
|
@@ -26,6 +26,10 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
26
26
|
* the shell at text-meta. aria-hidden — affordances, not labels.
|
|
27
27
|
* iconLeft — name of a leading icon rendered inside the shell (e.g.
|
|
28
28
|
* "search-16"). iconSize overrides the size-derived default.
|
|
29
|
+
* slotLeft — arbitrary leading node rendered inside the shell, before
|
|
30
|
+
* iconLeft/prefix — the paint-bar anatomy ([swatch] FFFFFF is ONE
|
|
31
|
+
* container, not two boxes; ColorSwatchFieldSizing 2026-08-12). The
|
|
32
|
+
* consumer owns the node's sizing; the shell's padding frames it.
|
|
29
33
|
*
|
|
30
34
|
* Chrome (bg/border/padding/transition/disabled) comes from .kol-control;
|
|
31
35
|
* Input owns prefix/suffix/icon layout + the inner <input> styling.
|
|
@@ -42,6 +46,7 @@ export default function Input({
|
|
|
42
46
|
chars,
|
|
43
47
|
prefix,
|
|
44
48
|
suffix,
|
|
49
|
+
slotLeft,
|
|
45
50
|
iconLeft,
|
|
46
51
|
iconSize = null,
|
|
47
52
|
placeholder,
|
|
@@ -99,6 +104,9 @@ export default function Input({
|
|
|
99
104
|
style={width ? { width: typeof width === 'number' ? `${width}px` : width } : undefined}
|
|
100
105
|
aria-disabled={disabled || undefined}
|
|
101
106
|
>
|
|
107
|
+
{slotLeft && (
|
|
108
|
+
<span className="flex items-center shrink-0 pr-2">{slotLeft}</span>
|
|
109
|
+
)}
|
|
102
110
|
{iconLeft && (
|
|
103
111
|
<span aria-hidden="true" className="flex items-center text-auto opacity-50 shrink-0 pr-2">
|
|
104
112
|
<Icon name={iconLeft} size={resolvedIconSize} />
|
package/src/atoms/Textarea.jsx
CHANGED
|
@@ -15,7 +15,14 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
15
15
|
* dominating the panel). Resize is real (2026-07-08): native resize is
|
|
16
16
|
* OFF (Firefox's built-in grip cannot be hidden any other way) and the
|
|
17
17
|
* kol-icon-set-v1 `resize-grip` icon IS the drag handle — corner drag,
|
|
18
|
-
*
|
|
18
|
+
* min 120×40. One grip, every browser.
|
|
19
|
+
*
|
|
20
|
+
* The X-drag is container-clamped (2026-08-12, TextareaResizeClamp): the
|
|
21
|
+
* width write caps at the parent's content width, so a drag can never
|
|
22
|
+
* overflow the box the Textarea sits in. `axis` narrows the drag:
|
|
23
|
+
* axis="both" (default) — corner drag, clamped
|
|
24
|
+
* axis="y" — height only; rail-mounted textareas have
|
|
25
|
+
* nowhere meaningful to grow on X
|
|
19
26
|
*
|
|
20
27
|
* Controlled OR uncontrolled:
|
|
21
28
|
* - pass `value` + `onChange` for controlled,
|
|
@@ -32,6 +39,7 @@ export default function Textarea({
|
|
|
32
39
|
variant = 'filled',
|
|
33
40
|
size = 'md',
|
|
34
41
|
rows = 3,
|
|
42
|
+
axis = 'both',
|
|
35
43
|
placeholder,
|
|
36
44
|
disabled = false,
|
|
37
45
|
className = '',
|
|
@@ -72,8 +80,11 @@ export default function Textarea({
|
|
|
72
80
|
aria-hidden="true"
|
|
73
81
|
className="kol-textarea-resize-icon"
|
|
74
82
|
onPointerDown={(e) => {
|
|
75
|
-
// The grip IS the resize handle — corner drag
|
|
76
|
-
//
|
|
83
|
+
// The grip IS the resize handle — corner drag (min 120×40).
|
|
84
|
+
// Width lands on the shell, height on the textarea. The width
|
|
85
|
+
// write is clamped to the parent's content width so an X-drag
|
|
86
|
+
// can never overflow the container; the 120 floor wins if the
|
|
87
|
+
// container is narrower than that.
|
|
77
88
|
const shell = e.currentTarget.parentElement
|
|
78
89
|
const ta = shell?.querySelector('textarea')
|
|
79
90
|
if (!shell || !ta) return
|
|
@@ -82,8 +93,17 @@ export default function Textarea({
|
|
|
82
93
|
const startY = e.clientY
|
|
83
94
|
const startW = shell.offsetWidth
|
|
84
95
|
const startH = ta.offsetHeight
|
|
96
|
+
const parent = shell.parentElement
|
|
97
|
+
let maxW = Infinity
|
|
98
|
+
if (parent) {
|
|
99
|
+
const pcs = getComputedStyle(parent)
|
|
100
|
+
maxW = parent.clientWidth
|
|
101
|
+
- parseFloat(pcs.paddingLeft) - parseFloat(pcs.paddingRight)
|
|
102
|
+
}
|
|
85
103
|
const move = (ev) => {
|
|
86
|
-
|
|
104
|
+
if (axis !== 'y') {
|
|
105
|
+
shell.style.width = `${Math.max(Math.min(startW + ev.clientX - startX, maxW), 120)}px`
|
|
106
|
+
}
|
|
87
107
|
ta.style.height = `${Math.max(startH + ev.clientY - startY, 40)}px`
|
|
88
108
|
}
|
|
89
109
|
const up = () => {
|
package/src/graphics/Graphic.jsx
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* than silently empty.
|
|
13
13
|
*/
|
|
14
14
|
import { useEffect, useState } from 'react'
|
|
15
|
-
import AssetPlaceholder from '../
|
|
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 './
|
|
21
|
-
export { default as AssetPlaceholder } from './
|
|
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 './
|
|
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 './
|
|
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 './
|
|
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 './
|
|
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 './
|
|
42
|
-
export { default as ProsePreview } from './
|
|
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 './
|
|
56
|
-
export { default as TransparentX } from './
|
|
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 './
|
|
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 './
|
|
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 './
|
|
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 './
|
|
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 './
|
|
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 './
|
|
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 '../
|
|
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 '../
|
|
5
|
+
import { usePopover, PopoverPanel } from '../utilities/Popover'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* ColorInputRow — swatch chip + `#` hex input row. The single merged form of
|
|
@@ -127,7 +127,9 @@ export default function ColorInputRow({
|
|
|
127
127
|
chip(24)
|
|
128
128
|
)
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
/* leading-normal: truncate's overflow clip cuts mono descenders on
|
|
131
|
+
* kol-helper's 1-em line box (MenuItemDescenderClip sweep, 2026-08-12). */
|
|
132
|
+
const labelCls = `kol-helper-12 truncate leading-normal ${unused ? 'text-meta' : 'text-emphasis'}`
|
|
131
133
|
|
|
132
134
|
const hexInput = (
|
|
133
135
|
<Input
|
|
@@ -156,7 +158,7 @@ export default function ColorInputRow({
|
|
|
156
158
|
>
|
|
157
159
|
{swatchCell}
|
|
158
160
|
<span className={labelCls}>{labelVisible ? label : ''}</span>
|
|
159
|
-
<span className={`kol-helper-10 truncate ${unused ? 'text-subtle' : 'text-meta'}`}>
|
|
161
|
+
<span className={`kol-helper-10 truncate leading-normal ${unused ? 'text-subtle' : 'text-meta'}`}>
|
|
160
162
|
{tokenName}
|
|
161
163
|
</span>
|
|
162
164
|
{hexInput}
|
|
@@ -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 '../
|
|
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-
|
|
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-
|
|
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 '../
|
|
4
|
+
import { usePopover, PopoverPanel } from '../utilities/Popover'
|
|
5
5
|
import Dropdown from './Dropdown'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -236,7 +236,8 @@ export default function FieldRow({
|
|
|
236
236
|
<div className={`kol-mono-12 text-emphasis${type === 'media' ? ' self-start pt-1' : ''}`}>{label}</div>
|
|
237
237
|
<div className="min-w-0">{control}</div>
|
|
238
238
|
{hint != null && (
|
|
239
|
-
|
|
239
|
+
/* leading-normal: descender fix (MenuItemDescenderClip sweep, 2026-08-12) */
|
|
240
|
+
<div className="col-start-2 kol-helper-12 text-meta pt-2 truncate leading-normal">{hint}</div>
|
|
240
241
|
)}
|
|
241
242
|
</div>
|
|
242
243
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
|
-
import { PopoverPanel, usePopover } from '../
|
|
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
|
|
@@ -99,7 +101,11 @@ export function MenuDropdownItem({ onClick, disabled, prefix, iconLeft, shortcut
|
|
|
99
101
|
>
|
|
100
102
|
{prefix && <span className="shrink-0 inline-flex items-center">{prefix}</span>}
|
|
101
103
|
{iconLeft && <span className="shrink-0 w-4 inline-flex items-center justify-center">{iconLeft}</span>}
|
|
102
|
-
|
|
104
|
+
{/* leading-normal: kol-helper-12 is line-height 1, and truncate's
|
|
105
|
+
* overflow clip cuts mono descenders on a 1-em line box ("Show grid"
|
|
106
|
+
* loses its g). The h-8 centered row absorbs the taller line box —
|
|
107
|
+
* zero layout shift (MenuItemDescenderClip, 2026-08-12). */}
|
|
108
|
+
<span className="flex-1 truncate leading-normal">{children}</span>
|
|
103
109
|
{shortcut && <span className="kol-helper-10 text-emphasis shrink-0 inline-flex items-center">{shortcut}</span>}
|
|
104
110
|
</button>
|
|
105
111
|
)
|
|
@@ -131,7 +137,8 @@ export function MenuDropdownNest({ prefix, iconLeft, label, children }) {
|
|
|
131
137
|
>
|
|
132
138
|
{prefix && <span className="shrink-0 inline-flex items-center">{prefix}</span>}
|
|
133
139
|
{iconLeft && <span className="shrink-0 w-4 inline-flex items-center justify-center">{iconLeft}</span>}
|
|
134
|
-
|
|
140
|
+
{/* leading-normal — same descender fix as MenuDropdownItem above. */}
|
|
141
|
+
<span className="flex-1 truncate leading-normal">{label}</span>
|
|
135
142
|
<Icon
|
|
136
143
|
name="chevron-down"
|
|
137
144
|
size={10}
|
package/src/molecules/Modal.jsx
CHANGED
|
@@ -9,6 +9,13 @@ import Input from '../atoms/Input.jsx'
|
|
|
9
9
|
* const { prompt, confirm } = useModal()
|
|
10
10
|
* const name = await prompt('Name this frame:', 'Untitled')
|
|
11
11
|
* const proceed = await confirm('Discard unsaved changes?')
|
|
12
|
+
* const restore = await confirm('Restore your last canvas?',
|
|
13
|
+
* { okLabel: 'Restore', cancelLabel: 'New file' })
|
|
14
|
+
*
|
|
15
|
+
* Both take an options object — `{ okLabel, cancelLabel }` (prompt: third
|
|
16
|
+
* arg, after defaultValue) — so the buttons can SAY the outcome; defaults
|
|
17
|
+
* stay OK / Cancel, existing callers untouched (ModalConfirmLabels,
|
|
18
|
+
* 2026-08-12). Enter/Escape keep their meanings regardless of labels.
|
|
12
19
|
*
|
|
13
20
|
* Returned promise resolves to:
|
|
14
21
|
* - prompt → string (value) on submit, `null` on cancel
|
|
@@ -30,12 +37,12 @@ export function ModalProvider({ children }) {
|
|
|
30
37
|
})
|
|
31
38
|
}, [])
|
|
32
39
|
|
|
33
|
-
const prompt = useCallback((title, defaultValue = '') =>
|
|
34
|
-
new Promise((resolve) => setState({ kind: 'prompt', title, defaultValue, resolve })),
|
|
40
|
+
const prompt = useCallback((title, defaultValue = '', { okLabel, cancelLabel } = {}) =>
|
|
41
|
+
new Promise((resolve) => setState({ kind: 'prompt', title, defaultValue, okLabel, cancelLabel, resolve })),
|
|
35
42
|
[])
|
|
36
43
|
|
|
37
|
-
const confirm = useCallback((title) =>
|
|
38
|
-
new Promise((resolve) => setState({ kind: 'confirm', title, resolve })),
|
|
44
|
+
const confirm = useCallback((title, { okLabel, cancelLabel } = {}) =>
|
|
45
|
+
new Promise((resolve) => setState({ kind: 'confirm', title, okLabel, cancelLabel, resolve })),
|
|
39
46
|
[])
|
|
40
47
|
|
|
41
48
|
return (
|
|
@@ -85,7 +92,9 @@ function ModalView({ state, closeWith }) {
|
|
|
85
92
|
maxWidth: '90vw',
|
|
86
93
|
}}
|
|
87
94
|
>
|
|
88
|
-
|
|
95
|
+
{/* kol-mono-12, not helper: dialog copy WRAPS, and helper's
|
|
96
|
+
* line-height 1 is single-line chrome only (type protocol). */}
|
|
97
|
+
<p className="kol-mono-12 text-emphasis">{state.title}</p>
|
|
89
98
|
{state.kind === 'prompt' && (
|
|
90
99
|
<Input
|
|
91
100
|
ref={inputRef}
|
|
@@ -97,19 +106,28 @@ function ModalView({ state, closeWith }) {
|
|
|
97
106
|
/>
|
|
98
107
|
)}
|
|
99
108
|
<div className="flex gap-2 justify-end">
|
|
100
|
-
<Button variant="secondary" size="sm" onClick={cancel}>Cancel</Button>
|
|
101
|
-
<Button variant="primary" size="sm" onClick={submit}>OK</Button>
|
|
109
|
+
<Button variant="secondary" size="sm" onClick={cancel}>{state.cancelLabel ?? 'Cancel'}</Button>
|
|
110
|
+
<Button variant="primary" size="sm" onClick={submit}>{state.okLabel ?? 'OK'}</Button>
|
|
102
111
|
</div>
|
|
103
112
|
</div>
|
|
104
113
|
</div>
|
|
105
114
|
)
|
|
106
115
|
}
|
|
107
116
|
|
|
117
|
+
/* Warn once, not per call — the fallback swallowing a missing ModalProvider
|
|
118
|
+
* silently is how kol-fxr ran months on native window.confirm without
|
|
119
|
+
* noticing (ModalConfirmLabels, 2026-08-12). */
|
|
120
|
+
let warnedNoProvider = false
|
|
121
|
+
|
|
108
122
|
export function useModal() {
|
|
109
123
|
const ctx = useContext(ModalCtx)
|
|
110
124
|
if (ctx) return ctx
|
|
111
125
|
/* No-context fallback — falls back to native prompt/confirm so callers
|
|
112
126
|
* don't need to null-check. */
|
|
127
|
+
if (!warnedNoProvider && typeof console !== 'undefined') {
|
|
128
|
+
warnedNoProvider = true
|
|
129
|
+
console.warn('[kol] useModal(): no <ModalProvider> mounted — falling back to native window.prompt/confirm. Custom labels are ignored on the fallback.')
|
|
130
|
+
}
|
|
113
131
|
return {
|
|
114
132
|
prompt: async (title, def = '') => {
|
|
115
133
|
if (typeof window === 'undefined') return null
|
|
@@ -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 '../
|
|
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 '../
|
|
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 '../
|
|
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 '../
|
|
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 '../
|
|
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 '../
|
|
10
|
+
import { Tooltip } from '../utilities/Popover'
|
|
11
11
|
import Table from './Table'
|
|
12
12
|
import MediaLibrary from './MediaLibrary'
|
|
13
13
|
|
|
@@ -353,7 +353,7 @@ export default function RecordManager({
|
|
|
353
353
|
{onPreview && (
|
|
354
354
|
<ToolbarIcon name="play" label="Preview" onClick={() => onPreview(record)} />
|
|
355
355
|
)}
|
|
356
|
-
{saveState != null && <span className="kol-helper-10 text-meta truncate">{saveState}</span>}
|
|
356
|
+
{saveState != null && <span className="kol-helper-10 text-meta truncate leading-normal">{saveState}</span>}
|
|
357
357
|
{onPublish && (
|
|
358
358
|
<Button variant="primary" size="sm" onClick={() => onPublish(record)}>
|
|
359
359
|
{publishLabel}
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|