Uniform Arrays and Structures

One uniform3fv() call with 18 floats on the location of uColors (or uColors[0]) fills all six vec3 elements; the location of uColors[2] starts at the third, and values past the end are ignored. A structure has no location of its own: set each field by its full name, such as uLight.color or uLights[1].color in an array of structures. GLH.load() keys its setters by those names:

Setting a uniform array and a structure's fields by nameJavaScript
uniforms.uColors(GLH.covers.flatMap(GLH.rgb));          // 6 x vec3 = 18 floats
uniforms['uLight.color']([1.0, 0.95, 0.85]);            // struct Light { vec3 color; ...
uniforms['uLight.power'](1.2);                           //                float power; }

Uniform space is small: MAX_FRAGMENT_UNIFORM_VECTORS is 1024 here but only 224 is guaranteed, and each array element takes a whole four-component vector. Large data belongs in a uniform buffer (Light UBOs) or a texture.

A uniform vec3 array for cover colours and an array of Light structs set field by fieldHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; }
  canvas { display: block; width: 100%; max-width: 600px; }
</style>
<canvas id="view" width="600" height="240"></canvas>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
void main() { gl_Position = vec4(gl_VertexID == 1 ? 3.0 : -1.0, gl_VertexID == 2 ? 3.0 : -1.0, 0.0, 1.0); }`],
  [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float;
struct Light { vec2 position; vec3 color; float power; };
uniform Light uLights[2];     // an array of structures
uniform vec3 uColors[6];      // six vec3 elements: each takes a whole vec4 slot
out vec4 outColor;
void main() {
  vec2 p = gl_FragCoord.xy;
  int book = int(p.x / 100.0);
  vec2 local = vec2(mod(p.x, 100.0), p.y);
  bool cover = local.x > 16.0 && local.x < 84.0 && p.y > 30.0 && p.y < 210.0;
  vec3 base = cover ? uColors[book] * mix(0.55, 1.0, step(24.0, local.x)) : vec3(0.85, 0.82, 0.76);
  vec3 light = vec3(0.3);
  for (int i = 0; i < 2; i++) {
    float d = distance(p, uLights[i].position) / 150.0;
    light += uLights[i].color * uLights[i].power / (1.0 + d * d * 3.0);
  }
  outColor = vec4(base * light, 1.0);
}`]]) {
  const s = gl.createShader(type);
  gl.shaderSource(s, src); gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
  gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());

// One call fills all six elements: 18 floats on the location of uColors (or uColors[0]).
const covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const rgb = (hex) => [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);
gl.uniform3fv(gl.getUniformLocation(program, 'uColors'), covers.flatMap(rgb));

// A structure has no location of its own: every field is set by its full name.
const at = (name) => gl.getUniformLocation(program, name);
gl.uniform3f(at('uLights[0].color'), 1.0, 0.85, 0.6);
gl.uniform1f(at('uLights[0].power'), 1.0);
gl.uniform3f(at('uLights[1].color'), 0.5, 0.7, 1.0);
gl.uniform1f(at('uLights[1].power'), 0.8);
const positions = [at('uLights[0].position'), at('uLights[1].position')];

function frame(ms) {
  const t = ms * 0.0007;
  gl.uniform2f(positions[0], 300 + Math.cos(t) * 250, 200);
  gl.uniform2f(positions[1], 300 - Math.cos(t * 1.3) * 250, 60);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>