@empoweredvote/ev-ui 0.8.11 → 0.8.13

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/dist/index.mjs CHANGED
@@ -1,6 +1,34 @@
1
1
  // src/RadarChartCore.jsx
2
- import React, { useState, useRef, useEffect } from "react";
2
+ import React from "react";
3
3
  import { animated, useSpring } from "@react-spring/web";
4
+ import { useRef, useEffect as useEffect2 } from "react";
5
+
6
+ // src/useTheme.js
7
+ import { useState, useEffect } from "react";
8
+ function useTheme() {
9
+ const read = () => {
10
+ if (typeof document === "undefined") return false;
11
+ const root = document.documentElement;
12
+ const body = document.body;
13
+ const hasDarkClass = (el) => el && el.classList && el.classList.contains("dark");
14
+ const hasDarkAttr = (el) => el && el.getAttribute && el.getAttribute("data-theme") === "dark";
15
+ return hasDarkClass(root) || hasDarkAttr(root) || hasDarkClass(body) || hasDarkAttr(body);
16
+ };
17
+ const [isDark, setIsDark] = useState(read);
18
+ useEffect(() => {
19
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
20
+ const update = () => setIsDark(read());
21
+ update();
22
+ const observer = new MutationObserver(update);
23
+ const opts = { attributes: true, attributeFilter: ["class", "data-theme"] };
24
+ observer.observe(document.documentElement, opts);
25
+ if (document.body) observer.observe(document.body, opts);
26
+ return () => observer.disconnect();
27
+ }, []);
28
+ return { isDark };
29
+ }
30
+
31
+ // src/RadarChartCore.jsx
4
32
  function RadarChartCore({
5
33
  topics,
6
34
  data,
@@ -12,95 +40,83 @@ function RadarChartCore({
12
40
  onReplaceTopic = () => {
13
41
  },
14
42
  size = 400,
15
- labelFontSize = 13,
43
+ labelFontSize = 18,
16
44
  padding = 80,
17
- labelOffset = 52,
18
- tightFit = false,
19
- activeSpoke = null,
20
- writeIns = {},
21
- darkMode = false,
22
- lineColor,
23
- ringColor,
24
- // replacedSpokes: short_titles of substitute spokes (not the user's original defaults)
25
- replacedSpokes = {},
26
- // boldOriginalSpokes: when true, bold spokes that are NOT replacements
27
- boldOriginalSpokes = false,
28
- highlightedSpoke = null
45
+ labelOffset = 20,
46
+ tightFit = false
29
47
  }) {
30
- var _a;
48
+ const { isDark } = useTheme();
31
49
  const radius = size / 2 - 40;
32
- const extRadius = radius * 1.15;
33
50
  const centerX = size / 2;
34
51
  const centerY = size / 2;
35
- const [tooltip, setTooltip] = useState(null);
36
52
  const spokes = Object.entries(data);
37
53
  const numSpokes = Math.max(spokes.length, 1);
38
54
  const prevCountRef = useRef(numSpokes);
39
55
  const countChanged = numSpokes !== prevCountRef.current;
40
- useEffect(() => {
56
+ useEffect2(() => {
41
57
  prevCountRef.current = numSpokes;
42
58
  }, [numSpokes]);
43
- function calcAdjusted(value, shortTitle) {
44
- var _a2;
45
- if (!value || Number(value) === 0) return 0;
46
- const topic = topics.find((t) => t.short_title === shortTitle);
47
- const max = ((_a2 = topic == null ? void 0 : topic.stances) == null ? void 0 : _a2.length) || 10;
48
- const pct = Number(value) / max * 10;
49
- return invertedSpokes[shortTitle] ? (max + 1) * 10 / max - pct : pct;
50
- }
51
59
  const pointsArr = spokes.map(([shortTitle, value], index) => {
52
- const adjusted = calcAdjusted(value, shortTitle);
53
- const r = adjusted / 10 * radius;
60
+ var _a;
61
+ const topic = topics.find((t) => t.short_title === shortTitle);
62
+ const max = ((_a = topic == null ? void 0 : topic.stances) == null ? void 0 : _a.length) || 10;
63
+ const pct = Math.min(value / max * 10, 10);
54
64
  const angle = 2 * Math.PI * index / numSpokes;
55
- return [centerX + r * Math.sin(angle), centerY - r * Math.cos(angle)];
65
+ const adjusted = value === 0 ? 0 : invertedSpokes[shortTitle] ? 11 - pct : pct;
66
+ const r = adjusted / 10 * radius;
67
+ const x = centerX + r * Math.sin(angle);
68
+ const y = centerY - r * Math.cos(angle);
69
+ return [x, y];
56
70
  });
57
71
  const targetPoints = pointsArr.map((p) => p.join(",")).join(" ");
58
- const springBase = {
72
+ const spring = useSpring({
59
73
  to: { points: targetPoints },
60
74
  immediate: countChanged,
75
+ reset: countChanged,
61
76
  config: { tension: 300, friction: 30 }
62
- };
63
- if (countChanged) springBase.from = { points: targetPoints };
64
- const spring = useSpring(springBase);
77
+ });
65
78
  const hasCompareData = Object.keys(compareData).length > 0;
66
79
  const centerPoints = spokes.map(() => `${centerX},${centerY}`).join(" ");
67
80
  let comparePoints = null;
68
81
  let compareCoords = null;
69
82
  if (hasCompareData) {
70
83
  const ccoords = spokes.map(([shortTitle], index) => {
71
- var _a2;
72
- const value = (_a2 = compareData[shortTitle]) != null ? _a2 : 0;
73
- const adjusted = calcAdjusted(value, shortTitle);
74
- const r = adjusted / 10 * radius;
84
+ var _a, _b;
85
+ const value = (_a = compareData[shortTitle]) != null ? _a : 0;
86
+ const topic = topics.find((t) => t.short_title === shortTitle);
87
+ const max = ((_b = topic == null ? void 0 : topic.stances) == null ? void 0 : _b.length) || 10;
88
+ const pct = Math.min(value / max * 10, 10);
75
89
  const angle = 2 * Math.PI * index / numSpokes;
76
- return [centerX + r * Math.sin(angle), centerY - r * Math.cos(angle)];
90
+ const adjusted = value === 0 ? 0 : invertedSpokes[shortTitle] ? 11 - pct : pct;
91
+ const r = adjusted / 10 * radius;
92
+ const x = centerX + r * Math.sin(angle);
93
+ const y = centerY - r * Math.cos(angle);
94
+ return [x, y];
77
95
  });
78
96
  compareCoords = ccoords;
79
97
  comparePoints = ccoords.map((p) => p.join(",")).join(" ");
80
98
  }
81
99
  const hadCompareDataRef = useRef(false);
82
100
  const compareJustAppeared = hasCompareData && !hadCompareDataRef.current;
83
- useEffect(() => {
101
+ useEffect2(() => {
84
102
  hadCompareDataRef.current = hasCompareData;
85
103
  }, [hasCompareData]);
86
- const compareSpringBase = {
104
+ const compareSpring = useSpring({
87
105
  to: { points: comparePoints || centerPoints || `${centerX},${centerY}` },
88
106
  immediate: countChanged || compareJustAppeared,
107
+ reset: countChanged || compareJustAppeared,
89
108
  config: { tension: 300, friction: 30 }
90
- };
91
- if (countChanged || compareJustAppeared) compareSpringBase.from = compareSpringBase.to;
92
- const compareSpring = useSpring(compareSpringBase);
93
- const spokeExtension = spokes.reduce((acc, [shortTitle]) => {
94
- const userAdj = calcAdjusted(data[shortTitle], shortTitle);
95
- const cmpAdj = hasCompareData && compareData[shortTitle] ? calcAdjusted(compareData[shortTitle], shortTitle) : 0;
96
- acc[shortTitle] = userAdj > 10 || cmpAdj > 10;
97
- return acc;
98
- }, {});
109
+ });
99
110
  const labelMeta = spokes.map(([shortTitle], i) => {
100
111
  const angle = 2 * Math.PI * i / numSpokes;
112
+ const baseFSize = labelFontSize;
101
113
  const lines = wrapLabel(shortTitle, 12);
102
- const longestLineLen = lines.reduce((m, ln) => ln.length > m ? ln.length : m, 0);
103
- const estimatedWidth = longestLineLen * labelFontSize * 0.55;
114
+ const longestLineLen = lines.reduce(
115
+ (max, ln) => ln.length > max ? ln.length : max,
116
+ 0
117
+ );
118
+ const fSize = adaptiveFontSize(longestLineLen, baseFSize);
119
+ const estimatedWidth = longestLineLen * fSize * 0.6;
104
120
  const sinA = Math.sin(angle);
105
121
  const side = sinA < -0.1 ? "left" : sinA > 0.1 ? "right" : "center";
106
122
  return { estimatedWidth, side };
@@ -109,9 +125,23 @@ function RadarChartCore({
109
125
  const rightLabelWidths = labelMeta.filter((m) => m.side === "right").map((m) => m.estimatedWidth);
110
126
  const minPadding = 40;
111
127
  const maxPadding = padding * 2;
112
- const rawLeft = Math.min(Math.max(leftLabelWidths.length ? Math.max(...leftLabelWidths) + labelOffset : 0, minPadding), maxPadding);
113
- const rawRight = Math.min(Math.max(rightLabelWidths.length ? Math.max(...rightLabelWidths) + labelOffset : 0, minPadding), maxPadding);
114
- const sidePadding = Math.max(rawLeft, rawRight);
128
+ const rawLeftPadding = Math.min(
129
+ Math.max(
130
+ leftLabelWidths.length ? Math.max(...leftLabelWidths) + labelOffset : 0,
131
+ minPadding
132
+ ),
133
+ maxPadding
134
+ );
135
+ const rawRightPadding = Math.min(
136
+ Math.max(
137
+ rightLabelWidths.length ? Math.max(...rightLabelWidths) + labelOffset : 0,
138
+ minPadding
139
+ ),
140
+ maxPadding
141
+ );
142
+ const symmetricPadding = Math.max(rawLeftPadding, rawRightPadding);
143
+ const leftPadding = symmetricPadding;
144
+ const rightPadding = symmetricPadding;
115
145
  const verticalPadding = padding;
116
146
  let tightTop = -verticalPadding;
117
147
  let tightHeight = size + verticalPadding * 2;
@@ -119,12 +149,11 @@ function RadarChartCore({
119
149
  let yMin = Infinity, yMax = -Infinity;
120
150
  for (let i = 0; i < numSpokes; i++) {
121
151
  const angle = 2 * Math.PI * i / numSpokes;
122
- const effectiveR = spokeExtension[(_a = spokes[i]) == null ? void 0 : _a[0]] ? extRadius : radius;
123
- const y = centerY - effectiveR * Math.cos(angle);
152
+ const y = centerY - radius * Math.cos(angle);
124
153
  if (y < yMin) yMin = y;
125
154
  if (y > yMax) yMax = y;
126
155
  }
127
- const labelMargin = labelOffset + labelFontSize * 2.5;
156
+ const labelMargin = labelOffset + labelFontSize * 1.4 + labelFontSize * 1.1;
128
157
  tightTop = yMin - labelMargin;
129
158
  tightHeight = yMax + labelMargin - tightTop;
130
159
  }
@@ -134,93 +163,75 @@ function RadarChartCore({
134
163
  const ring = [];
135
164
  for (let i = 0; i < numSpokes; i++) {
136
165
  const angle = 2 * Math.PI * i / numSpokes;
137
- ring.push(`${centerX + radius * scale * Math.sin(angle)},${centerY - radius * scale * Math.cos(angle)}`);
166
+ const x = centerX + radius * scale * Math.sin(angle);
167
+ const y = centerY - radius * scale * Math.cos(angle);
168
+ ring.push(`${x},${y}`);
138
169
  }
139
170
  guidePolygons.push(
140
- /* @__PURE__ */ React.createElement("polygon", { key: level, points: ring.join(" "), fill: "none", stroke: ringColor != null ? ringColor : darkMode ? "#555" : "#ccc" })
171
+ /* @__PURE__ */ React.createElement("polygon", { key: level, points: ring.join(" "), fill: "none", stroke: isDark ? "#374151" : "#ccc" })
141
172
  );
142
173
  }
143
- const getStanceText = (shortTitle, value) => {
144
- if (writeIns[shortTitle]) return writeIns[shortTitle];
145
- const topic = topics.find((t) => t.short_title === shortTitle);
146
- const stances = (topic == null ? void 0 : topic.stances) || [];
147
- const idx = Math.round(Number(value)) - 1;
148
- if (idx >= 0 && idx < stances.length) {
149
- return stances[idx].text || shortTitle;
150
- }
151
- return shortTitle;
152
- };
153
174
  return /* @__PURE__ */ React.createElement(
154
175
  "svg",
155
176
  {
156
177
  className: "w-full h-auto max-h-full",
157
- viewBox: `-${sidePadding} ${tightTop} ${size + sidePadding * 2} ${tightHeight}`,
158
- preserveAspectRatio: "xMidYMid meet",
159
- onMouseLeave: () => setTooltip(null)
178
+ viewBox: `-${leftPadding} ${tightTop} ${size + leftPadding + rightPadding} ${tightHeight}`,
179
+ preserveAspectRatio: "xMidYMid meet"
160
180
  },
161
181
  guidePolygons,
162
182
  spokes.map(([shortTitle], i) => {
163
183
  const angle = 2 * Math.PI * i / numSpokes;
164
- const endX = centerX + radius * Math.sin(angle);
165
- const endY = centerY - radius * Math.cos(angle);
184
+ const x = centerX + radius * Math.sin(angle);
185
+ const y = centerY - radius * Math.cos(angle);
166
186
  const isUnanswered = !!unansweredSpokes[shortTitle];
167
- const needsExt = spokeExtension[shortTitle];
168
- const extX = centerX + extRadius * Math.sin(angle);
169
- const extY = centerY - extRadius * Math.cos(angle);
170
- return /* @__PURE__ */ React.createElement("g", { key: `spoke-${shortTitle}` }, /* @__PURE__ */ React.createElement(
187
+ return /* @__PURE__ */ React.createElement(
171
188
  "line",
172
189
  {
190
+ key: `line-${shortTitle}`,
173
191
  x1: centerX,
174
192
  y1: centerY,
175
- x2: endX,
176
- y2: endY,
177
- stroke: isUnanswered ? "#9ca3af" : lineColor != null ? lineColor : darkMode ? "#bbb" : "#888",
193
+ x2: x,
194
+ y2: y,
195
+ stroke: isUnanswered ? isDark ? "#6b7280" : "#9ca3af" : isDark ? "#d1d5db" : "black",
178
196
  strokeDasharray: isUnanswered ? "4 3" : void 0,
179
197
  opacity: isUnanswered ? 0.6 : 1
180
198
  }
181
- ), needsExt && /* @__PURE__ */ React.createElement(
182
- "line",
183
- {
184
- x1: endX,
185
- y1: endY,
186
- x2: extX,
187
- y2: extY,
188
- stroke: "#aaa",
189
- strokeDasharray: "4 4",
190
- opacity: 0.4
191
- }
192
- ));
199
+ );
193
200
  }),
194
201
  spokes.map(([shortTitle], i) => {
195
202
  const angle = 2 * Math.PI * i / numSpokes;
203
+ const anchor = angle > Math.PI ? "end" : angle < Math.PI && angle > 0 ? "start" : "middle";
196
204
  const cosA = Math.cos(angle);
197
- const isBottom = cosA < -0.5;
198
205
  const isTop = cosA > 0.5;
206
+ const isBottom = cosA < -0.5;
207
+ const baseFSize = labelFontSize;
199
208
  const lines = wrapLabel(shortTitle, 12);
200
- const longestLine = lines.reduce((m, ln) => ln.length > m ? ln.length : m, 0);
201
- const lineHeight = labelFontSize * 1.2;
202
- const dynamicOffset = lines.length > 1 ? labelOffset + 6 : labelOffset;
203
- const offset2 = radius + dynamicOffset;
209
+ const longestLine = lines.reduce(
210
+ (max, ln) => ln.length > max ? ln.length : max,
211
+ 0
212
+ );
213
+ const fSize = adaptiveFontSize(longestLine, baseFSize);
214
+ const lineHeight = fSize * 1.1;
215
+ const dynamicLabelOffset = lines.length > 1 ? labelOffset + 8 : labelOffset;
216
+ const offset2 = radius + dynamicLabelOffset;
204
217
  const labelX = centerX + offset2 * Math.sin(angle);
205
218
  let labelY = centerY - offset2 * Math.cos(angle);
206
- if (isTop && lines.length > 1) labelY -= (lines.length - 1) * lineHeight;
207
- const isActive = shortTitle === activeSpoke;
219
+ if (isTop && lines.length > 1) {
220
+ labelY -= (lines.length - 1) * lineHeight;
221
+ }
208
222
  const isUnansweredLabel = !!unansweredSpokes[shortTitle];
209
- const isReplaced = !!replacedSpokes[shortTitle];
210
- const shouldBold = boldOriginalSpokes && !isReplaced;
211
- const fSize = labelFontSize;
212
223
  return /* @__PURE__ */ React.createElement(
213
224
  "text",
214
225
  {
215
226
  key: `label-${shortTitle}`,
216
227
  x: labelX,
217
228
  y: labelY,
218
- textAnchor: "middle",
229
+ textAnchor: anchor,
219
230
  dominantBaseline: isBottom ? "hanging" : "auto",
220
231
  onClick: () => onReplaceTopic(shortTitle),
221
- fill: isActive ? "#E85D26" : shortTitle === highlightedSpoke ? darkMode ? "#7DD4E8" : "#59B0C4" : isUnansweredLabel ? "#9ca3af" : darkMode ? "#D3D7DE" : "#555",
222
- fontWeight: isActive || shouldBold ? "bold" : "normal",
223
- style: { cursor: "pointer", userSelect: "none", fontSize: fSize, fontFamily: "sans-serif" }
232
+ className: "text-xl font-medium mb-1 md:text-base md:font-normal",
233
+ fill: isUnansweredLabel ? isDark ? "#6b7280" : "#9ca3af" : isDark ? "#d1d5db" : void 0,
234
+ style: { cursor: "pointer", userSelect: "none", fontSize: fSize }
224
235
  },
225
236
  isUnansweredLabel && /* @__PURE__ */ React.createElement("title", null, "No stance on file for this topic."),
226
237
  lines.map((ln, idx) => /* @__PURE__ */ React.createElement("tspan", { key: idx, x: labelX, dy: idx === 0 ? "0" : `${lineHeight}` }, ln))
@@ -231,14 +242,22 @@ function RadarChartCore({
231
242
  {
232
243
  key: "user-static",
233
244
  points: targetPoints,
234
- style: { fill: "rgba(124, 107, 158, 0.4)", stroke: "#7C6B9E", strokeWidth: 3 }
245
+ style: {
246
+ fill: "rgba(124, 107, 158, 0.4)",
247
+ stroke: "#7C6B9E",
248
+ strokeWidth: 3
249
+ }
235
250
  }
236
251
  ) : /* @__PURE__ */ React.createElement(
237
252
  animated.polygon,
238
253
  {
239
254
  key: "user-animated",
240
255
  points: spring.points,
241
- style: { fill: "rgba(124, 107, 158, 0.4)", stroke: "#7C6B9E", strokeWidth: 3 }
256
+ style: {
257
+ fill: "rgba(124, 107, 158, 0.4)",
258
+ stroke: "#7C6B9E",
259
+ strokeWidth: 3
260
+ }
242
261
  }
243
262
  ),
244
263
  pointsArr.map(([cx, cy], i) => {
@@ -256,8 +275,7 @@ function RadarChartCore({
256
275
  r: matches ? 8 : 7,
257
276
  fill: matches ? "#fed12e" : "#7C6B9E",
258
277
  stroke: "#FFFFFF",
259
- strokeWidth: 3,
260
- style: { pointerEvents: "none" }
278
+ strokeWidth: 3
261
279
  }
262
280
  );
263
281
  }),
@@ -267,10 +285,9 @@ function RadarChartCore({
267
285
  key: "compare-static",
268
286
  points: comparePoints,
269
287
  style: {
270
- fill: darkMode ? "rgba(110, 210, 140, 0.55)" : "rgba(90, 154, 110, 0.45)",
271
- stroke: darkMode ? "#6DD28C" : "#5A9A6E",
272
- strokeWidth: 2,
273
- mixBlendMode: darkMode ? "normal" : "multiply"
288
+ fill: "rgba(90, 154, 110, 0.3)",
289
+ stroke: "#5A9A6E",
290
+ strokeWidth: 2
274
291
  }
275
292
  }
276
293
  ) : /* @__PURE__ */ React.createElement(
@@ -279,10 +296,9 @@ function RadarChartCore({
279
296
  key: "compare-animated",
280
297
  points: compareSpring.points,
281
298
  style: {
282
- fill: darkMode ? "rgba(110, 210, 140, 0.55)" : "rgba(90, 154, 110, 0.45)",
283
- stroke: darkMode ? "#6DD28C" : "#5A9A6E",
284
- strokeWidth: 2,
285
- mixBlendMode: darkMode ? "normal" : "multiply"
299
+ fill: "rgba(90, 154, 110, 0.3)",
300
+ stroke: "#5A9A6E",
301
+ strokeWidth: 2
286
302
  }
287
303
  }
288
304
  ) : null,
@@ -291,7 +307,7 @@ function RadarChartCore({
291
307
  const userVal = spokes[i][1];
292
308
  const compareVal = compareData[shortTitle];
293
309
  if (!compareVal || Number(compareVal) === 0) return null;
294
- const matches = userVal !== 0 && compareVal != null && Number(userVal) === Number(compareVal);
310
+ const matches = userVal !== 0 && compareVal !== 0 && compareVal != null && Number(userVal) === Number(compareVal);
295
311
  return /* @__PURE__ */ React.createElement(
296
312
  "circle",
297
313
  {
@@ -299,17 +315,16 @@ function RadarChartCore({
299
315
  cx,
300
316
  cy,
301
317
  r: matches ? 8 : 7,
302
- fill: matches ? "#fed12e" : darkMode ? "#6DD28C" : "#5A9A6E",
318
+ fill: matches ? "#fed12e" : "#5A9A6E",
303
319
  stroke: "#FFFFFF",
304
- strokeWidth: 3,
305
- style: { pointerEvents: "none" }
320
+ strokeWidth: 3
306
321
  }
307
322
  );
308
323
  }),
309
324
  spokes.map(([shortTitle], i) => {
310
325
  const angle = 2 * Math.PI * i / numSpokes;
311
- const endX = centerX + (spokeExtension[shortTitle] ? extRadius : radius) * Math.sin(angle);
312
- const endY = centerY - (spokeExtension[shortTitle] ? extRadius : radius) * Math.cos(angle);
326
+ const x = centerX + radius * Math.sin(angle);
327
+ const y = centerY - radius * Math.cos(angle);
313
328
  const isUnansweredHitbox = !!unansweredSpokes[shortTitle];
314
329
  return /* @__PURE__ */ React.createElement(
315
330
  "line",
@@ -317,91 +332,20 @@ function RadarChartCore({
317
332
  key: `hitbox-${shortTitle}`,
318
333
  x1: centerX,
319
334
  y1: centerY,
320
- x2: endX,
321
- y2: endY,
335
+ x2: x,
336
+ y2: y,
322
337
  stroke: "transparent",
323
338
  strokeWidth: 14,
324
339
  onClick: () => isUnansweredHitbox ? onReplaceTopic(shortTitle) : onToggleInversion(shortTitle),
325
340
  style: { cursor: "pointer" }
326
341
  }
327
342
  );
328
- }),
329
- pointsArr.map(([cx, cy], i) => {
330
- const shortTitle = spokes[i][0];
331
- const userVal = spokes[i][1];
332
- if (!userVal || Number(userVal) === 0) return null;
333
- return /* @__PURE__ */ React.createElement(
334
- "circle",
335
- {
336
- key: `user-sensor-${i}`,
337
- cx,
338
- cy,
339
- r: 14,
340
- fill: "transparent",
341
- stroke: "none",
342
- style: { cursor: "default" },
343
- onMouseEnter: () => setTooltip({ x: cx, y: cy, text: getStanceText(shortTitle, userVal) }),
344
- onMouseLeave: () => setTooltip(null)
345
- }
346
- );
347
- }),
348
- hasCompareData && compareCoords && compareCoords.map(([cx, cy], i) => {
349
- const shortTitle = spokes[i][0];
350
- const compareVal = compareData[shortTitle];
351
- if (!compareVal || Number(compareVal) === 0) return null;
352
- return /* @__PURE__ */ React.createElement(
353
- "circle",
354
- {
355
- key: `compare-sensor-${i}`,
356
- cx,
357
- cy,
358
- r: 14,
359
- fill: "transparent",
360
- stroke: "none",
361
- style: { cursor: "default" },
362
- onMouseEnter: () => setTooltip({ x: cx, y: cy, text: getStanceText(shortTitle, compareVal) }),
363
- onMouseLeave: () => setTooltip(null)
364
- }
365
- );
366
- }),
367
- tooltip && (() => {
368
- const tipW = 190;
369
- const tipH = 70;
370
- const svgW = size + sidePadding * 2;
371
- const tx = tooltip.x + sidePadding + 12 > svgW - tipW ? tooltip.x - tipW - 12 : tooltip.x + 12;
372
- const ty = tooltip.y - 10;
373
- return /* @__PURE__ */ React.createElement(
374
- "foreignObject",
375
- {
376
- x: tx,
377
- y: ty,
378
- width: tipW,
379
- height: tipH,
380
- style: { overflow: "visible", pointerEvents: "none" }
381
- },
382
- /* @__PURE__ */ React.createElement(
383
- "div",
384
- {
385
- xmlns: "http://www.w3.org/1999/xhtml",
386
- style: {
387
- background: darkMode ? "#2F3237" : "#ffffff",
388
- border: darkMode ? "1px solid #59B0C4" : "1px solid #d1d5db",
389
- borderRadius: 8,
390
- padding: "7px 11px",
391
- fontSize: 12,
392
- lineHeight: 1.4,
393
- color: darkMode ? "#EBEDEF" : "#111",
394
- boxShadow: darkMode ? "0 2px 12px rgba(0,0,0,0.4)" : "0 2px 8px rgba(0,0,0,0.12)",
395
- width: tipW + "px",
396
- wordWrap: "break-word"
397
- }
398
- },
399
- tooltip.text
400
- )
401
- );
402
- })()
343
+ })
403
344
  );
404
345
  }
346
+ function adaptiveFontSize(longestLineLen, baseFSize) {
347
+ return Math.max(baseFSize, 10);
348
+ }
405
349
  function wrapLabel(label, maxChars = 12) {
406
350
  const str = String(label);
407
351
  let rawWords;
@@ -435,7 +379,7 @@ function wrapLabel(label, maxChars = 12) {
435
379
  }
436
380
 
437
381
  // src/Header.jsx
438
- import React2, { useState as useState3, useRef as useRef2, useEffect as useEffect3 } from "react";
382
+ import React2, { useState as useState3, useRef as useRef2, useEffect as useEffect4 } from "react";
439
383
 
440
384
  // src/tokens.js
441
385
  var colorScales = {
@@ -831,13 +775,13 @@ var breakpoints = {
831
775
  };
832
776
 
833
777
  // src/useMediaQuery.js
834
- import { useState as useState2, useEffect as useEffect2 } from "react";
778
+ import { useState as useState2, useEffect as useEffect3 } from "react";
835
779
  function useMediaQuery(query) {
836
780
  const [matches, setMatches] = useState2(() => {
837
781
  if (typeof window === "undefined") return false;
838
782
  return window.matchMedia(query).matches;
839
783
  });
840
- useEffect2(() => {
784
+ useEffect3(() => {
841
785
  if (typeof window === "undefined") return;
842
786
  const mql = window.matchMedia(query);
843
787
  const handler = (e) => setMatches(e.matches);
@@ -849,16 +793,6 @@ function useMediaQuery(query) {
849
793
  }
850
794
 
851
795
  // src/Header.jsx
852
- var dark = {
853
- bg: "#1C1F23",
854
- bgElevated: "#2F3237",
855
- bgHover: "#41454E",
856
- border: "#41454E",
857
- link: "#59B0C4",
858
- text: "#D3D7DE",
859
- textMuted: "#9CA3AF",
860
- shadow: "0 10px 40px rgba(0,0,0,0.4)"
861
- };
862
796
  function Header({
863
797
  logoSrc,
864
798
  logoAlt = "Empowered Vote",
@@ -869,7 +803,6 @@ function Header({
869
803
  currentPath,
870
804
  onNavigate,
871
805
  profileMenu,
872
- darkMode = false,
873
806
  style = {}
874
807
  }) {
875
808
  const [mobileMenuOpen, setMobileMenuOpen] = useState3(false);
@@ -877,7 +810,7 @@ function Header({
877
810
  const [profileOpen, setProfileOpen] = useState3(false);
878
811
  const isMobile = useMediaQuery("(max-width: 768px)");
879
812
  const profileRef = useRef2(null);
880
- useEffect3(() => {
813
+ useEffect4(() => {
881
814
  if (!profileOpen) return;
882
815
  const handleClickOutside = (e) => {
883
816
  if (profileRef.current && !profileRef.current.contains(e.target)) {
@@ -893,16 +826,9 @@ function Header({
893
826
  onNavigate(href);
894
827
  }
895
828
  };
896
- const linkColor = darkMode ? dark.link : colors.evTeal;
897
- const dropdownBg = darkMode ? dark.bgElevated : colors.bgWhite;
898
- const dropdownShadow = darkMode ? dark.shadow : "0 10px 40px rgba(0,0,0,0.1)";
899
- const hoverBg = darkMode ? dark.bgHover : colors.bgLight;
900
- const borderColor = darkMode ? dark.border : colors.borderLight;
901
- const mobileShadow = darkMode ? "0 4px 20px rgba(0,0,0,0.4)" : "0 4px 20px rgba(0,0,0,0.1)";
902
829
  const styles2 = {
903
830
  header: {
904
- backgroundColor: darkMode ? dark.bg : colors.bgWhite,
905
- borderBottom: darkMode ? `1px solid ${dark.border}` : "none",
831
+ backgroundColor: colors.bgWhite,
906
832
  position: "sticky",
907
833
  top: 0,
908
834
  zIndex: 50,
@@ -930,7 +856,7 @@ function Header({
930
856
  fontFamily: fonts.primary,
931
857
  fontWeight: fontWeights.bold,
932
858
  fontSize: fontSizes.xl,
933
- color: linkColor,
859
+ color: colors.evTeal,
934
860
  textDecoration: "none",
935
861
  cursor: "pointer",
936
862
  display: "flex",
@@ -953,12 +879,11 @@ function Header({
953
879
  zIndex: 100
954
880
  },
955
881
  dropdownInner: {
956
- backgroundColor: dropdownBg,
882
+ backgroundColor: colors.bgWhite,
957
883
  borderRadius: borderRadius.lg,
958
- boxShadow: dropdownShadow,
884
+ boxShadow: "0 10px 40px rgba(0,0,0,0.1)",
959
885
  minWidth: "200px",
960
- padding: spacing[2],
961
- border: darkMode ? `1px solid ${dark.border}` : "none"
886
+ padding: spacing[2]
962
887
  },
963
888
  dropdownItem: {
964
889
  display: "block",
@@ -966,7 +891,7 @@ function Header({
966
891
  fontFamily: fonts.primary,
967
892
  fontWeight: fontWeights.medium,
968
893
  fontSize: fontSizes.base,
969
- color: linkColor,
894
+ color: colors.evTeal,
970
895
  textDecoration: "none",
971
896
  borderRadius: borderRadius.md,
972
897
  cursor: "pointer"
@@ -988,13 +913,13 @@ function Header({
988
913
  display: "inline-flex",
989
914
  alignItems: "center",
990
915
  backgroundColor: "transparent",
991
- color: linkColor,
916
+ color: colors.evTeal,
992
917
  fontFamily: fonts.primary,
993
918
  fontWeight: fontWeights.bold,
994
919
  fontSize: fontSizes.base,
995
920
  padding: `${spacing[2]} ${spacing[5]}`,
996
921
  borderRadius: borderRadius.full || "999px",
997
- border: `1.5px solid ${linkColor}`,
922
+ border: `1.5px solid ${colors.evTeal}`,
998
923
  cursor: "pointer",
999
924
  textDecoration: "none",
1000
925
  lineHeight: 1.2,
@@ -1010,7 +935,7 @@ function Header({
1010
935
  hamburger: {
1011
936
  width: "24px",
1012
937
  height: "2px",
1013
- backgroundColor: linkColor,
938
+ backgroundColor: colors.evTeal,
1014
939
  position: "relative"
1015
940
  },
1016
941
  mobileMenu: {
@@ -1020,26 +945,25 @@ function Header({
1020
945
  top: "100%",
1021
946
  left: 0,
1022
947
  right: 0,
1023
- backgroundColor: darkMode ? dark.bg : colors.bgWhite,
948
+ backgroundColor: colors.bgWhite,
1024
949
  padding: spacing[4],
1025
- boxShadow: mobileShadow,
1026
- borderTop: darkMode ? `1px solid ${dark.border}` : "none"
950
+ boxShadow: "0 4px 20px rgba(0,0,0,0.1)"
1027
951
  },
1028
952
  mobileNavItem: {
1029
953
  fontFamily: fonts.primary,
1030
954
  fontWeight: fontWeights.bold,
1031
955
  fontSize: fontSizes.lg,
1032
- color: linkColor,
956
+ color: colors.evTeal,
1033
957
  textDecoration: "none",
1034
958
  padding: `${spacing[3]} 0`,
1035
- borderBottom: `1px solid ${borderColor}`
959
+ borderBottom: `1px solid ${colors.borderLight}`
1036
960
  },
1037
961
  profileButton: {
1038
962
  width: "40px",
1039
963
  height: "40px",
1040
964
  borderRadius: borderRadius.full,
1041
- border: `2px solid ${linkColor}`,
1042
- backgroundColor: darkMode ? dark.bgElevated : colors.bgLight,
965
+ border: `2px solid ${colors.evTeal}`,
966
+ backgroundColor: colors.bgLight,
1043
967
  cursor: "pointer",
1044
968
  display: "flex",
1045
969
  alignItems: "center",
@@ -1054,12 +978,11 @@ function Header({
1054
978
  zIndex: 100
1055
979
  },
1056
980
  profileDropdownInner: {
1057
- backgroundColor: dropdownBg,
981
+ backgroundColor: colors.bgWhite,
1058
982
  borderRadius: borderRadius.lg,
1059
- boxShadow: dropdownShadow,
983
+ boxShadow: "0 10px 40px rgba(0,0,0,0.1)",
1060
984
  minWidth: "160px",
1061
- padding: spacing[2],
1062
- border: darkMode ? `1px solid ${dark.border}` : "none"
985
+ padding: spacing[2]
1063
986
  },
1064
987
  profileDropdownItem: {
1065
988
  display: "block",
@@ -1068,7 +991,7 @@ function Header({
1068
991
  fontFamily: fonts.primary,
1069
992
  fontWeight: fontWeights.medium,
1070
993
  fontSize: fontSizes.base,
1071
- color: linkColor,
994
+ color: colors.evTeal,
1072
995
  textDecoration: "none",
1073
996
  borderRadius: borderRadius.md,
1074
997
  cursor: "pointer",
@@ -1078,7 +1001,7 @@ function Header({
1078
1001
  },
1079
1002
  mobileDivider: {
1080
1003
  height: "1px",
1081
- backgroundColor: borderColor,
1004
+ backgroundColor: colors.borderLight,
1082
1005
  margin: `${spacing[3]} 0`
1083
1006
  },
1084
1007
  profileLabel: {
@@ -1086,8 +1009,8 @@ function Header({
1086
1009
  fontFamily: fonts.primary,
1087
1010
  fontWeight: fontWeights.semibold,
1088
1011
  fontSize: fontSizes.sm,
1089
- color: darkMode ? dark.textMuted : colors.textMuted,
1090
- borderBottom: `1px solid ${borderColor}`,
1012
+ color: colors.textMuted,
1013
+ borderBottom: `1px solid ${colors.borderLight}`,
1091
1014
  marginBottom: spacing[1],
1092
1015
  overflow: "hidden",
1093
1016
  textOverflow: "ellipsis",
@@ -1128,7 +1051,7 @@ function Header({
1128
1051
  "path",
1129
1052
  {
1130
1053
  d: "M1 1L8 8L15 1",
1131
- stroke: linkColor,
1054
+ stroke: colors.evTeal,
1132
1055
  strokeWidth: "2",
1133
1056
  strokeLinecap: "round",
1134
1057
  strokeLinejoin: "round"
@@ -1144,7 +1067,7 @@ function Header({
1144
1067
  onClick: (e) => handleNavClick(e, dropdownItem.href),
1145
1068
  style: styles2.dropdownItem,
1146
1069
  onMouseEnter: (e) => {
1147
- e.target.style.backgroundColor = hoverBg;
1070
+ e.target.style.backgroundColor = colors.bgLight;
1148
1071
  },
1149
1072
  onMouseLeave: (e) => {
1150
1073
  e.target.style.backgroundColor = "transparent";
@@ -1168,7 +1091,7 @@ function Header({
1168
1091
  fontFamily: fonts.primary,
1169
1092
  fontWeight: fontWeights.bold,
1170
1093
  fontSize: fontSizes["2xl"],
1171
- color: linkColor
1094
+ color: colors.evTeal
1172
1095
  }
1173
1096
  },
1174
1097
  "empowered.vote"
@@ -1187,12 +1110,12 @@ function Header({
1187
1110
  style: styles2.secondaryAction,
1188
1111
  className: "ev-header-secondary",
1189
1112
  onMouseEnter: (e) => {
1190
- e.currentTarget.style.backgroundColor = linkColor;
1113
+ e.currentTarget.style.backgroundColor = colors.evTeal;
1191
1114
  e.currentTarget.style.color = colors.textWhite;
1192
1115
  },
1193
1116
  onMouseLeave: (e) => {
1194
1117
  e.currentTarget.style.backgroundColor = "transparent";
1195
- e.currentTarget.style.color = linkColor;
1118
+ e.currentTarget.style.color = colors.evTeal;
1196
1119
  }
1197
1120
  },
1198
1121
  secondaryAction.label
@@ -1226,7 +1149,7 @@ function Header({
1226
1149
  height: "20",
1227
1150
  viewBox: "0 0 24 24",
1228
1151
  fill: "none",
1229
- stroke: linkColor,
1152
+ stroke: colors.evTeal,
1230
1153
  strokeWidth: "2",
1231
1154
  strokeLinecap: "round",
1232
1155
  strokeLinejoin: "round"
@@ -1250,7 +1173,7 @@ function Header({
1250
1173
  },
1251
1174
  style: styles2.profileDropdownItem,
1252
1175
  onMouseEnter: (e) => {
1253
- e.target.style.backgroundColor = hoverBg;
1176
+ e.target.style.backgroundColor = colors.bgLight;
1254
1177
  },
1255
1178
  onMouseLeave: (e) => {
1256
1179
  e.target.style.backgroundColor = "transparent";
@@ -1270,7 +1193,7 @@ function Header({
1270
1193
  },
1271
1194
  style: styles2.profileDropdownItem,
1272
1195
  onMouseEnter: (e) => {
1273
- e.target.style.backgroundColor = hoverBg;
1196
+ e.target.style.backgroundColor = colors.bgLight;
1274
1197
  },
1275
1198
  onMouseLeave: (e) => {
1276
1199
  e.target.style.backgroundColor = "transparent";
@@ -1296,7 +1219,7 @@ function Header({
1296
1219
  left: 0,
1297
1220
  width: "100%",
1298
1221
  height: "2px",
1299
- backgroundColor: linkColor
1222
+ backgroundColor: colors.evTeal
1300
1223
  }
1301
1224
  }
1302
1225
  ), /* @__PURE__ */ React2.createElement(
@@ -1308,7 +1231,7 @@ function Header({
1308
1231
  left: 0,
1309
1232
  width: "100%",
1310
1233
  height: "2px",
1311
- backgroundColor: linkColor
1234
+ backgroundColor: colors.evTeal
1312
1235
  }
1313
1236
  }
1314
1237
  ))
@@ -1561,7 +1484,6 @@ function SiteHeader({
1561
1484
  profileMenu,
1562
1485
  secondaryAction,
1563
1486
  feedbackFeature,
1564
- darkMode = false,
1565
1487
  style = {}
1566
1488
  }) {
1567
1489
  const handleNavigate = (href) => {
@@ -1590,7 +1512,6 @@ function SiteHeader({
1590
1512
  currentPath,
1591
1513
  onNavigate: handleNavigate,
1592
1514
  profileMenu,
1593
- darkMode,
1594
1515
  style
1595
1516
  }
1596
1517
  );
@@ -1800,7 +1721,7 @@ function FilterSidebar({
1800
1721
  }
1801
1722
 
1802
1723
  // src/PoliticianCard.jsx
1803
- import React6, { useState as useState4, useEffect as useEffect4 } from "react";
1724
+ import React6, { useState as useState4, useEffect as useEffect5 } from "react";
1804
1725
  function PoliticianCard({
1805
1726
  id,
1806
1727
  imageSrc,
@@ -1996,7 +1917,7 @@ function PoliticianCard({
1996
1917
  )
1997
1918
  );
1998
1919
  const [imgError, setImgError] = useState4(false);
1999
- useEffect4(() => {
1920
+ useEffect5(() => {
2000
1921
  setImgError(false);
2001
1922
  }, [imageSrc]);
2002
1923
  const getInitials = (n) => {
@@ -2062,7 +1983,7 @@ function PoliticianCard({
2062
1983
  }
2063
1984
 
2064
1985
  // src/CompassCardHorizontal.jsx
2065
- import React11, { useState as useState6, useEffect as useEffect5 } from "react";
1986
+ import React11, { useState as useState6, useEffect as useEffect6 } from "react";
2066
1987
 
2067
1988
  // src/PlaceholderRadar.jsx
2068
1989
  import React7 from "react";
@@ -2428,7 +2349,7 @@ function CompassCardHorizontal({
2428
2349
  const [focused, setFocused] = useState6(false);
2429
2350
  const [imgError, setImgError] = useState6(false);
2430
2351
  const [emptyCtaHovered, setEmptyCtaHovered] = useState6(false);
2431
- useEffect5(() => {
2352
+ useEffect6(() => {
2432
2353
  setImgError(false);
2433
2354
  }, [politician == null ? void 0 : politician.photo_origin_url]);
2434
2355
  const cardStyle = {
@@ -2677,7 +2598,7 @@ function CompassCardHorizontal({
2677
2598
  }
2678
2599
 
2679
2600
  // src/CompassCardVertical.jsx
2680
- import React12, { useState as useState7, useEffect as useEffect6 } from "react";
2601
+ import React12, { useState as useState7, useEffect as useEffect7 } from "react";
2681
2602
  var RADAR_SIZE2 = 400;
2682
2603
  function CompassCardVertical({
2683
2604
  politician,
@@ -2695,7 +2616,7 @@ function CompassCardVertical({
2695
2616
  const [focused, setFocused] = useState7(false);
2696
2617
  const [imgError, setImgError] = useState7(false);
2697
2618
  const [emptyCtaHovered, setEmptyCtaHovered] = useState7(false);
2698
- useEffect6(() => {
2619
+ useEffect7(() => {
2699
2620
  setImgError(false);
2700
2621
  }, [politician == null ? void 0 : politician.photo_origin_url]);
2701
2622
  const cardStyle = {
@@ -3029,7 +2950,7 @@ function CompassCardVertical({
3029
2950
  }
3030
2951
 
3031
2952
  // src/CompassKey.jsx
3032
- import React13, { useEffect as useEffect7, useState as useState8 } from "react";
2953
+ import React13, { useEffect as useEffect8, useState as useState8 } from "react";
3033
2954
 
3034
2955
  // src/evContext.js
3035
2956
  var DEFAULT_BROKER_URL = "https://ev-context.empowered.vote/";
@@ -3276,7 +3197,7 @@ var evContext = {
3276
3197
  function CompassKey({ compact = false } = {}) {
3277
3198
  const [dismissed, setDismissed] = useState8(false);
3278
3199
  const [loaded, setLoaded] = useState8(false);
3279
- useEffect7(() => {
3200
+ useEffect8(() => {
3280
3201
  let cancelled = false;
3281
3202
  evContext.get().then((shared) => {
3282
3203
  if (cancelled) return;
@@ -4043,7 +3964,7 @@ function CommitteeTable({
4043
3964
  }
4044
3965
 
4045
3966
  // src/PoliticianProfile.jsx
4046
- import React22, { useState as useState12, useEffect as useEffect8 } from "react";
3967
+ import React22, { useState as useState12, useEffect as useEffect9 } from "react";
4047
3968
 
4048
3969
  // src/LegislativeInlineSummary.jsx
4049
3970
  import React20 from "react";
@@ -4328,10 +4249,12 @@ function stripRetain(s) {
4328
4249
  return (s || "").replace(/\s*\(Retain\s+.+?\?\)/, "");
4329
4250
  }
4330
4251
  function qualifyLocalTitle(baseTitle, pol) {
4331
- if (!pol.government_name || !baseTitle) return baseTitle;
4252
+ if (!baseTitle) return baseTitle;
4332
4253
  const dt = pol.district_type || "";
4333
4254
  if (!dt.startsWith("LOCAL") && dt !== "COUNTY") return baseTitle;
4334
- const gov = pol.government_name.split(",")[0].trim();
4255
+ const govRaw = pol.government_name || pol.representing_city || "";
4256
+ if (!govRaw) return baseTitle;
4257
+ const gov = govRaw.split(",")[0].trim();
4335
4258
  const govCore = gov.replace(/^City of\s+/i, "").replace(/\s+County$/i, "").trim();
4336
4259
  if (govCore && baseTitle.toLowerCase().includes(govCore.toLowerCase())) return baseTitle;
4337
4260
  let prefix = dt === "COUNTY" ? gov : gov.replace(/^City of\s+/i, "");
@@ -4498,7 +4421,7 @@ function PoliticianProfile({
4498
4421
  })();
4499
4422
  const initials = [pol.first_name, pol.last_name].filter(Boolean).map((n) => n[0]).join("");
4500
4423
  const [imgError, setImgError] = useState12(false);
4501
- useEffect8(() => {
4424
+ useEffect9(() => {
4502
4425
  setImgError(false);
4503
4426
  }, [profileImageUrl]);
4504
4427
  const getSocialHandle = (type) => {
@@ -6018,7 +5941,7 @@ function computeTierCoverage(topics) {
6018
5941
  }
6019
5942
 
6020
5943
  // src/useEvContextPromotion.js
6021
- import { useEffect as useEffect9, useState as useState16, useCallback, useRef as useRef3 } from "react";
5944
+ import { useEffect as useEffect10, useState as useState16, useCallback, useRef as useRef3 } from "react";
6022
5945
  function useEvContextPromotion({
6023
5946
  domain,
6024
5947
  isLoggedIn,
@@ -6082,10 +6005,10 @@ function useEvContextPromotion({
6082
6005
  setPayload(null);
6083
6006
  }
6084
6007
  }, []);
6085
- useEffect9(() => {
6008
+ useEffect10(() => {
6086
6009
  detect();
6087
6010
  }, [detect, isLoggedIn, userId, apiData, enabled, domain]);
6088
- useEffect9(() => {
6011
+ useEffect10(() => {
6089
6012
  const unsub = evContext.subscribe(() => {
6090
6013
  detect();
6091
6014
  });
@@ -6177,7 +6100,7 @@ function isGuestPopulated(domain, fullEvContext) {
6177
6100
  }
6178
6101
 
6179
6102
  // src/StanceAccordion.jsx
6180
- import React30, { useState as useState17, useRef as useRef4, useCallback as useCallback2, useEffect as useEffect10 } from "react";
6103
+ import React30, { useState as useState17, useRef as useRef4, useCallback as useCallback2, useEffect as useEffect11 } from "react";
6181
6104
 
6182
6105
  // src/Favicon.jsx
6183
6106
  import React29 from "react";
@@ -6318,7 +6241,7 @@ function StanceAccordion({
6318
6241
  },
6319
6242
  [expandedTopicId, politicianId, apiUrl]
6320
6243
  );
6321
- useEffect10(() => {
6244
+ useEffect11(() => {
6322
6245
  if (initialExpandedTopicId && topics && topics.length > 0) {
6323
6246
  handleToggle(String(initialExpandedTopicId));
6324
6247
  }