@aparte/plugin-cards 0.0.0-stage → 0.29.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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -2
  3. package/dist/carousel-CPy-0JGN.js +63 -0
  4. package/dist/carousel-CPy-0JGN.js.map +1 -0
  5. package/dist/controls-BVX2u7sK.js +62 -0
  6. package/dist/controls-BVX2u7sK.js.map +1 -0
  7. package/dist/format-CchcJRum.js +720 -0
  8. package/dist/format-CchcJRum.js.map +1 -0
  9. package/dist/index.d.ts +28 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +35 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/items/icons.d.ts +12 -0
  14. package/dist/items/icons.d.ts.map +1 -0
  15. package/dist/items/index.d.ts +100 -0
  16. package/dist/items/index.d.ts.map +1 -0
  17. package/dist/items.js +698 -0
  18. package/dist/items.js.map +1 -0
  19. package/dist/kit/carousel.d.ts +17 -0
  20. package/dist/kit/carousel.d.ts.map +1 -0
  21. package/dist/kit/controls.d.ts +51 -0
  22. package/dist/kit/controls.d.ts.map +1 -0
  23. package/dist/kit/define.d.ts +194 -0
  24. package/dist/kit/define.d.ts.map +1 -0
  25. package/dist/kit/dom.d.ts +40 -0
  26. package/dist/kit/dom.d.ts.map +1 -0
  27. package/dist/kit/format.d.ts +49 -0
  28. package/dist/kit/format.d.ts.map +1 -0
  29. package/dist/kit/rating.d.ts +19 -0
  30. package/dist/kit/rating.d.ts.map +1 -0
  31. package/dist/kit/sparkline.d.ts +19 -0
  32. package/dist/kit/sparkline.d.ts.map +1 -0
  33. package/dist/kit/strings.d.ts +139 -0
  34. package/dist/kit/strings.d.ts.map +1 -0
  35. package/dist/link/icons.d.ts +16 -0
  36. package/dist/link/icons.d.ts.map +1 -0
  37. package/dist/link/index.d.ts +98 -0
  38. package/dist/link/index.d.ts.map +1 -0
  39. package/dist/link.js +387 -0
  40. package/dist/link.js.map +1 -0
  41. package/dist/media/icons.d.ts +28 -0
  42. package/dist/media/icons.d.ts.map +1 -0
  43. package/dist/media/index.d.ts +107 -0
  44. package/dist/media/index.d.ts.map +1 -0
  45. package/dist/media.js +610 -0
  46. package/dist/media.js.map +1 -0
  47. package/dist/order/icons.d.ts +49 -0
  48. package/dist/order/icons.d.ts.map +1 -0
  49. package/dist/order/index.d.ts +204 -0
  50. package/dist/order/index.d.ts.map +1 -0
  51. package/dist/order.js +1843 -0
  52. package/dist/order.js.map +1 -0
  53. package/dist/place/icons.d.ts +18 -0
  54. package/dist/place/icons.d.ts.map +1 -0
  55. package/dist/place/index.d.ts +207 -0
  56. package/dist/place/index.d.ts.map +1 -0
  57. package/dist/place.js +1456 -0
  58. package/dist/place.js.map +1 -0
  59. package/dist/plan/icons.d.ts +25 -0
  60. package/dist/plan/icons.d.ts.map +1 -0
  61. package/dist/plan/index.d.ts +153 -0
  62. package/dist/plan/index.d.ts.map +1 -0
  63. package/dist/plan.js +1191 -0
  64. package/dist/plan.js.map +1 -0
  65. package/dist/product/icons.d.ts +16 -0
  66. package/dist/product/icons.d.ts.map +1 -0
  67. package/dist/product/index.d.ts +133 -0
  68. package/dist/product/index.d.ts.map +1 -0
  69. package/dist/product.js +1245 -0
  70. package/dist/product.js.map +1 -0
  71. package/dist/rating-B8Mdx_b_.js +35 -0
  72. package/dist/rating-B8Mdx_b_.js.map +1 -0
  73. package/dist/score/icons.d.ts +35 -0
  74. package/dist/score/icons.d.ts.map +1 -0
  75. package/dist/score/index.d.ts +185 -0
  76. package/dist/score/index.d.ts.map +1 -0
  77. package/dist/score.js +1843 -0
  78. package/dist/score.js.map +1 -0
  79. package/dist/sparkline-C-cA9obz.js +56 -0
  80. package/dist/sparkline-C-cA9obz.js.map +1 -0
  81. package/dist/stats/icons.d.ts +19 -0
  82. package/dist/stats/icons.d.ts.map +1 -0
  83. package/dist/stats/index.d.ts +137 -0
  84. package/dist/stats/index.d.ts.map +1 -0
  85. package/dist/stats.js +1085 -0
  86. package/dist/stats.js.map +1 -0
  87. package/dist/stock/chart.d.ts +135 -0
  88. package/dist/stock/chart.d.ts.map +1 -0
  89. package/dist/stock/icons.d.ts +15 -0
  90. package/dist/stock/icons.d.ts.map +1 -0
  91. package/dist/stock/index.d.ts +195 -0
  92. package/dist/stock/index.d.ts.map +1 -0
  93. package/dist/stock.js +1013 -0
  94. package/dist/stock.js.map +1 -0
  95. package/dist/weather/conditions.d.ts +25 -0
  96. package/dist/weather/conditions.d.ts.map +1 -0
  97. package/dist/weather/icons.d.ts +12 -0
  98. package/dist/weather/icons.d.ts.map +1 -0
  99. package/dist/weather/index.d.ts +146 -0
  100. package/dist/weather/index.d.ts.map +1 -0
  101. package/dist/weather.js +1162 -0
  102. package/dist/weather.js.map +1 -0
  103. package/package.json +137 -3
