@liiift-studio/steadygray 0.0.1
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 +23 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +161 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Steady Gray
|
|
2
|
+
|
|
3
|
+
**[steadygray.com](https://steadygray.com)** · [npm](https://www.npmjs.com/package/@liiift-studio/steadygray) · [GitHub](https://github.com/Liiift-Studio/SteadyGray)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @liiift-studio/steadygray
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
See [steadygray.com](https://steadygray.com) for full API docs and a live demo.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Dev notes
|
|
18
|
+
|
|
19
|
+
### `next` in root devDependencies
|
|
20
|
+
|
|
21
|
+
`package.json` at the repo root lists `next` as a devDependency. This is a **Vercel detection workaround** — not a real dependency of the npm package. Vercel's build system inspects the root `package.json` to detect the framework; without `next` present it falls back to a static build and skips the Next.js pipeline, breaking the `/site` subdirectory deploy.
|
|
22
|
+
|
|
23
|
+
The package itself has zero runtime dependencies. Do not remove this entry.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const w=require("react"),W=require("react/jsx-runtime"),L={word:"gv-word",line:"gv-line"},C={targetDensity:"auto",method:"letter-spacing",maxAdjustment:.05,calibrationFactor:2};function z(o){const r=getComputedStyle(o),s=r.fontWeight,a=r.fontSize,t=r.fontFamily.split(",")[0].replace(/['"]/g,"").trim();return`${s} ${a} ${t}`}function v(o,r,s,a,t){const i=t.getContext("2d");if(!i)return 0;t.width=Math.max(1,Math.ceil(s)),t.height=Math.max(1,Math.ceil(a)),i.clearRect(0,0,t.width,t.height),i.fillStyle="white",i.fillRect(0,0,t.width,t.height),i.fillStyle="black",i.font=r,i.fillText(o,0,t.height*.75);const c=i.getImageData(0,0,t.width,t.height).data;let p=0;const f=t.width*t.height;for(let u=0;u<c.length;u+=4){const A=c[u],b=c[u+1],y=c[u+2];(A<200||b<200||y<200)&&p++}return f>0?p/f:0}function G(o){const r=o.cloneNode(!0);return r.querySelectorAll(`.${L.word}, .${L.line}`).forEach(a=>{const t=a.parentNode;if(t){for(;a.firstChild;)t.insertBefore(a.firstChild,a);t.removeChild(a)}}),r.querySelectorAll("br[data-gv-break]").forEach(a=>a.remove()),r.innerHTML}function V(o,r,s={},a){if(typeof window>"u")return;const t=window.scrollY,i=s.targetDensity??C.targetDensity,d=s.method??C.method,c=s.maxAdjustment??C.maxAdjustment,p=s.calibrationFactor??C.calibrationFactor;if(o.innerHTML=r,!o.offsetWidth){requestAnimationFrame(()=>{Math.abs(window.scrollY-t)>2&&window.scrollTo({top:t,behavior:"instant"})});return}const f=o.offsetWidth,u=parseFloat(getComputedStyle(o).fontSize)||16,A=z(o),b=[];(function n(e){e.nodeType===Node.TEXT_NODE?b.push(e):e.childNodes.forEach(n)})(o);const y=[];for(const n of b){const e=n.textContent??"";if(!e.trim())continue;const l=e.split(/(\S+)/),D=document.createDocumentFragment();for(let h=0;h<l.length;h+=2){const N=l[h],S=l[h+1];if(!S)continue;const M=l[h+3]===void 0?l[h+2]??"":"",x=document.createElement("span");x.className=L.word,x.style.cssText="display:inline-block;white-space:nowrap;",x.appendChild(document.createTextNode(N+S+M)),D.appendChild(x),y.push(x)}n.parentNode.replaceChild(D,n)}if(y.length===0){requestAnimationFrame(()=>{Math.abs(window.scrollY-t)>2&&window.scrollTo({top:t,behavior:"instant"})});return}const m=[];let F=null,g=null;for(const n of y){const e=n.getBoundingClientRect(),l=Math.round(e.top);F===null||l!==F?(F=l,g={text:"",width:e.width,height:e.height||u,spans:[n]},m.push(g)):(g.width+=e.width,e.height>g.height&&(g.height=e.height),g.spans.push(n))}for(const n of m)n.text=n.spans.map(e=>e.textContent??"").join("").trim();if(m.length===0){requestAnimationFrame(()=>{Math.abs(window.scrollY-t)>2&&window.scrollTo({top:t,behavior:"instant"})});return}const k=a??document.createElement("canvas"),T=m.map(n=>v(n.text,A,f,n.height||u,k));let E;if(typeof i=="number")E=i;else{const n=T.reduce((e,l)=>e+l,0);E=T.length>0?n/T.length:0}const Y=T.map(n=>{const e=(E-n)*p;return Math.max(-c,Math.min(c,e))});o.innerHTML=r;const H="display:inline-block;white-space:nowrap;vertical-align:top;";let R="";m.forEach((n,e)=>{const l=Y[e],h=`${H}${d==="word-spacing"?"word-spacing":"letter-spacing"}:${l}em;`,N=n.spans.map((S,j)=>{const M=S.textContent??"";return j===0?M.replace(/^[^\S\u00a0]+/,""):M}).join("");R+=`<span class="${L.line}" style="${h}">${N}</span>`,e<m.length-1&&(R+='<br data-gv-break="1">')}),o.innerHTML=R,requestAnimationFrame(()=>{Math.abs(window.scrollY-t)>2&&window.scrollTo({top:t,behavior:"instant"})})}function P(o,r){o.innerHTML=r}function $(o){const r=w.useRef(null),s=w.useRef(null),a=w.useRef(o);a.current=o;const t=w.useCallback(()=>{const i=r.current;i&&(s.current===null&&(s.current=G(i)),V(i,s.current,a.current))},[]);return w.useLayoutEffect(()=>{t();let i=0,d=0;const c=new ResizeObserver(p=>{const f=Math.round(p[0].contentRect.width);f!==i&&(i=f,cancelAnimationFrame(d),d=requestAnimationFrame(t))});return c.observe(r.current),()=>{c.disconnect(),cancelAnimationFrame(d)}},[t]),r}const q=w.forwardRef(function({children:r,className:s,style:a,as:t="p",...i},d){const c=$(i);return W.jsx(t,{ref:c,className:s,style:a,children:r})});q.displayName="GrayValueText";exports.GrayValueText=q;exports.applyGrayValue=V;exports.getCleanHTML=G;exports.measureLineDensity=v;exports.removeGrayValue=P;exports.useGrayValue=$;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { default as default_2 } from 'react';
|
|
2
|
+
import { RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Applies gray-value optical density equalization to an element.
|
|
6
|
+
*
|
|
7
|
+
* Algorithm (7 passes):
|
|
8
|
+
* 1. Reset — restore original HTML
|
|
9
|
+
* 2. Word wrap — wrap each word in a gv-word span
|
|
10
|
+
* 3. Read phase — group words into visual lines via BCR
|
|
11
|
+
* 4. Measure — render each line to Canvas and compute density ratio
|
|
12
|
+
* 5. Target — compute target density (average or user-specified)
|
|
13
|
+
* 6. Adjust — calculate per-line letter-spacing via linear approximation
|
|
14
|
+
* 7. Write — rebuild HTML with gv-line spans carrying adjusted spacing
|
|
15
|
+
*
|
|
16
|
+
* Note: binary-search refinement of the spacing value is a future enhancement.
|
|
17
|
+
* The linear approximation (calibrationFactor × density delta) is sufficient for v1
|
|
18
|
+
* because relative density comparison between lines is what matters most.
|
|
19
|
+
*
|
|
20
|
+
* @param element - Live DOM element to adjust (must be rendered and visible)
|
|
21
|
+
* @param originalHTML - HTML snapshot taken before the first adjustment run
|
|
22
|
+
* @param options - GrayValueOptions (merged with defaults)
|
|
23
|
+
* @param _canvas - Optional injectable Canvas for testing (creates one if absent)
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyGrayValue(element: HTMLElement, originalHTML: string, options?: GrayValueOptions, _canvas?: HTMLCanvasElement): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the innerHTML of an element with all gray-value injected spans removed,
|
|
29
|
+
* unwrapping their children in place. Safe for complex markup. Idempotent.
|
|
30
|
+
*
|
|
31
|
+
* @param el - Element that may contain gray-value markup
|
|
32
|
+
*/
|
|
33
|
+
export declare function getCleanHTML(el: HTMLElement): string;
|
|
34
|
+
|
|
35
|
+
/** Options controlling the gray-value density-equalization algorithm */
|
|
36
|
+
export declare interface GrayValueOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Target optical density ratio (0–1).
|
|
39
|
+
* 'auto' = average of all measured line densities (default).
|
|
40
|
+
*/
|
|
41
|
+
targetDensity?: number | 'auto';
|
|
42
|
+
/**
|
|
43
|
+
* Which CSS spacing property to adjust per line.
|
|
44
|
+
* Default: 'letter-spacing'.
|
|
45
|
+
*/
|
|
46
|
+
method?: 'letter-spacing' | 'word-spacing';
|
|
47
|
+
/**
|
|
48
|
+
* Maximum spacing adjustment in em units.
|
|
49
|
+
* Positive and negative adjustments are both clamped to this magnitude.
|
|
50
|
+
* Default: 0.05
|
|
51
|
+
*/
|
|
52
|
+
maxAdjustment?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Acceptable density difference before a line is considered equalized.
|
|
55
|
+
* Default: 0.01
|
|
56
|
+
*/
|
|
57
|
+
tolerance?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Linear scaling factor: em spacing change per 1.0 density unit difference.
|
|
60
|
+
* Increase to apply stronger corrections, decrease to be more conservative.
|
|
61
|
+
* Default: 2.0
|
|
62
|
+
*/
|
|
63
|
+
calibrationFactor?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Drop-in component that applies the gray-value effect to its children.
|
|
68
|
+
*/
|
|
69
|
+
export declare const GrayValueText: default_2.ForwardRefExoticComponent<GrayValueTextProps & default_2.RefAttributes<HTMLElement>>;
|
|
70
|
+
|
|
71
|
+
declare interface GrayValueTextProps extends GrayValueOptions {
|
|
72
|
+
children: default_2.ReactNode;
|
|
73
|
+
className?: string;
|
|
74
|
+
style?: default_2.CSSProperties;
|
|
75
|
+
as?: default_2.ElementType;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Measures the optical density (ink pixel ratio) of a single line of text
|
|
80
|
+
* by rendering it to a Canvas and counting non-white pixels.
|
|
81
|
+
*
|
|
82
|
+
* Returns a value in [0, 1] where 0 = no ink and 1 = fully black.
|
|
83
|
+
*
|
|
84
|
+
* @param text - The text content of the line
|
|
85
|
+
* @param fontStyle - Canvas-compatible font string (e.g. "400 18px Georgia")
|
|
86
|
+
* @param targetWidth - Width of the canvas in CSS pixels
|
|
87
|
+
* @param lineHeight - Height of the canvas in CSS pixels
|
|
88
|
+
* @param canvas - Canvas element to render into (reused across calls)
|
|
89
|
+
*/
|
|
90
|
+
export declare function measureLineDensity(text: string, fontStyle: string, targetWidth: number, lineHeight: number, canvas: HTMLCanvasElement): number;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove gray-value markup and restore original HTML.
|
|
94
|
+
*
|
|
95
|
+
* @param element - Element previously adjusted by applyGrayValue
|
|
96
|
+
* @param originalHTML - The clean HTML snapshot passed to applyGrayValue
|
|
97
|
+
*/
|
|
98
|
+
export declare function removeGrayValue(element: HTMLElement, originalHTML: string): void;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* React hook that applies the gray-value effect to a ref'd element.
|
|
102
|
+
* Automatically re-runs on resize (width changes only).
|
|
103
|
+
*/
|
|
104
|
+
export declare function useGrayValue(options: GrayValueOptions): RefObject<HTMLElement | null>;
|
|
105
|
+
|
|
106
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useRef as R, useCallback as Y, useLayoutEffect as q, forwardRef as G } from "react";
|
|
2
|
+
import { jsx as V } from "react/jsx-runtime";
|
|
3
|
+
const A = {
|
|
4
|
+
word: "gv-word",
|
|
5
|
+
line: "gv-line"
|
|
6
|
+
}, M = {
|
|
7
|
+
targetDensity: "auto",
|
|
8
|
+
method: "letter-spacing",
|
|
9
|
+
maxAdjustment: 0.05,
|
|
10
|
+
calibrationFactor: 2
|
|
11
|
+
};
|
|
12
|
+
function W(o) {
|
|
13
|
+
const i = getComputedStyle(o), a = i.fontWeight, s = i.fontSize, t = i.fontFamily.split(",")[0].replace(/['"]/g, "").trim();
|
|
14
|
+
return `${a} ${s} ${t}`;
|
|
15
|
+
}
|
|
16
|
+
function H(o, i, a, s, t) {
|
|
17
|
+
const r = t.getContext("2d");
|
|
18
|
+
if (!r) return 0;
|
|
19
|
+
t.width = Math.max(1, Math.ceil(a)), t.height = Math.max(1, Math.ceil(s)), r.clearRect(0, 0, t.width, t.height), r.fillStyle = "white", r.fillRect(0, 0, t.width, t.height), r.fillStyle = "black", r.font = i, r.fillText(o, 0, t.height * 0.75);
|
|
20
|
+
const c = r.getImageData(0, 0, t.width, t.height).data;
|
|
21
|
+
let p = 0;
|
|
22
|
+
const f = t.width * t.height;
|
|
23
|
+
for (let h = 0; h < c.length; h += 4) {
|
|
24
|
+
const C = c[h], x = c[h + 1], w = c[h + 2];
|
|
25
|
+
(C < 200 || x < 200 || w < 200) && p++;
|
|
26
|
+
}
|
|
27
|
+
return f > 0 ? p / f : 0;
|
|
28
|
+
}
|
|
29
|
+
function z(o) {
|
|
30
|
+
const i = o.cloneNode(!0);
|
|
31
|
+
return i.querySelectorAll(
|
|
32
|
+
`.${A.word}, .${A.line}`
|
|
33
|
+
).forEach((s) => {
|
|
34
|
+
const t = s.parentNode;
|
|
35
|
+
if (t) {
|
|
36
|
+
for (; s.firstChild; ) t.insertBefore(s.firstChild, s);
|
|
37
|
+
t.removeChild(s);
|
|
38
|
+
}
|
|
39
|
+
}), i.querySelectorAll("br[data-gv-break]").forEach((s) => s.remove()), i.innerHTML;
|
|
40
|
+
}
|
|
41
|
+
function _(o, i, a = {}, s) {
|
|
42
|
+
if (typeof window > "u") return;
|
|
43
|
+
const t = window.scrollY, r = a.targetDensity ?? M.targetDensity, d = a.method ?? M.method, c = a.maxAdjustment ?? M.maxAdjustment, p = a.calibrationFactor ?? M.calibrationFactor;
|
|
44
|
+
if (o.innerHTML = i, !o.offsetWidth) {
|
|
45
|
+
requestAnimationFrame(() => {
|
|
46
|
+
Math.abs(window.scrollY - t) > 2 && window.scrollTo({ top: t, behavior: "instant" });
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const f = o.offsetWidth, h = parseFloat(getComputedStyle(o).fontSize) || 16, C = W(o), x = [];
|
|
51
|
+
(function n(e) {
|
|
52
|
+
e.nodeType === Node.TEXT_NODE ? x.push(e) : e.childNodes.forEach(n);
|
|
53
|
+
})(o);
|
|
54
|
+
const w = [];
|
|
55
|
+
for (const n of x) {
|
|
56
|
+
const e = n.textContent ?? "";
|
|
57
|
+
if (!e.trim()) continue;
|
|
58
|
+
const l = e.split(/(\S+)/), N = document.createDocumentFragment();
|
|
59
|
+
for (let u = 0; u < l.length; u += 2) {
|
|
60
|
+
const D = l[u], T = l[u + 1];
|
|
61
|
+
if (!T) continue;
|
|
62
|
+
const S = l[u + 3] === void 0 ? l[u + 2] ?? "" : "", y = document.createElement("span");
|
|
63
|
+
y.className = A.word, y.style.cssText = "display:inline-block;white-space:nowrap;", y.appendChild(document.createTextNode(D + T + S)), N.appendChild(y), w.push(y);
|
|
64
|
+
}
|
|
65
|
+
n.parentNode.replaceChild(N, n);
|
|
66
|
+
}
|
|
67
|
+
if (w.length === 0) {
|
|
68
|
+
requestAnimationFrame(() => {
|
|
69
|
+
Math.abs(window.scrollY - t) > 2 && window.scrollTo({ top: t, behavior: "instant" });
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const m = [];
|
|
74
|
+
let F = null, g = null;
|
|
75
|
+
for (const n of w) {
|
|
76
|
+
const e = n.getBoundingClientRect(), l = Math.round(e.top);
|
|
77
|
+
F === null || l !== F ? (F = l, g = {
|
|
78
|
+
text: "",
|
|
79
|
+
width: e.width,
|
|
80
|
+
height: e.height || h,
|
|
81
|
+
spans: [n]
|
|
82
|
+
}, m.push(g)) : (g.width += e.width, e.height > g.height && (g.height = e.height), g.spans.push(n));
|
|
83
|
+
}
|
|
84
|
+
for (const n of m)
|
|
85
|
+
n.text = n.spans.map((e) => e.textContent ?? "").join("").trim();
|
|
86
|
+
if (m.length === 0) {
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
Math.abs(window.scrollY - t) > 2 && window.scrollTo({ top: t, behavior: "instant" });
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const j = s ?? document.createElement("canvas"), b = m.map(
|
|
93
|
+
(n) => H(
|
|
94
|
+
n.text,
|
|
95
|
+
C,
|
|
96
|
+
f,
|
|
97
|
+
n.height || h,
|
|
98
|
+
j
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
let L;
|
|
102
|
+
if (typeof r == "number")
|
|
103
|
+
L = r;
|
|
104
|
+
else {
|
|
105
|
+
const n = b.reduce((e, l) => e + l, 0);
|
|
106
|
+
L = b.length > 0 ? n / b.length : 0;
|
|
107
|
+
}
|
|
108
|
+
const v = b.map((n) => {
|
|
109
|
+
const e = (L - n) * p;
|
|
110
|
+
return Math.max(-c, Math.min(c, e));
|
|
111
|
+
});
|
|
112
|
+
o.innerHTML = i;
|
|
113
|
+
const k = "display:inline-block;white-space:nowrap;vertical-align:top;";
|
|
114
|
+
let E = "";
|
|
115
|
+
m.forEach((n, e) => {
|
|
116
|
+
const l = v[e], u = `${k}${d === "word-spacing" ? "word-spacing" : "letter-spacing"}:${l}em;`, D = n.spans.map((T, $) => {
|
|
117
|
+
const S = T.textContent ?? "";
|
|
118
|
+
return $ === 0 ? S.replace(/^[^\S\u00a0]+/, "") : S;
|
|
119
|
+
}).join("");
|
|
120
|
+
E += `<span class="${A.line}" style="${u}">${D}</span>`, e < m.length - 1 && (E += '<br data-gv-break="1">');
|
|
121
|
+
}), o.innerHTML = E, requestAnimationFrame(() => {
|
|
122
|
+
Math.abs(window.scrollY - t) > 2 && window.scrollTo({ top: t, behavior: "instant" });
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function U(o, i) {
|
|
126
|
+
o.innerHTML = i;
|
|
127
|
+
}
|
|
128
|
+
function P(o) {
|
|
129
|
+
const i = R(null), a = R(null), s = R(o);
|
|
130
|
+
s.current = o;
|
|
131
|
+
const t = Y(() => {
|
|
132
|
+
const r = i.current;
|
|
133
|
+
r && (a.current === null && (a.current = z(r)), _(r, a.current, s.current));
|
|
134
|
+
}, []);
|
|
135
|
+
return q(() => {
|
|
136
|
+
t();
|
|
137
|
+
let r = 0, d = 0;
|
|
138
|
+
const c = new ResizeObserver((p) => {
|
|
139
|
+
const f = Math.round(p[0].contentRect.width);
|
|
140
|
+
f !== r && (r = f, cancelAnimationFrame(d), d = requestAnimationFrame(t));
|
|
141
|
+
});
|
|
142
|
+
return c.observe(i.current), () => {
|
|
143
|
+
c.disconnect(), cancelAnimationFrame(d);
|
|
144
|
+
};
|
|
145
|
+
}, [t]), i;
|
|
146
|
+
}
|
|
147
|
+
const I = G(
|
|
148
|
+
function({ children: i, className: a, style: s, as: t = "p", ...r }, d) {
|
|
149
|
+
const c = P(r);
|
|
150
|
+
return /* @__PURE__ */ V(t, { ref: c, className: a, style: s, children: i });
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
I.displayName = "GrayValueText";
|
|
154
|
+
export {
|
|
155
|
+
I as GrayValueText,
|
|
156
|
+
_ as applyGrayValue,
|
|
157
|
+
z as getCleanHTML,
|
|
158
|
+
H as measureLineDensity,
|
|
159
|
+
U as removeGrayValue,
|
|
160
|
+
P as useGrayValue
|
|
161
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liiift-studio/steadygray",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Paragraph optical density equalization — measure actual glyph area per line and equalize visual density across all lines",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"test": "vitest",
|
|
22
|
+
"test:run": "vitest run",
|
|
23
|
+
"lint": "tsc --noEmit",
|
|
24
|
+
"prepublishOnly": "npm run test:run && npm run build"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": ">=17",
|
|
28
|
+
"react-dom": ">=17"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"react": {
|
|
32
|
+
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"react-dom": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^19.0.0",
|
|
40
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
41
|
+
"happy-dom": "^12.0.0",
|
|
42
|
+
"next": "16.2.2",
|
|
43
|
+
"react": "^19.0.0",
|
|
44
|
+
"typescript": "^5.0.0",
|
|
45
|
+
"vite": "^6.0.0",
|
|
46
|
+
"vite-plugin-dts": "^4.0.0",
|
|
47
|
+
"vitest": "^3.0.0"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"typography",
|
|
51
|
+
"gray-value",
|
|
52
|
+
"optical-density",
|
|
53
|
+
"typesetting",
|
|
54
|
+
"react",
|
|
55
|
+
"typescript"
|
|
56
|
+
],
|
|
57
|
+
"author": "Quinn Keaveney <quinn@liiift.studio>",
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"homepage": "https://steadygray.com",
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "https://github.com/Liiift-Studio/SteadyGray.git"
|
|
63
|
+
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/Liiift-Studio/SteadyGray/issues"
|
|
66
|
+
},
|
|
67
|
+
"sideEffects": false
|
|
68
|
+
}
|