react-headings 4.0.1 → 4.0.3

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 CHANGED
@@ -18,7 +18,7 @@ Hardcoded heading levels break accessibility and SEO when components are reused.
18
18
  ## Features
19
19
 
20
20
  - Supports server-side rendering
21
- - Only 369 bytes minified & brotlied
21
+ - Only 373 bytes minified & brotlied
22
22
  - Typed with TypeScript
23
23
  - Works with any CSS solution (Tailwind, CSS-in-JS, etc.)
24
24
  - Plays nicely with component libraries
package/dist/index.d.ts CHANGED
@@ -1,40 +1,40 @@
1
- import type { ComponentPropsWithoutRef, ComponentPropsWithRef, ReactElement, ReactNode } from "react";
1
+ import { ComponentPropsWithRef, ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
2
  type Level = 1 | 2 | 3 | 4 | 5 | 6;
3
3
  type Tag = `h${Level}`;
4
4
  export type HeadingProps = ComponentPropsWithoutRef<"h1"> & {
5
- render?: (props: ComponentPropsWithRef<"h1">, heading: {
6
- level: Level;
7
- tag: Tag;
8
- }) => ReactElement;
5
+ render?: (props: ComponentPropsWithRef<"h1">, heading: {
6
+ level: Level;
7
+ tag: Tag;
8
+ }) => ReactElement;
9
9
  };
10
10
  export type HeadingLevelProps = {
11
- children?: ReactNode;
11
+ children?: ReactNode;
12
12
  } & ({
13
- offset?: number;
14
- level?: never;
13
+ offset?: number;
14
+ level?: never;
15
15
  } | {
16
- level: number;
17
- offset?: never;
16
+ level: number;
17
+ offset?: never;
18
18
  });
19
19
  /**
20
20
  * Renders an `h1`–`h6` according to the current level.
21
21
  */
22
22
  export declare const Heading: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>, "ref"> & {
23
- render?: (props: ComponentPropsWithRef<"h1">, heading: {
24
- level: Level;
25
- tag: Tag;
26
- }) => ReactElement;
23
+ render?: (props: ComponentPropsWithRef<"h1">, heading: {
24
+ level: Level;
25
+ tag: Tag;
26
+ }) => ReactElement;
27
27
  } & import("react").RefAttributes<HTMLHeadingElement>>;
28
28
  /**
29
29
  * Renders headings inside `offset` levels deeper (default 1), or at `level`
30
30
  * regardless of ancestors.
31
31
  */
32
- export declare function HeadingLevel({ offset, level, children, }: HeadingLevelProps): import("react").JSX.Element;
32
+ export declare function HeadingLevel({ offset, level, children }: HeadingLevelProps): import("react").JSX.Element;
33
33
  /**
34
34
  * Returns the level and tag a `Heading` would render at this position.
35
35
  */
36
36
  export declare function useHeadingLevel(): {
37
- level: Level;
38
- tag: Tag;
37
+ level: Level;
38
+ tag: Tag;
39
39
  };
40
- export {};
40
+ export {};
@@ -1,103 +1,135 @@
1
1
  "use client";
2
-
3
- // src/index.tsx
4
- import {
5
- createContext,
6
- forwardRef,
7
- useContext,
8
- useEffect,
9
- useMemo
10
- } from "react";
2
+ import { createContext, forwardRef, useContext, useEffect, useMemo } from "react";
11
3
  import { jsx } from "react/jsx-runtime";
12
- var LevelContext = createContext(1);
4
+ // src/index.tsx
5
+ const LevelContext = createContext(1);
13
6
  function isValidOffset(offset) {
14
- return Number.isInteger(offset) && offset >= 0;
7
+ return Number.isInteger(offset) && offset >= 0;
15
8
  }
16
9
  function isValidLevel(level) {
17
- return Number.isInteger(level) && level >= 1;
10
+ return Number.isInteger(level) && level >= 1;
18
11
  }
19
- var Heading = forwardRef(
20
- function({ render, ...props }, ref) {
21
- let { level, tag } = useHeadingLevel();
22
- return useHeadingChecks(), render ? render({ ...props, ref }, { level, tag }) : /* @__PURE__ */ jsx(tag, { ref, ...props });
23
- }
24
- );
12
+ /**
13
+ * Renders an `h1`–`h6` according to the current level.
14
+ */
15
+ const Heading = forwardRef(function Heading({ render, ...props }, ref) {
16
+ const { level, tag } = useHeadingLevel();
17
+ useHeadingChecks();
18
+ if (render) return render({
19
+ ...props,
20
+ ref
21
+ }, {
22
+ level,
23
+ tag
24
+ });
25
+ return /* @__PURE__ */ jsx(tag, {
26
+ ref,
27
+ ...props
28
+ });
29
+ });
25
30
  Heading.displayName = "Heading";
26
- function HeadingLevel({
27
- offset = 1,
28
- level,
29
- children
30
- }) {
31
- let parentLevel = useContext(LevelContext), isReset = isValidLevel(level), isOffsetValid = isValidOffset(offset), innerLevel = isReset ? level : parentLevel + (isOffsetValid ? offset : 1);
32
- return useInvalidPropWarning("offset", offset, isOffsetValid), useInvalidPropWarning("level", level, isReset), children = /* @__PURE__ */ jsx(
33
- SkippedLevelScope,
34
- {
35
- firstLevel: isReset ? innerLevel : parentLevel + 1,
36
- isIsolated: isReset,
37
- isPassthrough: !isReset && offset === 0,
38
- children
39
- }
40
- ), /* @__PURE__ */ jsx(LevelContext.Provider, { value: innerLevel, children });
31
+ /**
32
+ * Renders headings inside `offset` levels deeper (default 1), or at `level`
33
+ * regardless of ancestors.
34
+ */
35
+ function HeadingLevel({ offset = 1, level, children }) {
36
+ const parentLevel = useContext(LevelContext);
37
+ const isReset = isValidLevel(level);
38
+ const isOffsetValid = isValidOffset(offset);
39
+ const innerLevel = isReset ? level : parentLevel + (isOffsetValid ? offset : 1);
40
+ useInvalidPropWarning("offset", offset, isOffsetValid);
41
+ useInvalidPropWarning("level", level, isReset);
42
+ children = /* @__PURE__ */ jsx(SkippedLevelScope, {
43
+ firstLevel: isReset ? innerLevel : parentLevel + 1,
44
+ isIsolated: isReset,
45
+ isPassthrough: !isReset && offset === 0,
46
+ children
47
+ });
48
+ return /* @__PURE__ */ jsx(LevelContext.Provider, {
49
+ value: innerLevel,
50
+ children
51
+ });
41
52
  }
53
+ /**
54
+ * Returns the level and tag a `Heading` would render at this position.
55
+ */
42
56
  function useHeadingLevel() {
43
- let level = Math.min(useContext(LevelContext), 6);
44
- return { level, tag: `h${level}` };
57
+ const level = Math.min(useContext(LevelContext), 6);
58
+ return {
59
+ level,
60
+ tag: `h${level}`
61
+ };
45
62
  }
46
- var ScopeContext = /* @__PURE__ */ createContext(null);
63
+ const ScopeContext = /* @__PURE__ */ createContext(null);
64
+ /**
65
+ * Collects the heading levels rendered inside a HeadingLevel and warns if one between `firstLevel` and the deepest is missing. A
66
+ * non-empty nested scope counts as a heading at its own `firstLevel` in its
67
+ * parent, so each gap is reported by exactly one scope.
68
+ */
47
69
  function createScopeRegistry(firstLevel, parentRegistry) {
48
- let headingLevels = /* @__PURE__ */ new Map(), unregisterFromParent, isCheckScheduled = !1, hasWarned = !1;
49
- function warnIfLevelSkipped() {
50
- if (isCheckScheduled = !1, hasWarned || headingLevels.size === 0) return;
51
- let presentLevels = new Set(headingLevels.values()), deepestLevel = Math.max(...presentLevels);
52
- for (let level = firstLevel; level < deepestLevel; level++)
53
- if (!presentLevels.has(level)) {
54
- hasWarned = !0, console.warn(
55
- `react-headings: Skipped heading level ${level}: found a level ${deepestLevel} heading but no level ${level} heading.`
56
- );
57
- return;
58
- }
59
- }
60
- function scheduleCheck() {
61
- isCheckScheduled || (isCheckScheduled = !0, queueMicrotask(warnIfLevelSkipped));
62
- }
63
- return {
64
- register(level) {
65
- let token = {};
66
- return headingLevels.set(token, level), headingLevels.size === 1 && (unregisterFromParent = parentRegistry?.register(firstLevel)), scheduleCheck(), () => {
67
- headingLevels.delete(token), headingLevels.size === 0 && (unregisterFromParent?.(), unregisterFromParent = void 0), scheduleCheck();
68
- };
69
- }
70
- };
70
+ const headingLevels = /* @__PURE__ */ new Map();
71
+ let unregisterFromParent;
72
+ let isCheckScheduled = false;
73
+ let hasWarned = false;
74
+ function warnIfLevelSkipped() {
75
+ isCheckScheduled = false;
76
+ if (hasWarned || headingLevels.size === 0) return;
77
+ const presentLevels = new Set(headingLevels.values());
78
+ const deepestLevel = Math.max(...presentLevels);
79
+ for (let level = firstLevel; level < deepestLevel; level++) if (!presentLevels.has(level)) {
80
+ hasWarned = true;
81
+ console.warn(`react-headings: Skipped heading level ${level}: found a level ${deepestLevel} heading but no level ${level} heading.`);
82
+ return;
83
+ }
84
+ }
85
+ function scheduleCheck() {
86
+ if (isCheckScheduled) return;
87
+ isCheckScheduled = true;
88
+ queueMicrotask(warnIfLevelSkipped);
89
+ }
90
+ return { register(level) {
91
+ const token = {};
92
+ headingLevels.set(token, level);
93
+ if (headingLevels.size === 1) unregisterFromParent = parentRegistry?.register(firstLevel);
94
+ scheduleCheck();
95
+ return () => {
96
+ headingLevels.delete(token);
97
+ if (headingLevels.size === 0) {
98
+ unregisterFromParent?.();
99
+ unregisterFromParent = void 0;
100
+ }
101
+ scheduleCheck();
102
+ };
103
+ } };
71
104
  }
72
- function SkippedLevelScope({
73
- firstLevel,
74
- isIsolated,
75
- isPassthrough,
76
- children
77
- }) {
78
- let parentRegistry = useContext(ScopeContext), registry = useMemo(
79
- () => isPassthrough ? parentRegistry : createScopeRegistry(firstLevel, isIsolated ? null : parentRegistry),
80
- [firstLevel, isIsolated, isPassthrough, parentRegistry]
81
- );
82
- return /* @__PURE__ */ jsx(ScopeContext.Provider, { value: registry, children });
105
+ function SkippedLevelScope({ firstLevel, isIsolated, isPassthrough, children }) {
106
+ const parentRegistry = useContext(ScopeContext);
107
+ const registry = useMemo(() => isPassthrough ? parentRegistry : createScopeRegistry(firstLevel, isIsolated ? null : parentRegistry), [
108
+ firstLevel,
109
+ isIsolated,
110
+ isPassthrough,
111
+ parentRegistry
112
+ ]);
113
+ return /* @__PURE__ */ jsx(ScopeContext.Provider, {
114
+ value: registry,
115
+ children
116
+ });
83
117
  }
84
118
  function useHeadingChecks() {
85
- let level = useContext(LevelContext), scopeRegistry = useContext(ScopeContext);
86
- useEffect(() => {
87
- level > 6 && console.warn(
88
- `react-headings: Heading level ${level} exceeds 6 and was clamped to h6.`
89
- );
90
- }, [level]), useEffect(() => scopeRegistry?.register(level), [scopeRegistry, level]);
119
+ const level = useContext(LevelContext);
120
+ const scopeRegistry = useContext(ScopeContext);
121
+ useEffect(() => {
122
+ if (level > 6) console.warn(`react-headings: Heading level ${level} exceeds 6 and was clamped to h6.`);
123
+ }, [level]);
124
+ useEffect(() => scopeRegistry?.register(level), [scopeRegistry, level]);
91
125
  }
92
126
  function useInvalidPropWarning(prop, value, isValid) {
93
- useEffect(() => {
94
- value != null && !isValid && console.warn(
95
- `react-headings: HeadingLevel received invalid ${prop} ${value}, ignored.`
96
- );
97
- }, [prop, value, isValid]);
127
+ useEffect(() => {
128
+ if (value != null && !isValid) console.warn(`react-headings: HeadingLevel received invalid ${prop} ${value}, ignored.`);
129
+ }, [
130
+ prop,
131
+ value,
132
+ isValid
133
+ ]);
98
134
  }
99
- export {
100
- Heading,
101
- HeadingLevel,
102
- useHeadingLevel
103
- };
135
+ export { Heading, HeadingLevel, useHeadingLevel };
package/dist/index.js CHANGED
@@ -1,42 +1,54 @@
1
1
  "use client";
2
-
3
- // src/index.tsx
4
- import {
5
- createContext,
6
- forwardRef,
7
- useContext,
8
- useEffect,
9
- useMemo
10
- } from "react";
2
+ import { createContext, forwardRef, useContext } from "react";
11
3
  import { jsx } from "react/jsx-runtime";
12
- var LevelContext = createContext(1);
4
+ // src/index.tsx
5
+ const LevelContext = createContext(1);
13
6
  function isValidOffset(offset) {
14
- return Number.isInteger(offset) && offset >= 0;
7
+ return Number.isInteger(offset) && offset >= 0;
15
8
  }
16
9
  function isValidLevel(level) {
17
- return Number.isInteger(level) && level >= 1;
10
+ return Number.isInteger(level) && level >= 1;
18
11
  }
19
- var Heading = forwardRef(
20
- function({ render, ...props }, ref) {
21
- let { level, tag } = useHeadingLevel();
22
- return render ? render({ ...props, ref }, { level, tag }) : /* @__PURE__ */ jsx(tag, { ref, ...props });
23
- }
24
- );
12
+ /**
13
+ * Renders an `h1`–`h6` according to the current level.
14
+ */
15
+ const Heading = forwardRef(function Heading({ render, ...props }, ref) {
16
+ const { level, tag } = useHeadingLevel();
17
+ if (render) return render({
18
+ ...props,
19
+ ref
20
+ }, {
21
+ level,
22
+ tag
23
+ });
24
+ return /* @__PURE__ */ jsx(tag, {
25
+ ref,
26
+ ...props
27
+ });
28
+ });
25
29
  Heading.displayName = "Heading";
26
- function HeadingLevel({
27
- offset = 1,
28
- level,
29
- children
30
- }) {
31
- let parentLevel = useContext(LevelContext), isReset = isValidLevel(level), isOffsetValid = isValidOffset(offset), innerLevel = isReset ? level : parentLevel + (isOffsetValid ? offset : 1);
32
- return /* @__PURE__ */ jsx(LevelContext.Provider, { value: innerLevel, children });
30
+ /**
31
+ * Renders headings inside `offset` levels deeper (default 1), or at `level`
32
+ * regardless of ancestors.
33
+ */
34
+ function HeadingLevel({ offset = 1, level, children }) {
35
+ const parentLevel = useContext(LevelContext);
36
+ const isReset = isValidLevel(level);
37
+ const isOffsetValid = isValidOffset(offset);
38
+ const innerLevel = isReset ? level : parentLevel + (isOffsetValid ? offset : 1);
39
+ return /* @__PURE__ */ jsx(LevelContext.Provider, {
40
+ value: innerLevel,
41
+ children
42
+ });
33
43
  }
44
+ /**
45
+ * Returns the level and tag a `Heading` would render at this position.
46
+ */
34
47
  function useHeadingLevel() {
35
- let level = Math.min(useContext(LevelContext), 6);
36
- return { level, tag: `h${level}` };
48
+ const level = Math.min(useContext(LevelContext), 6);
49
+ return {
50
+ level,
51
+ tag: `h${level}`
52
+ };
37
53
  }
38
- export {
39
- Heading,
40
- HeadingLevel,
41
- useHeadingLevel
42
- };
54
+ export { Heading, HeadingLevel, useHeadingLevel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-headings",
3
- "version": "4.0.1",
3
+ "version": "4.0.3",
4
4
  "type": "module",
5
5
  "description": "Dynamic heading levels for React.",
6
6
  "author": "Alex Nault",
@@ -29,7 +29,7 @@
29
29
  "./package.json": "./package.json"
30
30
  },
31
31
  "scripts": {
32
- "build": "rm -rf dist && node scripts/build.js && tsc -p tsconfig.build.json",
32
+ "build": "tsdown",
33
33
  "typecheck": "tsc",
34
34
  "format": "prettier ./ --write",
35
35
  "lint": "eslint ./ --max-warnings=0",
@@ -48,21 +48,22 @@
48
48
  "@size-limit/preset-small-lib": "^14.3.0",
49
49
  "@testing-library/dom": "^10.4.2",
50
50
  "@testing-library/react": "^16.3.3",
51
+ "@types/node": "^24.19.2",
51
52
  "@types/react": "^19.3.0",
52
53
  "@types/react-dom": "^19.3.0",
53
- "@vitest/coverage-v8": "^5.0.5",
54
+ "@vitest/coverage-v8": "^5.1.0",
54
55
  "conventional-changelog-conventionalcommits": "^9.3.1",
55
- "esbuild": "^0.28.2",
56
56
  "eslint": "^10.14.0",
57
57
  "eslint-plugin-react-hooks": "^7.1.1",
58
58
  "jsdom": "^30.1.3",
59
59
  "prettier": "^3.9.10",
60
60
  "react": "^19.3.1",
61
61
  "react-dom": "^19.3.1",
62
- "semantic-release": "^25.0.3",
62
+ "semantic-release": "^25.0.9",
63
63
  "size-limit": "^14.3.0",
64
- "typescript": "^5.9.3",
64
+ "tsdown": "^0.23.5",
65
+ "typescript": "^6.0.3",
65
66
  "typescript-eslint": "^8.72.1",
66
- "vitest": "^5.0.5"
67
+ "vitest": "^5.1.0"
67
68
  }
68
69
  }