@highlighters/react 1.0.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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/index.cjs +82 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jace Attard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @highlighters/react
|
|
2
|
+
|
|
3
|
+
React bindings for [`@highlighters/core`](https://www.npmjs.com/package/@highlighters/core) - realistic highlighter-pen marks for web text. Wet-ink colour, organic frayed edges, true multiply-blend optics, drawn over your text without altering it.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @highlighters/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@highlighters/core` is bundled in as a dependency, so you do not install it separately. React 18+ is a peer dependency.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Highlight } from "@highlighters/react";
|
|
17
|
+
|
|
18
|
+
function Article() {
|
|
19
|
+
return (
|
|
20
|
+
<p>
|
|
21
|
+
The part that matters most is{" "}
|
|
22
|
+
<Highlight options={{ color: { palette: "fluorescent", swatch: "yellow" } }}>
|
|
23
|
+
this exact sentence
|
|
24
|
+
</Highlight>
|
|
25
|
+
.
|
|
26
|
+
</p>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`Highlight` is polymorphic via `as` (defaults to `<span>`). For highlighting an existing element you already hold a ref to, use the hook:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { useRef } from "react";
|
|
35
|
+
import { useHighlight } from "@highlighters/react";
|
|
36
|
+
|
|
37
|
+
function Note() {
|
|
38
|
+
const ref = useRef<HTMLParagraphElement>(null);
|
|
39
|
+
useHighlight(ref, { color: { palette: "mild", swatch: "blue" } });
|
|
40
|
+
return <p ref={ref}>Highlighted on mount, updated when options change.</p>;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Documentation
|
|
45
|
+
|
|
46
|
+
Full guides, the options reference, and live demos live at **[highlighte.rs](https://highlighte.rs)** and in the [repository](https://github.com/JaceThings/highlighters).
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
Highlight: () => Highlight,
|
|
25
|
+
useHighlight: () => useHighlight
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/use-highlight.ts
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
var import_core = require("@highlighters/core");
|
|
32
|
+
var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
|
|
33
|
+
function resolveTarget(target) {
|
|
34
|
+
if (target && typeof target === "object" && "current" in target) {
|
|
35
|
+
return target.current ?? null;
|
|
36
|
+
}
|
|
37
|
+
return target ?? null;
|
|
38
|
+
}
|
|
39
|
+
function useHighlight(target, options, host) {
|
|
40
|
+
const handleRef = (0, import_react.useRef)(null);
|
|
41
|
+
const optionsRef = (0, import_react.useRef)(options);
|
|
42
|
+
optionsRef.current = options;
|
|
43
|
+
const optionsKey = JSON.stringify(options ?? null);
|
|
44
|
+
useIsoLayoutEffect(() => {
|
|
45
|
+
const resolved = resolveTarget(target);
|
|
46
|
+
if (resolved != null && !handleRef.current) {
|
|
47
|
+
handleRef.current = (0, import_core.highlight)(resolved, optionsRef.current, host ?? void 0);
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
handleRef.current?.remove();
|
|
51
|
+
handleRef.current = null;
|
|
52
|
+
};
|
|
53
|
+
}, [target, host]);
|
|
54
|
+
useIsoLayoutEffect(() => {
|
|
55
|
+
if (handleRef.current) return;
|
|
56
|
+
const resolved = resolveTarget(target);
|
|
57
|
+
if (resolved != null) {
|
|
58
|
+
handleRef.current = (0, import_core.highlight)(resolved, optionsRef.current, host ?? void 0);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
useIsoLayoutEffect(() => {
|
|
62
|
+
handleRef.current?.update(optionsRef.current ?? {});
|
|
63
|
+
}, [optionsKey]);
|
|
64
|
+
return handleRef;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/highlight.tsx
|
|
68
|
+
var import_react2 = require("react");
|
|
69
|
+
function Highlight(props) {
|
|
70
|
+
const { as, options, host, children, ...rest } = props;
|
|
71
|
+
const Component = as ?? "span";
|
|
72
|
+
const [node, setNode] = (0, import_react2.useState)(null);
|
|
73
|
+
const ref = (0, import_react2.useCallback)((el) => setNode(el), []);
|
|
74
|
+
useHighlight(node, options, host);
|
|
75
|
+
return (0, import_react2.createElement)(Component, { ...rest, ref }, children);
|
|
76
|
+
}
|
|
77
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
78
|
+
0 && (module.exports = {
|
|
79
|
+
Highlight,
|
|
80
|
+
useHighlight
|
|
81
|
+
});
|
|
82
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/use-highlight.ts","../src/highlight.tsx"],"sourcesContent":["/** `@highlighters/react`: React bindings for `@highlighters/core`. */\nexport { useHighlight, type HighlightTarget } from \"./use-highlight.js\";\nexport { Highlight, type HighlightProps, type HighlightOwnProps } from \"./highlight.js\";\n\n// Re-export core types for convenience.\nexport type {\n HighlightOptions,\n MarkHandle,\n GroupHandle,\n Target,\n TextTarget,\n PageTarget,\n PaletteName,\n PaletteSwatch,\n Palette,\n MarkType,\n ShapeType,\n TipType,\n EdgeCap,\n BlendMode,\n SnapMode,\n RendererTier,\n RendererTierPreference,\n GradientConfig,\n GradientStop,\n TipOptions,\n InkOptions,\n EdgeOptions,\n PaperOptions,\n GlowOptions,\n AnimationOptions,\n} from \"@highlighters/core\";\n","import { useEffect, useLayoutEffect, useRef } from \"react\";\nimport { highlight } from \"@highlighters/core\";\nimport type { HighlightOptions, MarkHandle, Target } from \"@highlighters/core\";\n\n// useLayoutEffect on the client, useEffect during SSR to avoid React's server warning.\nconst useIsoLayoutEffect = typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\n/** A React ref, any core {@link Target}, or `null`. */\nexport type HighlightTarget = React.RefObject<Element | null> | Target | null;\n\nfunction resolveTarget(target: HighlightTarget): Target | null {\n if (target && typeof target === \"object\" && \"current\" in target) {\n return target.current ?? null;\n }\n return (target as Target) ?? null;\n}\n\n/** Applies a highlighter mark to a ref, DOM node, or core {@link Target}, returning its live {@link MarkHandle}. */\nexport function useHighlight(\n target: HighlightTarget,\n options?: HighlightOptions,\n host?: HTMLElement | null,\n): React.RefObject<MarkHandle | null> {\n const handleRef = useRef<MarkHandle | null>(null);\n\n // Read options through a ref so the setup effect doesn't re-seed the mark each render.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // Stable dep so a fresh literal with identical contents doesn't re-subscribe.\n const optionsKey = JSON.stringify(options ?? null);\n\n // (Re)create the mark when the resolved target changes.\n useIsoLayoutEffect(() => {\n const resolved = resolveTarget(target);\n if (resolved != null && !handleRef.current) {\n handleRef.current = highlight(resolved, optionsRef.current, host ?? undefined);\n }\n return () => {\n handleRef.current?.remove();\n handleRef.current = null;\n };\n }, [target, host]);\n\n // A bare RefObject can populate after mount without changing identity, so re-check each render.\n useIsoLayoutEffect(() => {\n if (handleRef.current) return;\n const resolved = resolveTarget(target);\n if (resolved != null) {\n handleRef.current = highlight(resolved, optionsRef.current, host ?? undefined);\n }\n });\n\n // Sync option changes through the handle without re-seeding geometry.\n useIsoLayoutEffect(() => {\n handleRef.current?.update(optionsRef.current ?? {});\n }, [optionsKey]);\n\n return handleRef;\n}\n","import {\n createElement,\n useCallback,\n useState,\n type ElementType,\n type ReactNode,\n type ComponentPropsWithoutRef,\n} from \"react\";\nimport { useHighlight } from \"./use-highlight.js\";\nimport type { HighlightOptions } from \"@highlighters/core\";\n\nexport type HighlightOwnProps = {\n children?: ReactNode;\n options?: HighlightOptions;\n /** Positioned element to mount the overlay inside, instead of the body. */\n host?: HTMLElement | null;\n};\n\ntype ReservedKeys = keyof HighlightOwnProps | \"as\";\n\n/** Polymorphic via `as` (default `<span>`), which determines available HTML attributes. */\nexport type HighlightProps<E extends ElementType = \"span\"> = HighlightOwnProps & {\n as?: E;\n} & Omit<ComponentPropsWithoutRef<E>, ReservedKeys>;\n\n/**\n * Renders an element whose text content is highlighted with a decorative overlay mark.\n *\n * @example\n * ```tsx\n * <Highlight as=\"p\" options={{ color: { palette: \"fluorescent\", swatch: \"pink\" } }}>\n * Highlight this paragraph\n * </Highlight>\n * ```\n */\nexport function Highlight<E extends ElementType = \"span\">(props: HighlightProps<E>) {\n const { as, options, host, children, ...rest } = props;\n const Component = (as ?? \"span\") as ElementType;\n // Callback ref into state so the hook tracks the actual mounted node across as-swaps.\n const [node, setNode] = useState<Element | null>(null);\n const ref = useCallback((el: Element | null) => setNode(el), []);\n\n useHighlight(node, options, host);\n\n return createElement(Component, { ...rest, ref }, children);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAmD;AACnD,kBAA0B;AAI1B,IAAM,qBAAqB,OAAO,WAAW,cAAc,+BAAkB;AAK7E,SAAS,cAAc,QAAwC;AAC7D,MAAI,UAAU,OAAO,WAAW,YAAY,aAAa,QAAQ;AAC/D,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAQ,UAAqB;AAC/B;AAGO,SAAS,aACd,QACA,SACA,MACoC;AACpC,QAAM,gBAAY,qBAA0B,IAAI;AAGhD,QAAM,iBAAa,qBAAO,OAAO;AACjC,aAAW,UAAU;AAGrB,QAAM,aAAa,KAAK,UAAU,WAAW,IAAI;AAGjD,qBAAmB,MAAM;AACvB,UAAM,WAAW,cAAc,MAAM;AACrC,QAAI,YAAY,QAAQ,CAAC,UAAU,SAAS;AAC1C,gBAAU,cAAU,uBAAU,UAAU,WAAW,SAAS,QAAQ,MAAS;AAAA,IAC/E;AACA,WAAO,MAAM;AACX,gBAAU,SAAS,OAAO;AAC1B,gBAAU,UAAU;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,QAAQ,IAAI,CAAC;AAGjB,qBAAmB,MAAM;AACvB,QAAI,UAAU,QAAS;AACvB,UAAM,WAAW,cAAc,MAAM;AACrC,QAAI,YAAY,MAAM;AACpB,gBAAU,cAAU,uBAAU,UAAU,WAAW,SAAS,QAAQ,MAAS;AAAA,IAC/E;AAAA,EACF,CAAC;AAGD,qBAAmB,MAAM;AACvB,cAAU,SAAS,OAAO,WAAW,WAAW,CAAC,CAAC;AAAA,EACpD,GAAG,CAAC,UAAU,CAAC;AAEf,SAAO;AACT;;;AC3DA,IAAAA,gBAOO;AA4BA,SAAS,UAA0C,OAA0B;AAClF,QAAM,EAAE,IAAI,SAAS,MAAM,UAAU,GAAG,KAAK,IAAI;AACjD,QAAM,YAAa,MAAM;AAEzB,QAAM,CAAC,MAAM,OAAO,QAAI,wBAAyB,IAAI;AACrD,QAAM,UAAM,2BAAY,CAAC,OAAuB,QAAQ,EAAE,GAAG,CAAC,CAAC;AAE/D,eAAa,MAAM,SAAS,IAAI;AAEhC,aAAO,6BAAc,WAAW,EAAE,GAAG,MAAM,IAAI,GAAG,QAAQ;AAC5D;","names":["import_react"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Target, HighlightOptions, MarkHandle } from '@highlighters/core';
|
|
2
|
+
export { AnimationOptions, BlendMode, EdgeCap, EdgeOptions, GlowOptions, GradientConfig, GradientStop, GroupHandle, HighlightOptions, InkOptions, MarkHandle, MarkType, PageTarget, Palette, PaletteName, PaletteSwatch, PaperOptions, RendererTier, RendererTierPreference, ShapeType, SnapMode, Target, TextTarget, TipOptions, TipType } from '@highlighters/core';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ElementType, ReactNode, ComponentPropsWithoutRef } from 'react';
|
|
5
|
+
|
|
6
|
+
/** A React ref, any core {@link Target}, or `null`. */
|
|
7
|
+
type HighlightTarget = React.RefObject<Element | null> | Target | null;
|
|
8
|
+
/** Applies a highlighter mark to a ref, DOM node, or core {@link Target}, returning its live {@link MarkHandle}. */
|
|
9
|
+
declare function useHighlight(target: HighlightTarget, options?: HighlightOptions, host?: HTMLElement | null): React.RefObject<MarkHandle | null>;
|
|
10
|
+
|
|
11
|
+
type HighlightOwnProps = {
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
options?: HighlightOptions;
|
|
14
|
+
/** Positioned element to mount the overlay inside, instead of the body. */
|
|
15
|
+
host?: HTMLElement | null;
|
|
16
|
+
};
|
|
17
|
+
type ReservedKeys = keyof HighlightOwnProps | "as";
|
|
18
|
+
/** Polymorphic via `as` (default `<span>`), which determines available HTML attributes. */
|
|
19
|
+
type HighlightProps<E extends ElementType = "span"> = HighlightOwnProps & {
|
|
20
|
+
as?: E;
|
|
21
|
+
} & Omit<ComponentPropsWithoutRef<E>, ReservedKeys>;
|
|
22
|
+
/**
|
|
23
|
+
* Renders an element whose text content is highlighted with a decorative overlay mark.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <Highlight as="p" options={{ color: { palette: "fluorescent", swatch: "pink" } }}>
|
|
28
|
+
* Highlight this paragraph
|
|
29
|
+
* </Highlight>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare function Highlight<E extends ElementType = "span">(props: HighlightProps<E>): react.ReactElement<any, string | react.JSXElementConstructor<any>>;
|
|
33
|
+
|
|
34
|
+
export { Highlight, type HighlightOwnProps, type HighlightProps, type HighlightTarget, useHighlight };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Target, HighlightOptions, MarkHandle } from '@highlighters/core';
|
|
2
|
+
export { AnimationOptions, BlendMode, EdgeCap, EdgeOptions, GlowOptions, GradientConfig, GradientStop, GroupHandle, HighlightOptions, InkOptions, MarkHandle, MarkType, PageTarget, Palette, PaletteName, PaletteSwatch, PaperOptions, RendererTier, RendererTierPreference, ShapeType, SnapMode, Target, TextTarget, TipOptions, TipType } from '@highlighters/core';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ElementType, ReactNode, ComponentPropsWithoutRef } from 'react';
|
|
5
|
+
|
|
6
|
+
/** A React ref, any core {@link Target}, or `null`. */
|
|
7
|
+
type HighlightTarget = React.RefObject<Element | null> | Target | null;
|
|
8
|
+
/** Applies a highlighter mark to a ref, DOM node, or core {@link Target}, returning its live {@link MarkHandle}. */
|
|
9
|
+
declare function useHighlight(target: HighlightTarget, options?: HighlightOptions, host?: HTMLElement | null): React.RefObject<MarkHandle | null>;
|
|
10
|
+
|
|
11
|
+
type HighlightOwnProps = {
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
options?: HighlightOptions;
|
|
14
|
+
/** Positioned element to mount the overlay inside, instead of the body. */
|
|
15
|
+
host?: HTMLElement | null;
|
|
16
|
+
};
|
|
17
|
+
type ReservedKeys = keyof HighlightOwnProps | "as";
|
|
18
|
+
/** Polymorphic via `as` (default `<span>`), which determines available HTML attributes. */
|
|
19
|
+
type HighlightProps<E extends ElementType = "span"> = HighlightOwnProps & {
|
|
20
|
+
as?: E;
|
|
21
|
+
} & Omit<ComponentPropsWithoutRef<E>, ReservedKeys>;
|
|
22
|
+
/**
|
|
23
|
+
* Renders an element whose text content is highlighted with a decorative overlay mark.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <Highlight as="p" options={{ color: { palette: "fluorescent", swatch: "pink" } }}>
|
|
28
|
+
* Highlight this paragraph
|
|
29
|
+
* </Highlight>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare function Highlight<E extends ElementType = "span">(props: HighlightProps<E>): react.ReactElement<any, string | react.JSXElementConstructor<any>>;
|
|
33
|
+
|
|
34
|
+
export { Highlight, type HighlightOwnProps, type HighlightProps, type HighlightTarget, useHighlight };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/use-highlight.ts
|
|
4
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
5
|
+
import { highlight } from "@highlighters/core";
|
|
6
|
+
var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
7
|
+
function resolveTarget(target) {
|
|
8
|
+
if (target && typeof target === "object" && "current" in target) {
|
|
9
|
+
return target.current ?? null;
|
|
10
|
+
}
|
|
11
|
+
return target ?? null;
|
|
12
|
+
}
|
|
13
|
+
function useHighlight(target, options, host) {
|
|
14
|
+
const handleRef = useRef(null);
|
|
15
|
+
const optionsRef = useRef(options);
|
|
16
|
+
optionsRef.current = options;
|
|
17
|
+
const optionsKey = JSON.stringify(options ?? null);
|
|
18
|
+
useIsoLayoutEffect(() => {
|
|
19
|
+
const resolved = resolveTarget(target);
|
|
20
|
+
if (resolved != null && !handleRef.current) {
|
|
21
|
+
handleRef.current = highlight(resolved, optionsRef.current, host ?? void 0);
|
|
22
|
+
}
|
|
23
|
+
return () => {
|
|
24
|
+
handleRef.current?.remove();
|
|
25
|
+
handleRef.current = null;
|
|
26
|
+
};
|
|
27
|
+
}, [target, host]);
|
|
28
|
+
useIsoLayoutEffect(() => {
|
|
29
|
+
if (handleRef.current) return;
|
|
30
|
+
const resolved = resolveTarget(target);
|
|
31
|
+
if (resolved != null) {
|
|
32
|
+
handleRef.current = highlight(resolved, optionsRef.current, host ?? void 0);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
useIsoLayoutEffect(() => {
|
|
36
|
+
handleRef.current?.update(optionsRef.current ?? {});
|
|
37
|
+
}, [optionsKey]);
|
|
38
|
+
return handleRef;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/highlight.tsx
|
|
42
|
+
import {
|
|
43
|
+
createElement,
|
|
44
|
+
useCallback,
|
|
45
|
+
useState
|
|
46
|
+
} from "react";
|
|
47
|
+
function Highlight(props) {
|
|
48
|
+
const { as, options, host, children, ...rest } = props;
|
|
49
|
+
const Component = as ?? "span";
|
|
50
|
+
const [node, setNode] = useState(null);
|
|
51
|
+
const ref = useCallback((el) => setNode(el), []);
|
|
52
|
+
useHighlight(node, options, host);
|
|
53
|
+
return createElement(Component, { ...rest, ref }, children);
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
Highlight,
|
|
57
|
+
useHighlight
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/use-highlight.ts","../src/highlight.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from \"react\";\nimport { highlight } from \"@highlighters/core\";\nimport type { HighlightOptions, MarkHandle, Target } from \"@highlighters/core\";\n\n// useLayoutEffect on the client, useEffect during SSR to avoid React's server warning.\nconst useIsoLayoutEffect = typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\n/** A React ref, any core {@link Target}, or `null`. */\nexport type HighlightTarget = React.RefObject<Element | null> | Target | null;\n\nfunction resolveTarget(target: HighlightTarget): Target | null {\n if (target && typeof target === \"object\" && \"current\" in target) {\n return target.current ?? null;\n }\n return (target as Target) ?? null;\n}\n\n/** Applies a highlighter mark to a ref, DOM node, or core {@link Target}, returning its live {@link MarkHandle}. */\nexport function useHighlight(\n target: HighlightTarget,\n options?: HighlightOptions,\n host?: HTMLElement | null,\n): React.RefObject<MarkHandle | null> {\n const handleRef = useRef<MarkHandle | null>(null);\n\n // Read options through a ref so the setup effect doesn't re-seed the mark each render.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // Stable dep so a fresh literal with identical contents doesn't re-subscribe.\n const optionsKey = JSON.stringify(options ?? null);\n\n // (Re)create the mark when the resolved target changes.\n useIsoLayoutEffect(() => {\n const resolved = resolveTarget(target);\n if (resolved != null && !handleRef.current) {\n handleRef.current = highlight(resolved, optionsRef.current, host ?? undefined);\n }\n return () => {\n handleRef.current?.remove();\n handleRef.current = null;\n };\n }, [target, host]);\n\n // A bare RefObject can populate after mount without changing identity, so re-check each render.\n useIsoLayoutEffect(() => {\n if (handleRef.current) return;\n const resolved = resolveTarget(target);\n if (resolved != null) {\n handleRef.current = highlight(resolved, optionsRef.current, host ?? undefined);\n }\n });\n\n // Sync option changes through the handle without re-seeding geometry.\n useIsoLayoutEffect(() => {\n handleRef.current?.update(optionsRef.current ?? {});\n }, [optionsKey]);\n\n return handleRef;\n}\n","import {\n createElement,\n useCallback,\n useState,\n type ElementType,\n type ReactNode,\n type ComponentPropsWithoutRef,\n} from \"react\";\nimport { useHighlight } from \"./use-highlight.js\";\nimport type { HighlightOptions } from \"@highlighters/core\";\n\nexport type HighlightOwnProps = {\n children?: ReactNode;\n options?: HighlightOptions;\n /** Positioned element to mount the overlay inside, instead of the body. */\n host?: HTMLElement | null;\n};\n\ntype ReservedKeys = keyof HighlightOwnProps | \"as\";\n\n/** Polymorphic via `as` (default `<span>`), which determines available HTML attributes. */\nexport type HighlightProps<E extends ElementType = \"span\"> = HighlightOwnProps & {\n as?: E;\n} & Omit<ComponentPropsWithoutRef<E>, ReservedKeys>;\n\n/**\n * Renders an element whose text content is highlighted with a decorative overlay mark.\n *\n * @example\n * ```tsx\n * <Highlight as=\"p\" options={{ color: { palette: \"fluorescent\", swatch: \"pink\" } }}>\n * Highlight this paragraph\n * </Highlight>\n * ```\n */\nexport function Highlight<E extends ElementType = \"span\">(props: HighlightProps<E>) {\n const { as, options, host, children, ...rest } = props;\n const Component = (as ?? \"span\") as ElementType;\n // Callback ref into state so the hook tracks the actual mounted node across as-swaps.\n const [node, setNode] = useState<Element | null>(null);\n const ref = useCallback((el: Element | null) => setNode(el), []);\n\n useHighlight(node, options, host);\n\n return createElement(Component, { ...rest, ref }, children);\n}\n"],"mappings":";;;AAAA,SAAS,WAAW,iBAAiB,cAAc;AACnD,SAAS,iBAAiB;AAI1B,IAAM,qBAAqB,OAAO,WAAW,cAAc,kBAAkB;AAK7E,SAAS,cAAc,QAAwC;AAC7D,MAAI,UAAU,OAAO,WAAW,YAAY,aAAa,QAAQ;AAC/D,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAQ,UAAqB;AAC/B;AAGO,SAAS,aACd,QACA,SACA,MACoC;AACpC,QAAM,YAAY,OAA0B,IAAI;AAGhD,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAGrB,QAAM,aAAa,KAAK,UAAU,WAAW,IAAI;AAGjD,qBAAmB,MAAM;AACvB,UAAM,WAAW,cAAc,MAAM;AACrC,QAAI,YAAY,QAAQ,CAAC,UAAU,SAAS;AAC1C,gBAAU,UAAU,UAAU,UAAU,WAAW,SAAS,QAAQ,MAAS;AAAA,IAC/E;AACA,WAAO,MAAM;AACX,gBAAU,SAAS,OAAO;AAC1B,gBAAU,UAAU;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,QAAQ,IAAI,CAAC;AAGjB,qBAAmB,MAAM;AACvB,QAAI,UAAU,QAAS;AACvB,UAAM,WAAW,cAAc,MAAM;AACrC,QAAI,YAAY,MAAM;AACpB,gBAAU,UAAU,UAAU,UAAU,WAAW,SAAS,QAAQ,MAAS;AAAA,IAC/E;AAAA,EACF,CAAC;AAGD,qBAAmB,MAAM;AACvB,cAAU,SAAS,OAAO,WAAW,WAAW,CAAC,CAAC;AAAA,EACpD,GAAG,CAAC,UAAU,CAAC;AAEf,SAAO;AACT;;;AC3DA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AA4BA,SAAS,UAA0C,OAA0B;AAClF,QAAM,EAAE,IAAI,SAAS,MAAM,UAAU,GAAG,KAAK,IAAI;AACjD,QAAM,YAAa,MAAM;AAEzB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAyB,IAAI;AACrD,QAAM,MAAM,YAAY,CAAC,OAAuB,QAAQ,EAAE,GAAG,CAAC,CAAC;AAE/D,eAAa,MAAM,SAAS,IAAI;AAEhC,SAAO,cAAc,WAAW,EAAE,GAAG,MAAM,IAAI,GAAG,QAAQ;AAC5D;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@highlighters/react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React hook and component for realistic highlighter marks",
|
|
5
|
+
"author": "Jace Attard <hi@ja.mt> (https://ja.mt)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"default": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@highlighters/core": "1.0.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/JaceThings/highlighters.git",
|
|
36
|
+
"directory": "packages/react"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"highlighter",
|
|
40
|
+
"highlight",
|
|
41
|
+
"marker",
|
|
42
|
+
"react",
|
|
43
|
+
"hook",
|
|
44
|
+
"component",
|
|
45
|
+
"annotation"
|
|
46
|
+
],
|
|
47
|
+
"homepage": "https://github.com/JaceThings/highlighters#readme",
|
|
48
|
+
"bugs": "https://github.com/JaceThings/highlighters/issues",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/react": "^19.2.13",
|
|
57
|
+
"@types/react-dom": "^19.2.3",
|
|
58
|
+
"react": "^19.2.5",
|
|
59
|
+
"react-dom": "^19.2.5"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsup",
|
|
63
|
+
"typecheck": "tsc --noEmit",
|
|
64
|
+
"clean": "rm -rf dist"
|
|
65
|
+
}
|
|
66
|
+
}
|