package/dist/stock.js ADDED
@@ -0,0 +1,1013 @@
1
+ import { e as el, p as word, d as defineCard, o as str, b as formatNumber, n as num, w as webUrl, c as currencyCode, k as glyph, t as toDate, l as intlTag, a as formatHour, j as formatWhen } from "./format-CchcJRum.js";
2
+ import { s as sparkline } from "./sparkline-C-cA9obz.js";
3
+ import { s as segmented } from "./controls-BVX2u7sK.js";
4
+ const CHART_ROLE = { en: "chart", fr: "graphique", de: "Diagramm", es: "gráfico", it: "grafico", ja: "チャート", ko: "차트", pt: "gráfico", ru: "график", zh: "图表" };
5
+ const SVG = "http://www.w3.org/2000/svg";
6
+ const PAD = 0.2;
7
+ function chartGeometry(values, baseline) {
8
+ if (values.length < 2) return null;
9
+ let hi = 0;
10
+ let lo = 0;
11
+ values.forEach((v, i) => {
12
+ if (v > values[hi]) hi = i;
13
+ if (v < values[lo]) lo = i;
14
+ });
15
+ const top = Math.max(values[hi], baseline);
16
+ const bottom = Math.min(values[lo], baseline);
17
+ const span = top - bottom;
18
+ const y = (v) => span === 0 ? 50 : 100 - (PAD * 100 + (v - bottom) / span * (100 - 2 * PAD * 100));
19
+ const step = 100 / (values.length - 1);
20
+ const points = values.map((v, i) => [i * step, y(v)]);
21
+ const at = (p) => `${p[0].toFixed(2)} ${p[1].toFixed(2)}`;
22
+ const line = `M${points.map(at).join(" L")}`;
23
+ const baseY = y(baseline);
24
+ const area = `${line} L100 ${baseY.toFixed(2)} L0 ${baseY.toFixed(2)} Z`;
25
+ return { line, area, points, baseY, hi, lo };
26
+ }
27
+ function thin(points, max = 240) {
28
+ if (points.length <= max) return points;
29
+ const buckets = Math.max(1, Math.floor(max / 2) - 1);
30
+ const size = points.length / buckets;
31
+ const out = [points[0]];
32
+ for (let b = 0; b < buckets; b++) {
33
+ const slice = points.slice(Math.floor(b * size), Math.floor((b + 1) * size));
34
+ if (!slice.length) continue;
35
+ let low = slice[0];
36
+ let high = slice[0];
37
+ for (const p of slice) {
38
+ if (p.value < low.value) low = p;
39
+ if (p.value > high.value) high = p;
40
+ }
41
+ for (const p of slice.indexOf(low) <= slice.indexOf(high) ? [low, high] : [high, low]) {
42
+ if (out[out.length - 1] !== p) out.push(p);
43
+ }
44
+ }
45
+ const last = points[points.length - 1];
46
+ if (out[out.length - 1] !== last) out.push(last);
47
+ return out;
48
+ }
49
+ function groupCandles(candles, max = 80) {
50
+ if (candles.length <= max) return candles;
51
+ const size = Math.ceil(candles.length / max);
52
+ const out = [];
53
+ for (let i = 0; i < candles.length; i += size) {
54
+ const group = candles.slice(i, i + size);
55
+ const first = group[0];
56
+ const volumes = group.map((c) => c.volume).filter((v) => v !== void 0);
57
+ out.push({
58
+ ...first.time ? { time: first.time } : {},
59
+ open: first.open,
60
+ close: group[group.length - 1].close,
61
+ high: Math.max(...group.map((c) => c.high)),
62
+ low: Math.min(...group.map((c) => c.low)),
63
+ ...volumes.length ? { volume: volumes.reduce((a, b) => a + b, 0) } : {}
64
+ });
65
+ }
66
+ return out;
67
+ }
68
+ const VOLUME_BAND = 16;
69
+ function candleGeometry(candles) {
70
+ if (candles.length === 0) return null;
71
+ let hi = 0;
72
+ let lo = 0;
73
+ candles.forEach((c, i) => {
74
+ if (c.high > candles[hi].high) hi = i;
75
+ if (c.low < candles[lo].low) lo = i;
76
+ });
77
+ const maxVolume = Math.max(0, ...candles.map((c) => c.volume ?? 0));
78
+ const floor = maxVolume > 0 ? 100 - VOLUME_BAND : 100;
79
+ const pad = PAD * 80;
80
+ const top = candles[hi].high;
81
+ const bottom = candles[lo].low;
82
+ const span = top - bottom;
83
+ const y = (v) => span === 0 ? floor / 2 : floor - pad - (v - bottom) / span * (floor - 2 * pad);
84
+ const slot = 100 / candles.length;
85
+ const shapes = candles.map((c, i) => {
86
+ const open = y(c.open);
87
+ const close = y(c.close);
88
+ return {
89
+ x: (i + 0.5) * slot,
90
+ width: slot * 0.62,
91
+ top: Math.min(open, close),
92
+ bottom: Math.max(open, close),
93
+ high: y(c.high),
94
+ low: y(c.low),
95
+ close,
96
+ direction: c.close > c.open ? "up" : c.close < c.open ? "down" : "flat",
97
+ volume: maxVolume > 0 ? (c.volume ?? 0) / maxVolume * (VOLUME_BAND - 2) : 0
98
+ };
99
+ });
100
+ return { shapes, hi, lo, volume: maxVolume > 0 };
101
+ }
102
+ let serial = 0;
103
+ function svgNode(tag, attrs) {
104
+ const node = document.createElementNS(SVG, tag);
105
+ for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, String(value));
106
+ return node;
107
+ }
108
+ function stretchedSvg() {
109
+ const svg = svgNode("svg", { viewBox: "0 0 100 100", preserveAspectRatio: "none", "aria-hidden": "true", focusable: "false" });
110
+ svg.setAttribute("class", "aparte-stock__svg");
111
+ return svg;
112
+ }
113
+ function place(node, x, y) {
114
+ node.style.setProperty("--aparte-stock-x", `${x.toFixed(2)}%`);
115
+ node.style.setProperty("--aparte-stock-y", `${y.toFixed(2)}%`);
116
+ }
117
+ const alignOf = (x) => x < 14 ? "start" : x > 86 ? "end" : "center";
118
+ function extreme(ctx, kind, x, y, text) {
119
+ const label = el("span", { class: `aparte-stock__extreme aparte-stock__extreme--${kind}`, attrs: { "aria-hidden": "true", "data-align": alignOf(x) } });
120
+ place(label, x, y);
121
+ ctx.bind(label, text);
122
+ return label;
123
+ }
124
+ function frame(ctx, options) {
125
+ const cursor = el("span", { class: "aparte-stock__cursor", attrs: { "aria-hidden": "true" } });
126
+ const cursorDot = el("span", { class: "aparte-stock__cursor-dot", attrs: { "aria-hidden": "true" } });
127
+ const plot = el(
128
+ "div",
129
+ { class: "aparte-stock__plot", attrs: { tabindex: 0, role: "application", dir: "ltr", "data-kind": options.kind } },
130
+ options.svg,
131
+ ...options.under,
132
+ cursor,
133
+ cursorDot,
134
+ ...options.over ?? []
135
+ );
136
+ ctx.bind(plot, options.label, "aria-label");
137
+ ctx.bind(plot, () => word(ctx.config, "cardsStockChartRole", CHART_ROLE), "aria-roledescription");
138
+ const shown = el("span", { class: "aparte-stock__readout-shown", attrs: { "aria-hidden": "true" } });
139
+ const spoken = el("span", { class: "aparte-sr-only" });
140
+ const readout = el("span", { class: "aparte-stock__readout", attrs: { "aria-live": "polite", "aria-atomic": "true" } }, shown, spoken);
141
+ const info = el("div", { class: "aparte-stock__chart-info" }, ...options.info ?? []);
142
+ const root = el("div", { class: "aparte-stock__chart" }, plot, el("div", { class: "aparte-stock__chart-foot" }, readout, info));
143
+ const last = options.count - 1;
144
+ let current = -1;
145
+ let byKeyboard = false;
146
+ function show(index) {
147
+ current = Math.max(0, Math.min(last, index));
148
+ const [x, y] = options.at(current);
149
+ place(cursor, x, y);
150
+ place(cursorDot, x, y);
151
+ root.setAttribute("data-scrub", "");
152
+ const text = options.readout(current);
153
+ shown.textContent = text.shown;
154
+ spoken.textContent = text.spoken;
155
+ }
156
+ function hide() {
157
+ current = -1;
158
+ root.removeAttribute("data-scrub");
159
+ shown.textContent = "";
160
+ spoken.textContent = "";
161
+ }
162
+ const fromPointer = (event) => {
163
+ const box = plot.getBoundingClientRect();
164
+ if (!box.width) return;
165
+ show(options.indexAt(Math.max(0, Math.min(1, (event.clientX - box.left) / box.width))));
166
+ };
167
+ plot.addEventListener("pointermove", fromPointer);
168
+ plot.addEventListener("pointerdown", (event) => {
169
+ byKeyboard = false;
170
+ fromPointer(event);
171
+ });
172
+ plot.addEventListener("pointerleave", () => {
173
+ if (!byKeyboard || document.activeElement !== plot) hide();
174
+ });
175
+ plot.addEventListener("pointercancel", hide);
176
+ plot.addEventListener("focus", () => {
177
+ if (current >= 0) return;
178
+ byKeyboard = true;
179
+ show(last);
180
+ });
181
+ plot.addEventListener("blur", hide);
182
+ plot.addEventListener("keydown", (event) => {
183
+ const page = Math.max(1, Math.round(options.count / 10));
184
+ const from = current < 0 ? last : current;
185
+ const moves = {
186
+ ArrowLeft: from - (event.shiftKey ? page : 1),
187
+ ArrowRight: from + (event.shiftKey ? page : 1),
188
+ PageUp: from - page,
189
+ PageDown: from + page,
190
+ Home: 0,
191
+ End: last
192
+ };
193
+ if (event.key === "Escape") {
194
+ hide();
195
+ return;
196
+ }
197
+ const to = moves[event.key];
198
+ if (to === void 0) return;
199
+ event.preventDefault();
200
+ byKeyboard = true;
201
+ show(to);
202
+ });
203
+ return root;
204
+ }
205
+ function priceChart(ctx, options) {
206
+ const shown = thin(options.points);
207
+ const geometry = chartGeometry(shown.map((p) => p.value), options.baseline);
208
+ if (!geometry) return null;
209
+ const { points, baseY } = geometry;
210
+ const id = `aparte-stock-${++serial}`;
211
+ const svg = stretchedSvg();
212
+ const band = (name, y, height) => {
213
+ const clip = svgNode("clipPath", { id: `${id}-${name}`, clipPathUnits: "userSpaceOnUse" });
214
+ clip.append(svgNode("rect", { x: -5, y: y.toFixed(2), width: 110, height: Math.max(0, height).toFixed(2) }));
215
+ return clip;
216
+ };
217
+ const gradient = (name, from, to) => {
218
+ const g = svgNode("linearGradient", { id: `${id}-fill-${name}`, gradientUnits: "userSpaceOnUse", x1: 0, x2: 0, y1: from.toFixed(2), y2: to.toFixed(2) });
219
+ const stop = (offset, opacity) => {
220
+ const s = svgNode("stop", { offset, "stop-opacity": opacity });
221
+ s.setAttribute("class", name === "up" ? "aparte-stock__stop aparte-stock__stop--up" : "aparte-stock__stop aparte-stock__stop--down");
222
+ return s;
223
+ };
224
+ g.append(stop(0, name === "up" ? 0.3 : 0.02), stop(1, name === "up" ? 0.02 : 0.3));
225
+ return g;
226
+ };
227
+ const defs = svgNode("defs", {});
228
+ defs.append(
229
+ band("up", -10, baseY + 10),
230
+ band("down", baseY, 110 - baseY),
231
+ gradient("up", Math.min(...points.map((p) => p[1])), baseY),
232
+ gradient("down", baseY, Math.max(...points.map((p) => p[1])))
233
+ );
234
+ const area = (name) => {
235
+ const path = svgNode("path", { d: geometry.area, fill: `url(#${id}-fill-${name})`, "clip-path": `url(#${id}-${name})` });
236
+ path.setAttribute("class", name === "up" ? "aparte-stock__area aparte-stock__area--up" : "aparte-stock__area aparte-stock__area--down");
237
+ return path;
238
+ };
239
+ const stroke = (name) => {
240
+ const path = svgNode("path", { d: geometry.line, "clip-path": `url(#${id}-${name})`, "vector-effect": "non-scaling-stroke" });
241
+ path.setAttribute("class", name === "up" ? "aparte-stock__line aparte-stock__line--up" : "aparte-stock__line aparte-stock__line--down");
242
+ return path;
243
+ };
244
+ svg.append(defs, area("up"), area("down"), stroke("up"), stroke("down"));
245
+ const baseline = el("span", { class: "aparte-stock__baseline", attrs: { "aria-hidden": "true" } });
246
+ place(baseline, 0, baseY);
247
+ const end = shown[shown.length - 1].value;
248
+ const side = end > options.baseline ? "up" : end < options.baseline ? "down" : "flat";
249
+ const dot = el("span", { class: "aparte-stock__dot", attrs: { "aria-hidden": "true", "data-live": options.live === true, "data-direction": side } });
250
+ const lastPoint = points[points.length - 1];
251
+ place(dot, lastPoint[0], lastPoint[1]);
252
+ const flat = shown[geometry.hi].value === shown[geometry.lo].value;
253
+ const mark = (kind, i) => extreme(ctx, kind, points[i][0], points[i][1], () => options.value(shown[i].value));
254
+ return frame(ctx, {
255
+ kind: "line",
256
+ svg,
257
+ under: [baseline, flat ? null : mark("hi", geometry.hi), flat ? null : mark("lo", geometry.lo)],
258
+ over: [dot],
259
+ count: points.length,
260
+ indexAt: (ratio) => Math.round(ratio * (points.length - 1)),
261
+ at: (i) => points[i],
262
+ readout: (i) => {
263
+ const p = shown[i];
264
+ const text = p.time ? `${options.value(p.value)} · ${options.time(p.time)}` : options.value(p.value);
265
+ return { shown: text, spoken: text };
266
+ },
267
+ label: options.label,
268
+ info: options.info ?? []
269
+ });
270
+ }
271
+ function candleChart(ctx, options) {
272
+ const candles = groupCandles(options.candles);
273
+ const geometry = candleGeometry(candles);
274
+ if (!geometry) return null;
275
+ const { shapes } = geometry;
276
+ const svg = stretchedSvg();
277
+ const paths = {};
278
+ const add = (key, d) => {
279
+ paths[key] = (paths[key] ?? "") + d;
280
+ };
281
+ const n = (v) => v.toFixed(2);
282
+ for (const s of shapes) {
283
+ const x0 = s.x - s.width / 2;
284
+ const x1 = s.x + s.width / 2;
285
+ const bottom = Math.max(s.bottom, s.top + 0.8);
286
+ add(`body-${s.direction}`, `M${n(x0)} ${n(s.top)}H${n(x1)}V${n(bottom)}H${n(x0)}Z`);
287
+ add(`wick-${s.direction}`, `M${n(s.x)} ${n(s.high)}V${n(s.top)}M${n(s.x)} ${n(bottom)}V${n(s.low)}`);
288
+ if (s.volume > 0) add(`volume-${s.direction}`, `M${n(x0)} 100H${n(x1)}V${n(100 - s.volume)}H${n(x0)}Z`);
289
+ }
290
+ for (const kind of ["volume", "wick", "body"]) {
291
+ for (const direction of ["up", "down", "flat"]) {
292
+ const d = paths[`${kind}-${direction}`];
293
+ if (!d) continue;
294
+ const path = svgNode("path", { d, "vector-effect": "non-scaling-stroke" });
295
+ const name = `aparte-stock__${kind}`;
296
+ path.setAttribute("class", [name, `${name}--${direction}`].join(" "));
297
+ svg.append(path);
298
+ }
299
+ }
300
+ const flat = candles[geometry.hi].high === candles[geometry.lo].low;
301
+ const hi = shapes[geometry.hi];
302
+ const lo = shapes[geometry.lo];
303
+ return frame(ctx, {
304
+ kind: "candles",
305
+ svg,
306
+ under: [
307
+ flat ? null : extreme(ctx, "hi", hi.x, hi.high, () => options.value(candles[geometry.hi].high)),
308
+ flat ? null : extreme(ctx, "lo", lo.x, lo.low, () => options.value(candles[geometry.lo].low))
309
+ ],
310
+ count: shapes.length,
311
+ indexAt: (ratio) => Math.min(shapes.length - 1, Math.floor(ratio * shapes.length)),
312
+ at: (i) => [shapes[i].x, shapes[i].close],
313
+ readout: (i) => {
314
+ const c = candles[i];
315
+ const text = options.ohlc(c);
316
+ const when = c.time ? ` · ${options.time(c.time)}` : "";
317
+ return { shown: `${text.shown}${when}`, spoken: `${text.spoken}${when}` };
318
+ },
319
+ label: options.label,
320
+ info: options.info ?? []
321
+ });
322
+ }
323
+ const UP = '<svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M6 2.2l4.3 6.6H1.7z"/></svg>';
324
+ const DOWN = '<svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M6 9.8L1.7 3.2h8.6z"/></svg>';
325
+ const CHART = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M3.5 4v15.5h17"/><path d="M6.5 15l4-4.5 3 2.5 5.5-6.5"/><path d="M15.8 6.5h3.2v3.2"/></svg>';
326
+ const stockStyles = '/* =========================================\n The stock card — @aparte/plugin-cards/stock\n =========================================\n Four variants of one card, chosen by `data-variant` on its root:\n chart one quote at length: the symbol, the price and its change, the chart\n against the previous close, the day\'s figures and the 52-week range.\n Several quotes in `chart` are rows, as in `compact`.\n compact a row per quote — symbol and name, sparkline, price, change.\n ticker a strip of quotes that scrolls sideways, by hand: nothing moves on its own.\n candles the chart card with candlesticks in place of the line (the line when the\n quote has no candles).\n Up and down are `--aparte-stock-up` / `--aparte-stock-down` (the theme\'s success and\n error by default): a market that writes a rise in red swaps the two. A change always\n carries its sign and an arrow too, so the colour is never the only telling.\n */\n\n.aparte-stock-card {\n --aparte-stock-up: var(--aparte-success);\n --aparte-stock-down: var(--aparte-error);\n --aparte-stock-flat: var(--aparte-text-muted);\n /* The colours as text on the card: mixed toward the ink, like the theme\'s own `-ink`\n tokens, and derived from the two above so a swap carries over to the text. */\n --aparte-stock-up-ink: color-mix(in oklab, var(--aparte-stock-up) 40%, var(--aparte-text));\n --aparte-stock-down-ink: color-mix(in oklab, var(--aparte-stock-down) 40%, var(--aparte-text));\n --aparte-stock-chart-height: 8.5rem;\n --aparte-stock-price-size: calc(var(--aparte-font-size-2xl) * 1.3);\n --aparte-stock-tone: var(--aparte-stock-flat);\n --aparte-stock-tone-ink: var(--aparte-text-muted);\n}\n\n.aparte-stock-card [data-direction="up"] {\n --aparte-stock-tone: var(--aparte-stock-up);\n --aparte-stock-tone-ink: var(--aparte-stock-up-ink);\n}\n\n.aparte-stock-card [data-direction="down"] {\n --aparte-stock-tone: var(--aparte-stock-down);\n --aparte-stock-tone-ink: var(--aparte-stock-down-ink);\n}\n\n.aparte-stock-card [data-direction="flat"] {\n --aparte-stock-tone: var(--aparte-stock-flat);\n --aparte-stock-tone-ink: var(--aparte-text-muted);\n}\n\n/* ── Widths: each variant asks for its own; the bubble\'s limit caps them all ── */\n\n.aparte-stock-card[data-variant="chart"],\n.aparte-stock-card[data-variant="candles"] { --aparte-rich-card-max-inline-size: 27rem; }\n.aparte-stock-card[data-variant="compact"],\n.aparte-stock-card:has(> .aparte-stock__list) { --aparte-rich-card-max-inline-size: 26rem; }\n.aparte-stock-card[data-variant="ticker"] { --aparte-rich-card-max-inline-size: 36rem; }\n\n/* ── A quote, whatever its shape ─────────────────────────────────────── */\n\n.aparte-stock {\n position: relative;\n min-inline-size: 0;\n color: var(--aparte-text);\n}\n\n.aparte-stock__heading {\n padding: var(--aparte-space-5) var(--aparte-rich-card-padding) var(--aparte-space-2);\n font-size: var(--aparte-font-size-md);\n font-weight: var(--aparte-font-weight-semibold);\n overflow-wrap: anywhere;\n}\n\n.aparte-stock__id {\n display: flex;\n align-items: baseline;\n gap: var(--aparte-space-3);\n min-inline-size: 0;\n}\n\n.aparte-stock__symbol {\n flex: none;\n font-weight: var(--aparte-font-weight-bold);\n letter-spacing: 0.01em;\n color: inherit;\n text-decoration: none;\n}\n\na.aparte-stock__symbol:hover {\n text-decoration: underline;\n text-decoration-thickness: 1px;\n text-underline-offset: 0.18em;\n}\n\na.aparte-stock__symbol:focus-visible {\n outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);\n outline-offset: var(--aparte-focus-outline-offset);\n border-radius: var(--aparte-radius-xs);\n}\n\n.aparte-stock__name {\n min-inline-size: 0;\n overflow: hidden;\n color: var(--aparte-text-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* The change: arrow and signed figures on a wash of its own colour. */\n.aparte-stock__change {\n display: inline-flex;\n align-items: center;\n gap: var(--aparte-space-1);\n padding: var(--aparte-space-1) var(--aparte-space-3);\n border-radius: var(--aparte-radius-full);\n background: color-mix(in oklab, var(--aparte-stock-tone) 14%, transparent);\n color: var(--aparte-stock-tone-ink);\n font-size: var(--aparte-font-size-sm);\n font-weight: var(--aparte-font-weight-semibold);\n line-height: 1.2;\n white-space: nowrap;\n --aparte-rich-card-glyph-size: 0.75em;\n}\n\n.aparte-stock__arrow { color: var(--aparte-stock-tone); }\n\n/* ── chart: the quote at length ──────────────────────────────────────── */\n\n.aparte-stock--chart {\n display: flex;\n flex-direction: column;\n gap: var(--aparte-space-5);\n padding: var(--aparte-space-7) var(--aparte-rich-card-padding) var(--aparte-space-6);\n /* A wash of the day\'s colour at the top, gone by the chart: the card says which way\n before a figure is read, and stays the card\'s surface where the figures are. */\n background: linear-gradient(\n to bottom,\n color-mix(in oklab, var(--aparte-stock-tone) 8%, var(--aparte-surface-1)),\n var(--aparte-surface-1) 55%\n );\n}\n\n.aparte-stock--chart .aparte-stock__head {\n display: flex;\n flex-direction: column;\n gap: var(--aparte-space-1);\n}\n\n.aparte-stock--chart .aparte-stock__id { font-size: var(--aparte-font-size-base); }\n\n.aparte-stock__meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n column-gap: var(--aparte-space-3);\n row-gap: var(--aparte-space-1);\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-sm);\n}\n\n.aparte-stock__meta > * + *::before {\n content: "·";\n margin-inline-end: var(--aparte-space-3);\n}\n\n.aparte-stock__market {\n display: inline-flex;\n align-items: center;\n gap: var(--aparte-space-2);\n}\n\n/* The dot is a colour AND a word: open, closed, before or after the session. */\n.aparte-stock__market-dot {\n display: inline-block;\n inline-size: 0.45rem;\n block-size: 0.45rem;\n border-radius: var(--aparte-radius-full);\n background: var(--aparte-stock-flat);\n}\n\n.aparte-stock__market[data-market="open"] .aparte-stock__market-dot {\n background: var(--aparte-success);\n box-shadow: 0 0 0 3px color-mix(in oklab, var(--aparte-success) 22%, transparent);\n}\n\n.aparte-stock__market[data-market="pre"] .aparte-stock__market-dot,\n.aparte-stock__market[data-market="post"] .aparte-stock__market-dot {\n background: var(--aparte-warning);\n}\n\n.aparte-stock__quote {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n column-gap: var(--aparte-space-5);\n row-gap: var(--aparte-space-2);\n}\n\n.aparte-stock__amount {\n display: inline-flex;\n align-items: baseline;\n gap: var(--aparte-space-2);\n min-inline-size: 0;\n}\n\n.aparte-stock__price {\n font-size: var(--aparte-stock-price-size);\n font-weight: var(--aparte-font-weight-semibold);\n letter-spacing: -0.02em;\n line-height: 1;\n overflow-wrap: anywhere;\n}\n\n.aparte-stock__currency {\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-sm);\n font-weight: var(--aparte-font-weight-medium);\n}\n\n.aparte-stock__quote .aparte-stock__change {\n align-self: center;\n font-size: var(--aparte-font-size-md);\n}\n\n/* ── The chart ───────────────────────────────────────────────────────── */\n\n.aparte-stock__chart {\n display: flex;\n flex-direction: column;\n gap: var(--aparte-space-3);\n}\n\n/* Time runs left to right (the element is `dir="ltr"`), so the spots laid over the\n plot are placed from its left edge, which is its start. */\n.aparte-stock__plot {\n position: relative;\n block-size: var(--aparte-stock-chart-height);\n border-radius: var(--aparte-radius-sm);\n cursor: crosshair;\n /* A finger sliding sideways explores the chart; up and down still scroll the chat. */\n touch-action: pan-y;\n -webkit-tap-highlight-color: transparent;\n}\n\n.aparte-stock__plot:focus-visible {\n outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);\n outline-offset: var(--aparte-focus-outline-offset);\n}\n\n.aparte-stock__svg {\n position: absolute;\n inset: 0;\n display: block;\n inline-size: 100%;\n block-size: 100%;\n overflow: visible;\n}\n\n.aparte-stock__stop--up { stop-color: var(--aparte-stock-up); }\n.aparte-stock__stop--down { stop-color: var(--aparte-stock-down); }\n\n.aparte-stock__line {\n fill: none;\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.aparte-stock__line--up { stroke: var(--aparte-stock-up); }\n.aparte-stock__line--down { stroke: var(--aparte-stock-down); }\n\n/* Everything laid over the plot is placed at `--aparte-stock-x/y`, in percent of it. */\n.aparte-stock__baseline,\n.aparte-stock__extreme,\n.aparte-stock__dot,\n.aparte-stock__cursor,\n.aparte-stock__cursor-dot {\n position: absolute;\n inset-inline-start: var(--aparte-stock-x, 0%);\n inset-block-start: var(--aparte-stock-y, 0%);\n pointer-events: none;\n}\n\n.aparte-stock__baseline {\n inset-inline: 0;\n border-block-start: 1px dashed color-mix(in oklab, var(--aparte-text) 32%, transparent);\n}\n\n.aparte-stock__extreme {\n --aparte-stock-shift: -50%;\n translate: var(--aparte-stock-shift) calc(-100% - var(--aparte-space-2));\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-2xs);\n font-weight: var(--aparte-font-weight-medium);\n line-height: 1;\n white-space: nowrap;\n}\n\n.aparte-stock__extreme--lo { translate: var(--aparte-stock-shift) var(--aparte-space-2); }\n.aparte-stock__extreme[data-align="start"] { --aparte-stock-shift: 0%; }\n.aparte-stock__extreme[data-align="end"] { --aparte-stock-shift: -100%; }\n\n/* The last point: the line\'s colour on a ring of the card\'s surface. */\n.aparte-stock__dot,\n.aparte-stock__cursor-dot {\n inline-size: 0.6rem;\n block-size: 0.6rem;\n translate: -50% -50%;\n border: 2px solid var(--aparte-surface-1);\n border-radius: var(--aparte-radius-full);\n background: var(--aparte-stock-tone);\n box-sizing: border-box;\n}\n\n/* Open market: the last point breathes. Under reduced motion the kit stops it. */\n.aparte-stock__dot[data-live]::after {\n content: "";\n position: absolute;\n inset: -2px;\n border-radius: inherit;\n background: var(--aparte-stock-tone);\n opacity: 0.45;\n animation: aparte-stock-pulse 1.8s ease-out infinite;\n}\n\n@keyframes aparte-stock-pulse {\n from { scale: 1; opacity: 0.45; }\n to { scale: 3; opacity: 0; }\n}\n\n.aparte-stock__cursor {\n inset-block: 0;\n inline-size: 0;\n translate: -50% 0;\n border-inline-start: 1px solid color-mix(in oklab, var(--aparte-text) 45%, transparent);\n visibility: hidden;\n}\n\n.aparte-stock__cursor-dot {\n visibility: hidden;\n background: var(--aparte-text);\n}\n\n.aparte-stock__chart[data-scrub] .aparte-stock__cursor,\n.aparte-stock__chart[data-scrub] .aparte-stock__cursor-dot { visibility: visible; }\n\n.aparte-stock__chart[data-scrub] .aparte-stock__dot { opacity: 0.35; }\n\n/* Under the plot: the range and the legend, or — while the chart is explored — the\n value and time under the cursor, in the same place. */\n.aparte-stock__chart-foot {\n display: grid;\n min-block-size: 1.5rem;\n font-size: var(--aparte-font-size-sm);\n}\n\n.aparte-stock__chart-foot > * { grid-area: 1 / 1; }\n\n.aparte-stock__chart-info {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--aparte-space-3);\n color: var(--aparte-text-muted);\n}\n\n.aparte-stock__readout {\n align-self: center;\n font-weight: var(--aparte-font-weight-semibold);\n white-space: nowrap;\n}\n\n.aparte-stock__chart[data-scrub] .aparte-stock__chart-info { visibility: hidden; }\n\n.aparte-stock__range {\n padding: var(--aparte-space-1) var(--aparte-space-3);\n border: var(--aparte-border-width) solid var(--aparte-border);\n border-radius: var(--aparte-radius-full);\n color: var(--aparte-text);\n font-size: var(--aparte-font-size-xs);\n font-weight: var(--aparte-font-weight-medium);\n}\n\n.aparte-stock__legend {\n display: inline-flex;\n align-items: center;\n gap: var(--aparte-space-2);\n margin-inline-start: auto;\n}\n\n.aparte-stock__legend-line {\n inline-size: 1rem;\n border-block-start: 1px dashed color-mix(in oklab, var(--aparte-text) 45%, transparent);\n}\n\n.aparte-stock__change--range {\n margin-inline-start: auto;\n font-size: var(--aparte-font-size-xs);\n}\n\n/* ── The range buttons, across the top of the chart ──────────────────── */\n\n.aparte-stock__ranges .aparte-rich-card__segmented {\n display: flex;\n inline-size: 100%;\n box-sizing: border-box;\n}\n\n.aparte-stock__ranges .aparte-rich-card__segment {\n flex: 1 1 0;\n min-inline-size: 0;\n padding-inline: var(--aparte-space-1);\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n}\n\n/* While the app answers a range, the chart it will replace steps back; the buttons stay\n as they are, so another range can be chosen without waiting. */\n.aparte-stock__chart {\n transition: opacity var(--aparte-duration-base) ease;\n}\n\n.aparte-stock-card[data-busy] .aparte-stock__chart { opacity: 0.45; }\n\n/* ── Candles: hollow when they rose, filled when they fell ───────────── */\n\n.aparte-stock__body,\n.aparte-stock__wick {\n stroke-width: 1;\n stroke-linejoin: miter;\n}\n\n.aparte-stock__body--up {\n fill: none;\n stroke: var(--aparte-stock-up);\n}\n\n.aparte-stock__body--down {\n fill: var(--aparte-stock-down);\n stroke: var(--aparte-stock-down);\n}\n\n.aparte-stock__body--flat {\n fill: var(--aparte-stock-flat);\n stroke: var(--aparte-stock-flat);\n}\n\n.aparte-stock__wick { fill: none; }\n.aparte-stock__wick--up { stroke: var(--aparte-stock-up); }\n.aparte-stock__wick--down { stroke: var(--aparte-stock-down); }\n.aparte-stock__wick--flat { stroke: var(--aparte-stock-flat); }\n\n/* The volume: faint, so it is read after the prices and never instead of them. */\n.aparte-stock__volume {\n stroke: none;\n opacity: 0.22;\n}\n\n.aparte-stock__volume--up { fill: var(--aparte-stock-up); }\n.aparte-stock__volume--down { fill: var(--aparte-stock-down); }\n.aparte-stock__volume--flat { fill: var(--aparte-stock-flat); }\n\n.aparte-stock__plot[data-kind="candles"] .aparte-stock__cursor-dot {\n inline-size: 0.45rem;\n block-size: 0.45rem;\n}\n\n/* ── The figures ─────────────────────────────────────────────────────── */\n\n.aparte-stock__stats {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(6.75rem, 1fr));\n gap: var(--aparte-space-4) var(--aparte-space-5);\n margin: 0;\n padding-block-start: var(--aparte-space-5);\n border-block-start: var(--aparte-border-width) solid var(--aparte-border);\n}\n\n.aparte-stock__stat {\n display: flex;\n flex-direction: column;\n gap: var(--aparte-space-1);\n min-inline-size: 0;\n}\n\n.aparte-stock__stat--wide { grid-column: 1 / -1; }\n\n.aparte-stock__stat-term {\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-xs);\n}\n\n.aparte-stock__stat-value {\n margin: 0;\n font-size: var(--aparte-font-size-md);\n font-weight: var(--aparte-font-weight-medium);\n overflow-wrap: anywhere;\n}\n\n/* The 52-week range: its two ends, the bar between, the price as a mark on it. */\n.aparte-stock__year {\n display: flex;\n align-items: center;\n gap: var(--aparte-space-4);\n}\n\n.aparte-stock__year-bar {\n position: relative;\n flex: 1;\n block-size: 0.3rem;\n border-radius: var(--aparte-radius-full);\n background: linear-gradient(\n to var(--aparte-stock-year-toward, right),\n color-mix(in oklab, var(--aparte-stock-down) 40%, var(--aparte-surface-2)),\n var(--aparte-surface-2) 50%,\n color-mix(in oklab, var(--aparte-stock-up) 40%, var(--aparte-surface-2))\n );\n}\n\n.aparte-stock__year-bar:dir(rtl) { --aparte-stock-year-toward: left; }\n\n.aparte-stock__year-mark {\n position: absolute;\n inset-block-start: 50%;\n inset-inline-start: var(--aparte-stock-position, 50%);\n inline-size: 0.2rem;\n block-size: 0.85rem;\n translate: -50% -50%;\n border-radius: var(--aparte-radius-full);\n background: var(--aparte-text);\n box-shadow: 0 0 0 2px var(--aparte-surface-1);\n}\n\n.aparte-stock__year-mark:dir(rtl) { translate: 50% -50%; }\n\n/* ── compact: a row per quote ────────────────────────────────────────── */\n\n.aparte-stock__list {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.aparte-stock__item + .aparte-stock__item {\n border-block-start: var(--aparte-border-width) solid var(--aparte-border);\n}\n\n.aparte-stock--row {\n display: grid;\n grid-template-columns: minmax(0, 1fr) minmax(3rem, 4.5rem) auto;\n align-items: center;\n gap: var(--aparte-space-5);\n padding: var(--aparte-space-5) var(--aparte-rich-card-padding);\n transition: background-color var(--aparte-duration-fast) ease;\n}\n\n.aparte-stock--row .aparte-stock__id {\n flex-direction: column;\n align-items: flex-start;\n gap: 0;\n}\n\n.aparte-stock--row .aparte-stock__name {\n max-inline-size: 100%;\n font-size: var(--aparte-font-size-sm);\n}\n\n.aparte-stock--row[data-linked]:hover { background: var(--aparte-surface-2); }\n\n/* The symbol\'s link covers the row or the tick: one link per quote, read once. */\n.aparte-stock__link--stretched { outline: none; }\n\n.aparte-stock__link--stretched::after {\n content: "";\n position: absolute;\n inset: 0;\n border-radius: inherit;\n}\n\n.aparte-stock--row:has(.aparte-stock__link--stretched:focus-visible),\n.aparte-stock__tick:has(.aparte-stock__link--stretched:focus-visible) {\n outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);\n outline-offset: calc(var(--aparte-focus-outline-width) * -1);\n}\n\n.aparte-stock__spark {\n block-size: 2rem;\n --aparte-rich-card-spark: var(--aparte-stock-tone);\n}\n\n.aparte-stock__spark-base {\n stroke: color-mix(in oklab, var(--aparte-text) 28%, transparent);\n stroke-width: 1;\n stroke-dasharray: 2 3;\n}\n\n.aparte-stock__sparkline .aparte-rich-card__sparkline-line { stroke-width: 1.6; }\n\n.aparte-stock__figures {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: var(--aparte-space-2);\n}\n\n.aparte-stock__row-price {\n font-weight: var(--aparte-font-weight-semibold);\n white-space: nowrap;\n}\n\n.aparte-stock__figures .aparte-stock__change {\n min-inline-size: 4.75rem;\n justify-content: flex-end;\n}\n\n/* ── ticker: a strip of quotes ───────────────────────────────────────── */\n\n.aparte-stock-card[data-variant="ticker"] > .aparte-stock__heading {\n padding-block-end: 0;\n}\n\n.aparte-stock__tape {\n display: flex;\n gap: var(--aparte-space-2);\n margin: 0;\n padding: var(--aparte-space-3);\n list-style: none;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n scroll-snap-type: x proximity;\n scroll-padding-inline: var(--aparte-space-3);\n scrollbar-width: thin;\n /* Both ends fade, so a strip that runs on says so — the same at the start and the end,\n whichever way the page reads. */\n -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--aparte-space-4), #000 calc(100% - var(--aparte-space-4)), transparent);\n mask-image: linear-gradient(to right, transparent, #000 var(--aparte-space-4), #000 calc(100% - var(--aparte-space-4)), transparent);\n}\n\n.aparte-stock__tape:focus-visible {\n outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);\n outline-offset: calc(var(--aparte-focus-outline-width) * -1);\n border-radius: inherit;\n}\n\n.aparte-stock__tick {\n position: relative;\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: var(--aparte-space-3);\n padding: var(--aparte-space-3) var(--aparte-space-4);\n border-radius: var(--aparte-radius-md);\n font-size: var(--aparte-font-size-md);\n white-space: nowrap;\n scroll-snap-align: start;\n transition: background-color var(--aparte-duration-fast) ease;\n}\n\n.aparte-stock__tick + .aparte-stock__tick::before {\n content: "";\n position: absolute;\n inset-block: 25%;\n inset-inline-start: calc(var(--aparte-space-1) * -1);\n border-inline-start: var(--aparte-border-width) solid var(--aparte-border);\n}\n\n.aparte-stock__tick[data-linked]:hover { background: var(--aparte-surface-2); }\n\n.aparte-stock__tick-price { color: var(--aparte-text); }\n\n.aparte-stock__tick .aparte-stock__change {\n padding: 0;\n background: none;\n}\n\n/* ── Skeletons ───────────────────────────────────────────────────────── */\n\n.aparte-stock--skeleton { pointer-events: none; }\n.aparte-stock-card .aparte-stock__sk-symbol { inline-size: 3.5rem; }\n.aparte-stock-card .aparte-stock__sk-name { inline-size: 7rem; margin: 0; }\n.aparte-stock-card .aparte-stock__sk-price { inline-size: 8rem; block-size: 2rem; }\n.aparte-stock-card .aparte-stock__sk-pill { inline-size: 5rem; block-size: 1.25rem; margin: 0; border-radius: var(--aparte-radius-full); }\n.aparte-stock-card .aparte-stock__sk-chart { block-size: var(--aparte-stock-chart-height); }\n.aparte-stock-card .aparte-stock__sk-term { inline-size: 3.5rem; }\n.aparte-stock-card .aparte-stock__sk-figure { inline-size: 4.5rem; margin: 0; }\n.aparte-stock-card .aparte-stock__sk-spark { block-size: 1.5rem; }\n.aparte-stock--skeleton .aparte-stock__quote { align-items: center; }\n.aparte-stock__tick--skeleton { gap: var(--aparte-space-4); }\n\n/* ── Narrow: a phone\'s width ─────────────────────────────────────────── */\n\n@media (max-width: 380px) {\n .aparte-stock-card {\n --aparte-stock-chart-height: 7rem;\n --aparte-stock-price-size: var(--aparte-font-size-2xl);\n }\n\n .aparte-stock--row {\n grid-template-columns: minmax(0, 1fr) 2.75rem auto;\n gap: var(--aparte-space-4);\n }\n\n .aparte-stock__stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .aparte-stock__dot[data-live]::after { animation: none; opacity: 0; }\n}\n';
327
+ const S = {
328
+ cardsStockTitle: { en: "Market quote", fr: "Cours de bourse", de: "Börsenkurs", es: "Cotización", it: "Quotazione", ja: "株価", ko: "시세", pt: "Cotação", ru: "Котировка", zh: "行情" },
329
+ cardsStockCount: { en: "{n} quotes", fr: "{n} cours", de: "{n} Kurse", es: "{n} cotizaciones", it: "{n} quotazioni", ja: "{n}件の銘柄", ko: "종목 {n}개", pt: "{n} cotações", ru: "Котировок: {n}", zh: "{n} 个行情" },
330
+ cardsStockMarketOpen: { en: "Market open", fr: "Marché ouvert", de: "Markt geöffnet", es: "Mercado abierto", it: "Mercato aperto", ja: "取引時間中", ko: "장 운영 중", pt: "Mercado aberto", ru: "Рынок открыт", zh: "交易中" },
331
+ cardsStockMarketClosed: { en: "Market closed", fr: "Marché fermé", de: "Markt geschlossen", es: "Mercado cerrado", it: "Mercato chiuso", ja: "取引終了", ko: "장 마감", pt: "Mercado fechado", ru: "Рынок закрыт", zh: "已收盘" },
332
+ cardsStockPreMarket: { en: "Pre-market", fr: "Pré-ouverture", de: "Vorbörslich", es: "Preapertura", it: "Pre-mercato", ja: "プレマーケット", ko: "장전 거래", pt: "Pré-mercado", ru: "Премаркет", zh: "盘前" },
333
+ cardsStockAfterHours: { en: "After hours", fr: "Après-clôture", de: "Nachbörslich", es: "Poscierre", it: "After hours", ja: "アフターマーケット", ko: "시간외 거래", pt: "After-market", ru: "Постмаркет", zh: "盘后" },
334
+ cardsStockAsOf: { en: "As of {time}", fr: "À {time}", de: "Stand: {time}", es: "A las {time}", it: "Alle {time}", ja: "{time}時点", ko: "{time} 기준", pt: "Às {time}", ru: "На {time}", zh: "截至 {time}" },
335
+ cardsStockOpen: { en: "Open", fr: "Ouverture", de: "Eröffnung", es: "Apertura", it: "Apertura", ja: "始値", ko: "시가", pt: "Abertura", ru: "Открытие", zh: "开盘" },
336
+ cardsStockHigh: { en: "High", fr: "Plus haut", de: "Hoch", es: "Máximo", it: "Massimo", ja: "高値", ko: "고가", pt: "Máxima", ru: "Максимум", zh: "最高" },
337
+ cardsStockLow: { en: "Low", fr: "Plus bas", de: "Tief", es: "Mínimo", it: "Minimo", ja: "安値", ko: "저가", pt: "Mínima", ru: "Минимум", zh: "最低" },
338
+ cardsStockPrevClose: { en: "Prev. close", fr: "Clôture veille", de: "Vortag", es: "Cierre anterior", it: "Chiusura prec.", ja: "前日終値", ko: "전일 종가", pt: "Fech. anterior", ru: "Пред. закрытие", zh: "昨收" },
339
+ cardsStockVolume: { en: "Volume", fr: "Volume", de: "Volumen", es: "Volumen", it: "Volume", ja: "出来高", ko: "거래량", pt: "Volume", ru: "Объём", zh: "成交量" },
340
+ cardsStockMarketCap: { en: "Market cap", fr: "Capitalisation", de: "Marktkap.", es: "Cap. bursátil", it: "Capitalizzazione", ja: "時価総額", ko: "시가총액", pt: "Valor de mercado", ru: "Капитализация", zh: "市值" },
341
+ cardsStockYearRange: { en: "52-week range", fr: "Sur 52 semaines", de: "52-Wochen-Spanne", es: "Rango de 52 semanas", it: "Intervallo 52 settimane", ja: "52週レンジ", ko: "52주 범위", pt: "Intervalo de 52 semanas", ru: "Диапазон за 52 недели", zh: "52周范围" },
342
+ cardsStockChart: {
343
+ en: "Price chart: low {low}, high {high}, last {last}",
344
+ fr: "Graphique du cours : plus bas {low}, plus haut {high}, dernier {last}",
345
+ de: "Kursverlauf: Tief {low}, Hoch {high}, zuletzt {last}",
346
+ es: "Gráfico de precio: mínimo {low}, máximo {high}, último {last}",
347
+ it: "Grafico del prezzo: minimo {low}, massimo {high}, ultimo {last}",
348
+ ja: "価格チャート:安値 {low}、高値 {high}、現在 {last}",
349
+ ko: "가격 차트: 저가 {low}, 고가 {high}, 현재 {last}",
350
+ pt: "Gráfico de preço: mínima {low}, máxima {high}, último {last}",
351
+ ru: "График цены: минимум {low}, максимум {high}, последняя {last}",
352
+ zh: "价格走势:最低 {low},最高 {high},最新 {last}"
353
+ },
354
+ cardsStockRange: { en: "Range", fr: "Période", de: "Zeitraum", es: "Periodo", it: "Periodo", ja: "期間", ko: "기간", pt: "Período", ru: "Период", zh: "时间范围" },
355
+ cardsStockOhlc: {
356
+ en: "O {open} H {high} L {low} C {close}",
357
+ fr: "O {open} H {high} B {low} C {close}",
358
+ de: "E {open} H {high} T {low} S {close}",
359
+ es: "A {open} Máx {high} Mín {low} C {close}",
360
+ it: "A {open} Max {high} Min {low} C {close}",
361
+ ja: "始 {open} 高 {high} 安 {low} 終 {close}",
362
+ ko: "시 {open} 고 {high} 저 {low} 종 {close}",
363
+ pt: "A {open} Máx {high} Mín {low} F {close}",
364
+ ru: "О {open} Макс {high} Мин {low} З {close}",
365
+ zh: "开 {open} 高 {high} 低 {low} 收 {close}"
366
+ },
367
+ cardsStockOhlcSaid: {
368
+ en: "Open {open}, high {high}, low {low}, close {close}",
369
+ fr: "Ouverture {open}, plus haut {high}, plus bas {low}, clôture {close}",
370
+ de: "Eröffnung {open}, Hoch {high}, Tief {low}, Schluss {close}",
371
+ es: "Apertura {open}, máximo {high}, mínimo {low}, cierre {close}",
372
+ it: "Apertura {open}, massimo {high}, minimo {low}, chiusura {close}",
373
+ ja: "始値 {open}、高値 {high}、安値 {low}、終値 {close}",
374
+ ko: "시가 {open}, 고가 {high}, 저가 {low}, 종가 {close}",
375
+ pt: "Abertura {open}, máxima {high}, mínima {low}, fechamento {close}",
376
+ ru: "Открытие {open}, максимум {high}, минимум {low}, закрытие {close}",
377
+ zh: "开盘 {open},最高 {high},最低 {low},收盘 {close}"
378
+ },
379
+ cardsStockChartRange: {
380
+ en: "Price chart, {range}: low {low}, high {high}, last {last}",
381
+ fr: "Graphique du cours, {range} : plus bas {low}, plus haut {high}, dernier {last}",
382
+ de: "Kursverlauf, {range}: Tief {low}, Hoch {high}, zuletzt {last}",
383
+ es: "Gráfico de precio, {range}: mínimo {low}, máximo {high}, último {last}",
384
+ it: "Grafico del prezzo, {range}: minimo {low}, massimo {high}, ultimo {last}",
385
+ ja: "価格チャート({range}):安値 {low}、高値 {high}、現在 {last}",
386
+ ko: "가격 차트({range}): 저가 {low}, 고가 {high}, 현재 {last}",
387
+ pt: "Gráfico de preço, {range}: mínima {low}, máxima {high}, último {last}",
388
+ ru: "График цены, {range}: минимум {low}, максимум {high}, последняя {last}",
389
+ zh: "价格走势({range}):最低 {low},最高 {high},最新 {last}"
390
+ }
391
+ };
392
+ const MARKET_WORD = {
393
+ open: "cardsStockMarketOpen",
394
+ closed: "cardsStockMarketClosed",
395
+ pre: "cardsStockPreMarket",
396
+ post: "cardsStockAfterHours"
397
+ };
398
+ const QUOTES_SHOWN = 12;
399
+ const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
400
+ function pick(r, keys) {
401
+ if (!r) return void 0;
402
+ for (const key of keys) if (r[key] !== void 0 && r[key] !== null) return r[key];
403
+ return void 0;
404
+ }
405
+ function percentOf(value) {
406
+ return typeof value === "string" ? num(value.replace(/[%\s]/g, "")) : num(value);
407
+ }
408
+ function readTime(value) {
409
+ if (value === 0) return void 0;
410
+ if (typeof value === "string") {
411
+ const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
412
+ if (day) return new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
413
+ }
414
+ return toDate(value) ?? void 0;
415
+ }
416
+ function readTimes(values) {
417
+ const ms = values.some((v) => typeof v === "number" && v >= 1e11);
418
+ return values.map((v) => readTime(ms && typeof v === "number" && v !== 0 ? new Date(v) : v));
419
+ }
420
+ function readMarketState(r) {
421
+ const value = pick(r, ["marketState", "market_state", "marketStatus", "market_status", "session"]);
422
+ if (typeof value === "string") {
423
+ const key = value.toLowerCase().replace(/[^a-z]/g, "");
424
+ if (["open", "regular", "trading", "live"].includes(key)) return "open";
425
+ if (["closed", "close", "prepre", "postpost"].includes(key)) return "closed";
426
+ if (key.startsWith("pre")) return "pre";
427
+ if (key.startsWith("post") || key.startsWith("after") || key === "extended") return "post";
428
+ return void 0;
429
+ }
430
+ const open = pick(r, ["isOpen", "is_open", "marketOpen", "market_open"]);
431
+ return typeof open === "boolean" ? open ? "open" : "closed" : void 0;
432
+ }
433
+ const VALUE_KEYS = ["value", "price", "close", "c", "y", "adjClose", "adj_close"];
434
+ const TIME_KEYS = ["time", "t", "date", "timestamp", "datetime", "x"];
435
+ function readSeries(value) {
436
+ if (!Array.isArray(value)) return [];
437
+ const points = [];
438
+ const times = readTimes(value.map((item) => Array.isArray(item) ? item[0] : isObject(item) ? pick(item, TIME_KEYS) : void 0));
439
+ value.forEach((item, i) => {
440
+ const v = num(Array.isArray(item) ? item[1] : isObject(item) ? pick(item, VALUE_KEYS) : item);
441
+ const time = times[i];
442
+ if (v !== void 0) points.push(time ? { value: v, time } : { value: v });
443
+ });
444
+ if (points.length > 1 && points.every((p) => p.time)) points.sort((a, b) => a.time.getTime() - b.time.getTime());
445
+ return points.slice(-2e3);
446
+ }
447
+ function readCandles(value) {
448
+ let list = value;
449
+ if (isObject(value)) {
450
+ const chart = isObject(value["chart"]) && Array.isArray(value["chart"]["result"]) ? value["chart"]["result"][0] : value;
451
+ const indicators = isObject(chart) && isObject(chart["indicators"]) && Array.isArray(chart["indicators"]["quote"]) ? chart["indicators"]["quote"][0] : void 0;
452
+ const times2 = isObject(chart) ? chart["timestamp"] : void 0;
453
+ if (!Array.isArray(times2) || !isObject(indicators)) return [];
454
+ const at = (key, i) => Array.isArray(indicators[key]) ? indicators[key][i] : void 0;
455
+ list = times2.map((t, i) => ({ t, o: at("open", i), h: at("high", i), l: at("low", i), c: at("close", i), v: at("volume", i) }));
456
+ }
457
+ if (!Array.isArray(list)) return [];
458
+ const rows = list.map((item) => Array.isArray(item) ? item : isObject(item) ? [pick(item, ["time", "t", "date", "timestamp", "openTime"]), pick(item, ["open", "o"]), pick(item, ["high", "h"]), pick(item, ["low", "l"]), pick(item, ["close", "c"]), pick(item, ["volume", "v"])] : []);
459
+ const times = readTimes(rows.map((row2) => row2[0]));
460
+ const candles = [];
461
+ rows.forEach(([, o, h, l, c, v], i) => {
462
+ const open = num(o);
463
+ const close = num(c);
464
+ let high = num(h);
465
+ let low = num(l);
466
+ if (open === void 0 || close === void 0 || high === void 0 || low === void 0) return;
467
+ if (high < low) [high, low] = [low, high];
468
+ const time = times[i];
469
+ const volume = num(v);
470
+ candles.push({
471
+ ...time ? { time } : {},
472
+ open,
473
+ high: Math.max(high, open, close),
474
+ low: Math.min(low, open, close),
475
+ close,
476
+ ...volume !== void 0 && volume >= 0 ? { volume } : {}
477
+ });
478
+ });
479
+ if (candles.length > 1 && candles.every((c) => c.time)) candles.sort((a, b) => a.time.getTime() - b.time.getTime());
480
+ return candles.slice(-2e3);
481
+ }
482
+ const ESTIMATED = Symbol("aparte-stock-estimated");
483
+ const isEstimated = (quote) => quote[ESTIMATED] === true;
484
+ function readQuote(raw) {
485
+ if (!isObject(raw)) return null;
486
+ const r = isObject(raw["meta"]) && isObject(raw["indicators"]) ? { ...raw["meta"], candles: raw } : raw;
487
+ const given = str(pick(r, ["symbol", "ticker", "01. symbol", "code"]));
488
+ if (!given) return null;
489
+ const symbol = /[A-Z]/.test(given) ? given : given.toUpperCase();
490
+ const sparkline7d = isObject(r["sparkline_in_7d"]) ? r["sparkline_in_7d"]["price"] : void 0;
491
+ const candles = readCandles(pick(r, ["candles", "ohlc", "klines", "bars"]) ?? (isObject(r["chart"]) ? r["chart"] : void 0));
492
+ let series = readSeries(pick(r, ["series", "prices", "history", "chart", "points", "sparkline"]) ?? sparkline7d);
493
+ if (!series.length && candles.length) series = candles.map((c) => c.time ? { value: c.close, time: c.time } : { value: c.close });
494
+ const price = num(pick(r, ["price", "regularMarketPrice", "05. price", "current_price", "lastPrice", "last", "c"])) ?? series[series.length - 1]?.value;
495
+ if (price === void 0) return null;
496
+ let change = num(pick(r, ["change", "regularMarketChange", "09. change", "price_change_24h", "d"]));
497
+ let changePercent = percentOf(pick(r, [
498
+ "changePercent",
499
+ "change_percent",
500
+ "percentChange",
501
+ "percent_change",
502
+ "changesPercentage",
503
+ "regularMarketChangePercent",
504
+ "10. change percent",
505
+ "price_change_percentage_24h",
506
+ "dp"
507
+ ]));
508
+ let previousClose = num(pick(r, ["previousClose", "previous_close", "prevClose", "regularMarketPreviousClose", "08. previous close", "pc"]));
509
+ if (previousClose === void 0 && str(r["range"])?.toLowerCase() === "1d") previousClose = num(r["chartPreviousClose"]);
510
+ if (price === 0 && !previousClose) return null;
511
+ if (previousClose === 0) previousClose = void 0;
512
+ let guessed = false;
513
+ if (change === void 0 && previousClose !== void 0) change = price - previousClose;
514
+ if (previousClose === void 0 && change !== void 0 && price - change !== 0) previousClose = price - change;
515
+ if (previousClose === void 0 && changePercent !== void 0 && changePercent !== -100) {
516
+ previousClose = price / (1 + changePercent / 100);
517
+ guessed = true;
518
+ }
519
+ if (changePercent === void 0 && change !== void 0 && previousClose) changePercent = change / previousClose * 100;
520
+ const range = str(pick(r, ["range", "period", "timeframe"])) ?? (sparkline7d && series.length ? "7D" : void 0);
521
+ const positive = (value) => {
522
+ const n = num(value);
523
+ return n !== void 0 && n >= 0 ? n : void 0;
524
+ };
525
+ const fields = {
526
+ name: str(pick(r, ["name", "shortName", "longName", "displayName", "companyName", "company_name"])),
527
+ exchange: str(pick(r, ["exchange", "fullExchangeName", "exchangeName", "exchange_name"])),
528
+ currency: currencyCode(pick(r, ["currency", "currency_code", "currencyCode", "financialCurrency"])),
529
+ change,
530
+ changePercent,
531
+ previousClose,
532
+ open: num(pick(r, ["open", "regularMarketOpen", "02. open", "o"])),
533
+ high: num(pick(r, ["high", "dayHigh", "regularMarketDayHigh", "03. high", "high_24h", "h"])),
534
+ low: num(pick(r, ["low", "dayLow", "regularMarketDayLow", "04. low", "low_24h", "l"])),
535
+ volume: positive(pick(r, ["volume", "regularMarketVolume", "06. volume", "total_volume"])),
536
+ marketCap: positive(pick(r, ["marketCap", "market_cap", "marketCapitalization", "MarketCapitalization"])),
537
+ yearHigh: num(pick(r, ["yearHigh", "fiftyTwoWeekHigh", "52WeekHigh", "week52High", "high52", "year_high"])),
538
+ yearLow: num(pick(r, ["yearLow", "fiftyTwoWeekLow", "52WeekLow", "week52Low", "low52", "year_low"])),
539
+ range,
540
+ ranges: Array.isArray(r["ranges"]) ? [...new Set(r["ranges"].map(str).filter((x) => x !== void 0))].slice(0, 8) : void 0,
541
+ marketState: readMarketState(r),
542
+ time: readTime(pick(r, ["time", "asOf", "as_of", "timestamp", "updatedAt", "updated_at", "lastUpdated", "last_updated", "regularMarketTime", "07. latest trading day", "t"])),
543
+ url: webUrl(pick(r, ["url", "link", "href"])) ?? void 0
544
+ };
545
+ const quote = { symbol, price, series, candles };
546
+ for (const [key, value] of Object.entries(fields)) {
547
+ if (value !== void 0 && !(Array.isArray(value) && value.length === 0)) quote[key] = value;
548
+ }
549
+ if (quote.yearHigh === void 0 || quote.yearLow === void 0 || quote.yearHigh <= quote.yearLow) {
550
+ delete quote.yearHigh;
551
+ delete quote.yearLow;
552
+ }
553
+ if (guessed || r[ESTIMATED] === true) quote[ESTIMATED] = true;
554
+ return quote;
555
+ }
556
+ function readQuotes(raw) {
557
+ if (!raw || typeof raw !== "object") return null;
558
+ let list;
559
+ let title;
560
+ if (Array.isArray(raw)) {
561
+ list = raw;
562
+ } else {
563
+ const r = raw;
564
+ const single = readQuote(r);
565
+ if (single) return { quotes: [single] };
566
+ const yahoo = isObject(r["quoteResponse"]) ? r["quoteResponse"]["result"] : void 0;
567
+ const yahooChart = isObject(r["chart"]) ? r["chart"]["result"] : void 0;
568
+ const many = [r["quotes"], r["results"], r["items"], r["watchlist"], r["data"], yahoo, yahooChart].find(Array.isArray);
569
+ if (many) {
570
+ list = many;
571
+ title = str(pick(r, ["title", "heading", "name"]));
572
+ } else {
573
+ list = [r["Global Quote"]];
574
+ }
575
+ }
576
+ const quotes = list.map(readQuote).filter((q) => q !== null).slice(0, QUOTES_SHOWN);
577
+ return quotes.length ? { ...title ? { title } : {}, quotes } : null;
578
+ }
579
+ function directionOf(quote) {
580
+ const moved = quote.change ?? quote.changePercent;
581
+ if (moved === void 0) return void 0;
582
+ return moved > 0 ? "up" : moved < 0 ? "down" : "flat";
583
+ }
584
+ function baselineOf(quote) {
585
+ const intraday = !quote.range || /^(1\s*d(ay)?|today|intraday)$/i.test(quote.range.trim());
586
+ if (intraday && quote.previousClose !== void 0) return { value: quote.previousClose, kind: "close" };
587
+ const first = quote.series[0];
588
+ return first ? { value: first.value, kind: "start" } : void 0;
589
+ }
590
+ const minorCache = /* @__PURE__ */ new Map();
591
+ function minorDigits(currency) {
592
+ if (!currency) return 2;
593
+ let digits = minorCache.get(currency);
594
+ if (digits === void 0) {
595
+ try {
596
+ digits = new Intl.NumberFormat("en", { style: "currency", currency }).resolvedOptions().maximumFractionDigits ?? 2;
597
+ } catch {
598
+ digits = 2;
599
+ }
600
+ minorCache.set(currency, digits);
601
+ }
602
+ return digits;
603
+ }
604
+ function decimalsOf(n) {
605
+ const [mantissa, exponent] = String(Math.abs(n)).split("e");
606
+ const decimals = (mantissa.split(".")[1] ?? "").length;
607
+ return Math.max(0, decimals - Number(exponent ?? 0));
608
+ }
609
+ function digitsOf(quote) {
610
+ const base = minorDigits(quote.currency);
611
+ const abs = Math.abs(quote.price);
612
+ const given = decimalsOf(quote.price);
613
+ if (abs > 0 && abs < 1) return Math.min(Math.max(base, given), Math.floor(-Math.log10(abs)) + 4, 8);
614
+ if (abs >= 10) return Math.max(base, Math.min(given, 2));
615
+ if (given <= base) return base;
616
+ return Math.min(given, 4);
617
+ }
618
+ function plain(config, quote, value) {
619
+ const d = digitsOf(quote);
620
+ return formatNumber(config, value, { minimumFractionDigits: d, maximumFractionDigits: d });
621
+ }
622
+ function money(config, quote, value) {
623
+ if (!quote.currency) return plain(config, quote, value);
624
+ const d = digitsOf(quote);
625
+ try {
626
+ return formatNumber(config, value, { style: "currency", currency: quote.currency, currencyDisplay: "narrowSymbol", minimumFractionDigits: d, maximumFractionDigits: d });
627
+ } catch {
628
+ return `${plain(config, quote, value)} ${quote.currency}`;
629
+ }
630
+ }
631
+ function percentText(config, percent) {
632
+ return formatNumber(config, percent / 100, { style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 2, signDisplay: "exceptZero" });
633
+ }
634
+ function changeText(config, quote, short) {
635
+ const absolute = quote.change !== void 0 ? formatNumber(config, quote.change, { minimumFractionDigits: digitsOf(quote), maximumFractionDigits: digitsOf(quote), signDisplay: "exceptZero" }) : void 0;
636
+ const percent = quote.changePercent !== void 0 ? percentText(config, quote.changePercent) : void 0;
637
+ if (short) return percent ?? absolute;
638
+ return absolute && percent ? `${absolute} (${percent})` : absolute ?? percent;
639
+ }
640
+ function compact(config, value, currency) {
641
+ const options = { notation: "compact", maximumFractionDigits: 2 };
642
+ if (currency) {
643
+ try {
644
+ return formatNumber(config, value, { ...options, style: "currency", currency, currencyDisplay: "narrowSymbol" });
645
+ } catch {
646
+ }
647
+ }
648
+ return formatNumber(config, value, options);
649
+ }
650
+ const UNITS = { d: "day", w: "week", m: "month", y: "year" };
651
+ function rangeText(config, range) {
652
+ const match = /^(\d+)\s*(d|days?|w|wks?|weeks?|m|mo|mos|mons?|months?|y|yrs?|years?)$/i.exec(range.trim());
653
+ if (!match) return range;
654
+ try {
655
+ return formatNumber(config, Number(match[1]), { style: "unit", unit: UNITS[match[2].charAt(0).toLowerCase()], unitDisplay: "long" });
656
+ } catch {
657
+ return range;
658
+ }
659
+ }
660
+ function rangeShort(config, range) {
661
+ const match = /^(\d+)\s*(d|days?|w|wks?|weeks?|m|mo|mos|mons?|months?|y|yrs?|years?)$/i.exec(range.trim());
662
+ if (!match) return range;
663
+ let text;
664
+ try {
665
+ text = formatNumber(config, Number(match[1]), { style: "unit", unit: UNITS[match[2].charAt(0).toLowerCase()], unitDisplay: "narrow" }).replace(/\.$/, "");
666
+ } catch {
667
+ return range;
668
+ }
669
+ return /^\d+\s?\p{L}{1,2}$/u.test(text) ? text.replace(/\s/g, "").toLocaleUpperCase(intlTag(config)) : text;
670
+ }
671
+ function whenText(config, date, now = /* @__PURE__ */ new Date()) {
672
+ const tag = intlTag(config);
673
+ const dayOnly = !date.getHours() && !date.getMinutes() && !date.getSeconds();
674
+ const sameYear = date.getFullYear() === now.getFullYear();
675
+ if (dayOnly) return new Intl.DateTimeFormat(tag, { month: "short", day: "numeric", year: sameYear ? void 0 : "numeric" }).format(date);
676
+ if (date.toDateString() === now.toDateString()) return formatHour(config, date);
677
+ return new Intl.DateTimeFormat(tag, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }).format(date);
678
+ }
679
+ function pointTime(config, date, span) {
680
+ const tag = intlTag(config);
681
+ if (span <= 36 * 36e5) return formatHour(config, date);
682
+ if (span <= 8 * 864e5) return formatWhen(config, date);
683
+ if (span <= 400 * 864e5) return new Intl.DateTimeFormat(tag, { month: "short", day: "numeric" }).format(date);
684
+ return new Intl.DateTimeFormat(tag, { month: "short", year: "numeric" }).format(date);
685
+ }
686
+ const notNull = (value) => value !== null && value !== void 0 && value !== false;
687
+ function symbolOf(quote, stretched = false) {
688
+ if (!quote.url) return el("span", { class: "aparte-stock__symbol", text: quote.symbol });
689
+ return el("a", {
690
+ class: `aparte-stock__symbol aparte-stock__link${stretched ? " aparte-stock__link--stretched" : ""}`,
691
+ attrs: { href: quote.url, target: "_blank", rel: "noopener noreferrer" },
692
+ text: quote.symbol
693
+ });
694
+ }
695
+ function changePill(ctx, quote, short = false) {
696
+ const direction = directionOf(quote);
697
+ if (!direction) return null;
698
+ const pill = el("span", { class: "aparte-stock__change", attrs: { "data-direction": direction } });
699
+ if (direction !== "flat") pill.append(glyph(direction === "up" ? UP : DOWN, "aparte-rich-card__glyph aparte-stock__arrow"));
700
+ const text = el("span", { class: "aparte-stock__change-text" });
701
+ ctx.bind(text, () => changeText(ctx.config, quote, short));
702
+ pill.append(text);
703
+ return pill;
704
+ }
705
+ function marketOf(ctx, quote) {
706
+ if (!quote.marketState) return null;
707
+ const text = el("span", { class: "aparte-stock__market-text" });
708
+ ctx.word(text, MARKET_WORD[quote.marketState], S[MARKET_WORD[quote.marketState]]);
709
+ return el(
710
+ "span",
711
+ { class: "aparte-stock__market", attrs: { "data-market": quote.marketState } },
712
+ el("span", { class: "aparte-stock__market-dot", attrs: { "aria-hidden": "true" } }),
713
+ text
714
+ );
715
+ }
716
+ function asOf(ctx, quote) {
717
+ if (!quote.time) return null;
718
+ const time = quote.time;
719
+ const node = el("time", { class: "aparte-stock__time", attrs: { datetime: time.toISOString() } });
720
+ ctx.bind(node, () => word(ctx.config, "cardsStockAsOf", S.cardsStockAsOf, { time: whenText(ctx.config, time) }));
721
+ return node;
722
+ }
723
+ function head(ctx, quote) {
724
+ const meta = [
725
+ quote.exchange ? el("span", { class: "aparte-stock__exchange", text: quote.exchange }) : null,
726
+ marketOf(ctx, quote),
727
+ asOf(ctx, quote)
728
+ ].filter(notNull);
729
+ return el(
730
+ "div",
731
+ { class: "aparte-stock__head" },
732
+ el(
733
+ "div",
734
+ { class: "aparte-stock__id" },
735
+ symbolOf(quote),
736
+ quote.name ? el("span", { class: "aparte-stock__name", text: quote.name }) : null
737
+ ),
738
+ meta.length ? el("div", { class: "aparte-stock__meta" }, ...meta) : null
739
+ );
740
+ }
741
+ function chartLabel(ctx, quote, low, high, last) {
742
+ const vars = {
743
+ low: plain(ctx.config, quote, low),
744
+ high: plain(ctx.config, quote, high),
745
+ last: plain(ctx.config, quote, last),
746
+ range: quote.range ? rangeText(ctx.config, quote.range) : ""
747
+ };
748
+ return quote.range ? word(ctx.config, "cardsStockChartRange", S.cardsStockChartRange, vars) : word(ctx.config, "cardsStockChart", S.cardsStockChart, vars);
749
+ }
750
+ function spanOf(times) {
751
+ const known = times.filter(notNull);
752
+ return known.length > 1 ? known[known.length - 1].getTime() - known[0].getTime() : 0;
753
+ }
754
+ function rangeChip(ctx, quote) {
755
+ if (!quote.range || ctx.live && quote.ranges?.length) return null;
756
+ const range = el("span", { class: "aparte-stock__range" });
757
+ const value = quote.range;
758
+ ctx.bind(range, () => rangeText(ctx.config, value));
759
+ return range;
760
+ }
761
+ function candlesOf(ctx, quote) {
762
+ const { candles } = quote;
763
+ const value = (v) => plain(ctx.config, quote, v);
764
+ const span = spanOf(candles.map((c) => c.time));
765
+ const low = Math.min(...candles.map((c) => c.low));
766
+ const high = Math.max(...candles.map((c) => c.high));
767
+ return candleChart(ctx, {
768
+ candles,
769
+ value,
770
+ time: (d) => pointTime(ctx.config, d, span),
771
+ ohlc: (c) => {
772
+ const vars = { open: value(c.open), high: value(c.high), low: value(c.low), close: value(c.close) };
773
+ return { shown: word(ctx.config, "cardsStockOhlc", S.cardsStockOhlc, vars), spoken: word(ctx.config, "cardsStockOhlcSaid", S.cardsStockOhlcSaid, vars) };
774
+ },
775
+ label: () => chartLabel(ctx, quote, low, high, candles[candles.length - 1].close),
776
+ info: [rangeChip(ctx, quote)].filter(notNull)
777
+ });
778
+ }
779
+ function chartOf(ctx, quote) {
780
+ if (ctx.variant === "candles" && quote.candles.length) return candlesOf(ctx, quote);
781
+ const base = baselineOf(quote);
782
+ if (!base || quote.series.length < 2) return null;
783
+ const info = [];
784
+ const chip = rangeChip(ctx, quote);
785
+ if (chip) info.push(chip);
786
+ if (base.kind === "close") {
787
+ const text = el("span", { class: "aparte-stock__legend-text" });
788
+ ctx.bind(text, () => `${word(ctx.config, "cardsStockPrevClose", S.cardsStockPrevClose)} ${plain(ctx.config, quote, base.value)}`);
789
+ info.push(el("span", { class: "aparte-stock__legend" }, el("span", { class: "aparte-stock__legend-line", attrs: { "aria-hidden": "true" } }), text));
790
+ } else if (base.value !== 0) {
791
+ const last = quote.series[quote.series.length - 1].value;
792
+ const moved = (last - base.value) / Math.abs(base.value) * 100;
793
+ const over = changePill(ctx, { ...quote, change: void 0, changePercent: moved }, true);
794
+ if (over) {
795
+ over.classList.add("aparte-stock__change--range");
796
+ info.push(over);
797
+ }
798
+ }
799
+ const span = spanOf(quote.series.map((p) => p.time));
800
+ const values = quote.series.map((p) => p.value);
801
+ return priceChart(ctx, {
802
+ points: quote.series,
803
+ baseline: base.value,
804
+ value: (v) => plain(ctx.config, quote, v),
805
+ time: (d) => pointTime(ctx.config, d, span),
806
+ label: () => chartLabel(ctx, quote, Math.min(...values), Math.max(...values), values[values.length - 1]),
807
+ live: quote.marketState === "open",
808
+ info
809
+ });
810
+ }
811
+ function stats(ctx, quote) {
812
+ const list = el("dl", { class: "aparte-stock__stats" });
813
+ const stat = (key, compute, extra) => {
814
+ const term = el("dt", { class: "aparte-stock__stat-term" });
815
+ ctx.word(term, key, S[key]);
816
+ const value = el("dd", { class: "aparte-stock__stat-value" });
817
+ if (extra) value.append(extra);
818
+ else ctx.bind(value, compute);
819
+ list.append(el("div", { class: `aparte-stock__stat${extra ? " aparte-stock__stat--wide" : ""}` }, term, value));
820
+ };
821
+ const figure = (value) => value === void 0 ? void 0 : () => plain(ctx.config, quote, value);
822
+ const rows = [
823
+ ["cardsStockOpen", figure(quote.open)],
824
+ ["cardsStockHigh", figure(quote.high)],
825
+ ["cardsStockLow", figure(quote.low)],
826
+ ["cardsStockPrevClose", isEstimated(quote) ? void 0 : figure(quote.previousClose)],
827
+ ["cardsStockVolume", quote.volume === void 0 ? void 0 : () => compact(ctx.config, quote.volume)],
828
+ ["cardsStockMarketCap", quote.marketCap === void 0 ? void 0 : () => compact(ctx.config, quote.marketCap, quote.currency)]
829
+ ];
830
+ for (const [key, compute] of rows) if (compute) stat(key, compute);
831
+ if (quote.yearLow !== void 0 && quote.yearHigh !== void 0) {
832
+ const { yearLow: low, yearHigh: high } = quote;
833
+ const position = Math.max(0, Math.min(1, (quote.price - low) / (high - low)));
834
+ const bar = el(
835
+ "span",
836
+ { class: "aparte-stock__year-bar", attrs: { "aria-hidden": "true" } },
837
+ el("span", { class: "aparte-stock__year-mark" })
838
+ );
839
+ bar.style.setProperty("--aparte-stock-position", `${(position * 100).toFixed(1)}%`);
840
+ const lowText = el("span", { class: "aparte-stock__year-low" });
841
+ ctx.bind(lowText, () => plain(ctx.config, quote, low));
842
+ const highText = el("span", { class: "aparte-stock__year-high" });
843
+ ctx.bind(highText, () => plain(ctx.config, quote, high));
844
+ stat("cardsStockYearRange", () => "", el("span", { class: "aparte-stock__year" }, lowText, bar, highText));
845
+ }
846
+ return list.childNodes.length ? list : null;
847
+ }
848
+ function rangesOf(ctx, quote) {
849
+ const ranges = quote.ranges ?? [];
850
+ const pressed = ranges.find((r) => r.toLowerCase() === quote.range?.toLowerCase()) ?? quote.range;
851
+ const group = segmented(ctx, {
852
+ name: "range",
853
+ value: pressed,
854
+ options: ranges.map((r) => ({ value: r, label: () => rangeShort(ctx.config, r) })),
855
+ label: () => word(ctx.config, "cardsStockRange", S.cardsStockRange)
856
+ });
857
+ if (!group) return null;
858
+ group.querySelectorAll("button").forEach((button, i) => ctx.bind(button, () => rangeText(ctx.config, ranges[i]), "aria-label"));
859
+ return el("div", { class: "aparte-stock__ranges" }, group);
860
+ }
861
+ function chartCard(ctx, quote) {
862
+ const price = el("span", { class: "aparte-stock__price" });
863
+ ctx.bind(price, () => plain(ctx.config, quote, quote.price));
864
+ const amount = el(
865
+ "span",
866
+ { class: "aparte-stock__amount" },
867
+ price,
868
+ quote.currency ? el("span", { class: "aparte-stock__currency", text: quote.currency }) : null
869
+ );
870
+ return el(
871
+ "div",
872
+ { class: "aparte-stock aparte-stock--chart", attrs: { "data-direction": directionOf(quote) } },
873
+ head(ctx, quote),
874
+ el("div", { class: "aparte-stock__quote" }, amount, changePill(ctx, quote)),
875
+ rangesOf(ctx, quote),
876
+ chartOf(ctx, quote),
877
+ stats(ctx, quote)
878
+ );
879
+ }
880
+ function sparkOf(quote) {
881
+ const base = baselineOf(quote);
882
+ if (!base || quote.series.length < 2) return null;
883
+ const values = thin(quote.series, 64).map((p) => p.value);
884
+ const lo = Math.min(...values, base.value);
885
+ const hi = Math.max(...values, base.value);
886
+ const pad = 0.12;
887
+ const svg = sparkline(values, { className: "aparte-stock__sparkline", range: [lo, hi], pad });
888
+ const y = hi === lo ? 20 : 40 - (pad * 40 + (base.value - lo) / (hi - lo) * (40 - 2 * pad * 40));
889
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
890
+ for (const [name, value] of Object.entries({ x1: 0, x2: 100, y1: y.toFixed(2), y2: y.toFixed(2), "vector-effect": "non-scaling-stroke" })) line.setAttribute(name, String(value));
891
+ line.setAttribute("class", "aparte-stock__spark-base");
892
+ svg.prepend(line);
893
+ return svg;
894
+ }
895
+ function row(ctx, quote) {
896
+ const price = el("span", { class: "aparte-stock__row-price" });
897
+ ctx.bind(price, () => money(ctx.config, quote, quote.price));
898
+ const spark = sparkOf(quote);
899
+ return el(
900
+ "div",
901
+ { class: "aparte-stock aparte-stock--row", attrs: { "data-direction": directionOf(quote), "data-linked": quote.url !== void 0 } },
902
+ el(
903
+ "div",
904
+ { class: "aparte-stock__id" },
905
+ symbolOf(quote, true),
906
+ quote.name ? el("span", { class: "aparte-stock__name", text: quote.name }) : null
907
+ ),
908
+ el("div", { class: "aparte-stock__spark", attrs: { "data-empty": !spark } }, spark),
909
+ el("div", { class: "aparte-stock__figures" }, price, changePill(ctx, quote, true))
910
+ );
911
+ }
912
+ function ticker(ctx, data) {
913
+ const tape = el("ul", { class: "aparte-stock__tape", attrs: { role: "list", tabindex: 0 } });
914
+ ctx.bind(tape, () => labelOf(data, ctx), "aria-label");
915
+ for (const quote of data.quotes) {
916
+ const price = el("span", { class: "aparte-stock__tick-price" });
917
+ ctx.bind(price, () => money(ctx.config, quote, quote.price));
918
+ tape.append(el(
919
+ "li",
920
+ { class: "aparte-stock__tick", attrs: { "data-direction": directionOf(quote), "data-linked": quote.url !== void 0 } },
921
+ symbolOf(quote, true),
922
+ price,
923
+ changePill(ctx, quote, true)
924
+ ));
925
+ }
926
+ return el("div", { class: "aparte-stock__ticker" }, tape);
927
+ }
928
+ function heading(data) {
929
+ return data.title ? el("div", { class: "aparte-stock__heading", text: data.title }) : null;
930
+ }
931
+ function labelOf(data, ctx) {
932
+ const quotes = data?.quotes ?? [];
933
+ if (quotes.length === 1) return quotes[0].name ? `${quotes[0].symbol} ${quotes[0].name}` : quotes[0].symbol;
934
+ if (data?.title) return data.title;
935
+ if (quotes.length > 1) return word(ctx.config, "cardsStockCount", S.cardsStockCount, { n: formatNumber(ctx.config, quotes.length) });
936
+ return word(ctx.config, "cardsStockTitle", S.cardsStockTitle);
937
+ }
938
+ function render(data, ctx) {
939
+ const { quotes } = data;
940
+ if (ctx.variant === "ticker") return [heading(data), ticker(ctx, data)].filter(notNull);
941
+ if (quotes.length === 1) return [ctx.variant === "compact" ? row(ctx, quotes[0]) : chartCard(ctx, quotes[0])];
942
+ const list = el(
943
+ "ul",
944
+ { class: "aparte-stock__list", attrs: { role: "list" } },
945
+ ...quotes.map((q) => el("li", { class: "aparte-stock__item" }, row(ctx, q)))
946
+ );
947
+ return [heading(data), list].filter(notNull);
948
+ }
949
+ function skeleton(partial, ctx) {
950
+ const block = (cls, kind = "text") => el("span", { class: `aparte-skeleton aparte-skeleton--${kind} ${cls}`, attrs: { "aria-hidden": "true" } });
951
+ const symbols = partial?.title ? partial.title.split(/,\s*/).filter(Boolean) : [];
952
+ const symbol = (text) => text ? el("span", { class: "aparte-stock__symbol", text }) : block("aparte-stock__sk-symbol");
953
+ if (ctx.variant === "ticker") {
954
+ const ticks = Array.from({ length: Math.max(4, symbols.length) }, (_, i) => el("li", { class: "aparte-stock__tick aparte-stock__tick--skeleton" }, symbol(symbols[i]), block("aparte-stock__sk-figure")));
955
+ return [el("div", { class: "aparte-stock__ticker" }, el("ul", { class: "aparte-stock__tape", attrs: { role: "list" } }, ...ticks))];
956
+ }
957
+ if (ctx.variant === "compact" || symbols.length > 1) {
958
+ const rows = Array.from({ length: Math.max(3, Math.min(symbols.length, QUOTES_SHOWN)) }, (_, i) => el(
959
+ "li",
960
+ { class: "aparte-stock__item" },
961
+ el(
962
+ "div",
963
+ { class: "aparte-stock aparte-stock--row aparte-stock--skeleton" },
964
+ el("div", { class: "aparte-stock__id" }, symbol(symbols[i]), block("aparte-stock__sk-name")),
965
+ block("aparte-stock__spark aparte-stock__sk-spark", "rect"),
966
+ el("div", { class: "aparte-stock__figures" }, block("aparte-stock__sk-figure"), block("aparte-stock__sk-pill"))
967
+ )
968
+ ));
969
+ return [el("ul", { class: "aparte-stock__list", attrs: { role: "list" } }, ...rows)];
970
+ }
971
+ return [el(
972
+ "div",
973
+ { class: "aparte-stock aparte-stock--chart aparte-stock--skeleton" },
974
+ el("div", { class: "aparte-stock__head" }, el("div", { class: "aparte-stock__id" }, symbol(symbols[0]), block("aparte-stock__sk-name"))),
975
+ el("div", { class: "aparte-stock__quote" }, block("aparte-stock__sk-price"), block("aparte-stock__sk-pill")),
976
+ block("aparte-stock__sk-chart", "rect"),
977
+ el("div", { class: "aparte-stock__stats" }, ...Array.from({ length: 6 }, () => el("div", { class: "aparte-stock__stat" }, block("aparte-stock__sk-term"), block("aparte-stock__sk-figure"))))
978
+ )];
979
+ }
980
+ const stockCard = defineCard({
981
+ name: "stock",
982
+ variants: ["chart", "compact", "ticker", "candles"],
983
+ styles: stockStyles,
984
+ icon: CHART,
985
+ normalize: readQuotes,
986
+ render,
987
+ skeleton,
988
+ label: labelOf,
989
+ fallback: (data, ctx) => {
990
+ const lines = data.quotes.map((q) => {
991
+ const price = `${plain(ctx.config, q, q.price)}${q.currency ? ` ${q.currency}` : ""}`;
992
+ return directionOf(q) ? `${q.symbol} ${price}, ${changeText(ctx.config, q, false)}` : `${q.symbol} ${price}`;
993
+ });
994
+ return [data.quotes.length > 1 ? data.title : void 0, ...lines].filter(notNull).join("\n");
995
+ },
996
+ partial: (input) => {
997
+ const raw = input["symbols"] ?? input["tickers"] ?? input["symbol"] ?? input["ticker"] ?? input["pair"];
998
+ const list = (Array.isArray(raw) ? raw : typeof raw === "string" ? raw.split(",") : []).map((s) => str(s)).filter((s) => s !== void 0).map((s) => /[A-Z]/.test(s) ? s : s.toUpperCase());
999
+ return list.length ? { title: list.slice(0, QUOTES_SHOWN).join(", ") } : null;
1000
+ }
1001
+ });
1002
+ function setupStockCard(options = {}) {
1003
+ return stockCard.setup(options);
1004
+ }
1005
+ export {
1006
+ baselineOf,
1007
+ digitsOf,
1008
+ directionOf,
1009
+ readQuotes,
1010
+ setupStockCard,
1011
+ stockCard
1012
+ };
1013
+ //# sourceMappingURL=stock.js.map