@kashi-ui/elements 0.0.0-stage → 0.25.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 (61) hide show
  1. package/README.md +45 -2
  2. package/dist/base.d.ts +47 -0
  3. package/dist/base.js +148 -0
  4. package/dist/calendar.d.ts +86 -0
  5. package/dist/calendar.js +439 -0
  6. package/dist/cdn/base-Cmq7zeIG.js +1 -0
  7. package/dist/cdn/calendar-Dm3fq2bs.js +16 -0
  8. package/dist/cdn/charts-DNOWi4sJ.js +1 -0
  9. package/dist/cdn/combobox-ZGrZ5okg.js +1 -0
  10. package/dist/cdn/controls-K-OZcjg4.js +1 -0
  11. package/dist/cdn/digits-DE1YGT94.js +1 -0
  12. package/dist/cdn/display-C975tS7j.js +1 -0
  13. package/dist/cdn/dist-D670Gw0K.js +1 -0
  14. package/dist/cdn/fields-BUTaSN8m.js +1 -0
  15. package/dist/cdn/forms-C-effPKn.js +1 -0
  16. package/dist/cdn/inputs-BjOddAM7.js +1 -0
  17. package/dist/cdn/jalali-BiM5Kfep.js +1 -0
  18. package/dist/cdn/kashi-elements.js +1 -0
  19. package/dist/cdn/map-BHuusa4t.js +12 -0
  20. package/dist/cdn/menu-DhrqyLgy.js +1 -0
  21. package/dist/cdn/nav-D_w5BV_K.js +1 -0
  22. package/dist/cdn/overlay-DX9un2xm.js +1 -0
  23. package/dist/cdn/pickers-BFLpUruK.js +1 -0
  24. package/dist/cdn/table-DPQfDMs-.js +1 -0
  25. package/dist/cdn/text-D1U32D6K.js +1 -0
  26. package/dist/cdn/words-9OdzG7y6.js +1 -0
  27. package/dist/cdn.d.ts +3 -0
  28. package/dist/cdn.js +44 -0
  29. package/dist/charts.d.ts +106 -0
  30. package/dist/charts.js +436 -0
  31. package/dist/combobox.d.ts +106 -0
  32. package/dist/combobox.js +439 -0
  33. package/dist/controls.d.ts +82 -0
  34. package/dist/controls.js +425 -0
  35. package/dist/display.d.ts +41 -0
  36. package/dist/display.js +144 -0
  37. package/dist/fields.d.ts +24 -0
  38. package/dist/fields.js +103 -0
  39. package/dist/forms.d.ts +186 -0
  40. package/dist/forms.js +902 -0
  41. package/dist/index.d.ts +86 -0
  42. package/dist/index.js +32 -0
  43. package/dist/inputs.d.ts +107 -0
  44. package/dist/inputs.js +371 -0
  45. package/dist/iranMapData.d.ts +6 -0
  46. package/dist/iranMapData.js +37 -0
  47. package/dist/map.d.ts +64 -0
  48. package/dist/map.js +416 -0
  49. package/dist/menu.d.ts +45 -0
  50. package/dist/menu.js +281 -0
  51. package/dist/nav.d.ts +163 -0
  52. package/dist/nav.js +918 -0
  53. package/dist/overlay.d.ts +79 -0
  54. package/dist/overlay.js +420 -0
  55. package/dist/pickers.d.ts +108 -0
  56. package/dist/pickers.js +655 -0
  57. package/dist/table.d.ts +53 -0
  58. package/dist/table.js +267 -0
  59. package/dist/toast.d.ts +45 -0
  60. package/dist/toast.js +141 -0
  61. package/package.json +63 -4
package/dist/charts.js ADDED
@@ -0,0 +1,436 @@
1
+ import { JALALI_MONTHS, formatJalali, formatNumber, jalaliMonthLength, toGregorian, toISODate, toJalali } from "@kashi-ui/persian";
2
+ import { HTMLBase, define, esc, uid } from "./base.js";
3
+ /** عدد کوتاه برای محور: ۱٬۲۰۰٬۰۰۰ ← ۱٫۲ م */
4
+ const compact = (n) => {
5
+ const a = Math.abs(n);
6
+ const fa = (x, u) => `${formatNumber(Math.round(x * 10) / 10).replace(".", "٫")}${u}`;
7
+ if (a >= 1e9)
8
+ return fa(n / 1e9, " میلیارد");
9
+ if (a >= 1e6)
10
+ return fa(n / 1e6, " م");
11
+ if (a >= 1e3)
12
+ return fa(n / 1e3, " هزار");
13
+ return formatNumber(n);
14
+ };
15
+ /** گام‌های گرد محور: ۰، ۲۵، ۵۰… */
16
+ function niceTicks(max, count = 4) {
17
+ if (max <= 0)
18
+ return [0];
19
+ const raw = max / count;
20
+ const mag = 10 ** Math.floor(Math.log10(raw));
21
+ const step = [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => s >= raw);
22
+ return Array.from({ length: Math.ceil(max / step) + 1 }, (_, i) => i * step);
23
+ }
24
+ const pctOf = (v, total) => `${formatNumber(Math.round((v / (total || 1)) * 1000) / 10).replace(".", "٫")}٪`;
25
+ const chartColor = (i) => `var(--k-chart-${i + 1})`;
26
+ const json = (t, fallback) => {
27
+ if (!t)
28
+ return fallback;
29
+ try {
30
+ return JSON.parse(t);
31
+ }
32
+ catch {
33
+ return fallback;
34
+ }
35
+ };
36
+ /** پایهٔ نمودارها: داده از ویژگی‌های JSON یا JavaScript، با هر تغییر دوباره رسم می‌شود */
37
+ class Chart extends HTMLBase {
38
+ constructor() {
39
+ super(...arguments);
40
+ this.key = uid("kashi-chart");
41
+ this.props = {};
42
+ }
43
+ connectedCallback() {
44
+ this.draw();
45
+ }
46
+ attributeChangedCallback() {
47
+ if (this.isConnected)
48
+ this.draw();
49
+ }
50
+ /** قالب عدد در راهنما و برچسب‌ها */
51
+ get format() {
52
+ const unit = this.getAttribute("unit");
53
+ return this.fmt ?? ((n) => `${formatNumber(Math.round(n))}${unit ? ` ${unit}` : ""}`);
54
+ }
55
+ set format(f) {
56
+ this.fmt = f;
57
+ if (this.isConnected)
58
+ this.draw();
59
+ }
60
+ /** مقدار داده‌شده با JavaScript، وگرنه ویژگی JSON */
61
+ read(name, fallback) {
62
+ return this.props[name] ?? json(this.getAttribute(name), fallback);
63
+ }
64
+ write(name, v) {
65
+ this.props[name] = v;
66
+ if (this.isConnected)
67
+ this.draw();
68
+ }
69
+ /** عنوان نمودار با ویژگی label (نه title، که راهنمای پیش‌فرض مرورگر را روی کل نمودار نشان می‌دهد) */
70
+ get heading() {
71
+ return this.getAttribute("label") ?? "";
72
+ }
73
+ /** راهنمای شناور داخل box در نقطهٔ (x, y)؛ داخل ظرف می‌ماند */
74
+ tip(box, x, y = 0, title, rows = []) {
75
+ let t = box.querySelector(":scope > .k-chart__tip");
76
+ if (x === null) {
77
+ t?.remove();
78
+ return;
79
+ }
80
+ if (!t) {
81
+ t = document.createElement("div");
82
+ t.className = "k-chart__tip";
83
+ t.setAttribute("aria-hidden", "true");
84
+ box.append(t);
85
+ }
86
+ const w = box.clientWidth;
87
+ t.style.left = `${w ? Math.min(Math.max(x, 80), w - 80) : x}px`;
88
+ t.style.top = `${y}px`;
89
+ t.innerHTML = `${title ? `<span class="k-chart__tip-title">${esc(title)}</span>` : ""}${rows.map((r) => `<span class="k-chart__tip-row"${r.color ? ` style="--_c: ${r.color}"` : ""}>${r.name !== undefined ? `<span>${r.color ? "<i></i>" : ""}${esc(r.name)}</span>` : ""}<strong>${esc(r.value)}</strong></span>`).join("")}`;
90
+ }
91
+ /** موقعیت بالای یک عنصر نسبت به ظرف */
92
+ at(el, box) {
93
+ const b = box.getBoundingClientRect(), r = el.getBoundingClientRect();
94
+ return { x: r.left + r.width / 2 - b.left, y: r.top - b.top };
95
+ }
96
+ }
97
+ /** نمودار ستونی: data='[{"label":"…","value":…}]'، یا انباشته با labels و series (تا چهار سری) */
98
+ export class KashiBarChart extends Chart {
99
+ get data() { return this.read("data", []); }
100
+ set data(v) { this.write("data", v); }
101
+ get series() { return this.read("series", []); }
102
+ set series(v) { this.write("series", v); }
103
+ get labels() { return this.read("labels", []); }
104
+ set labels(v) { this.write("labels", v); }
105
+ draw() {
106
+ const f = this.format;
107
+ const stacked = this.series.length ? this.series.slice(0, 4) : null;
108
+ const data = this.data;
109
+ const cats = stacked ? this.labels : data.map((d) => d.label);
110
+ const totals = cats.map((_, i) => (stacked ? stacked.reduce((n, s) => n + (s.values[i] ?? 0), 0) : data[i].value));
111
+ const max = Math.max(0, ...totals);
112
+ const ticks = niceTicks(max);
113
+ const top = ticks.at(-1) || 1;
114
+ const maxI = totals.indexOf(max);
115
+ const labelMax = this.getAttribute("label-max") !== "false";
116
+ const describe = (i) => (stacked ? `${cats[i]}: ${stacked.map((s) => `${s.name} ${f(s.values[i] ?? 0)}`).join("، ")}، جمع ${f(totals[i])}` : `${cats[i]}: ${f(totals[i])}`);
117
+ const height = this.getAttribute("height");
118
+ this.innerHTML = `<figure class="k-chart k-chart--bar" aria-labelledby="${this.key}-t" style="margin: 0${height ? `; --k-chart-height: ${esc(height)}` : ""}"><figcaption class="k-chart__head"><p class="k-chart__title" id="${this.key}-t">${esc(this.heading)}</p>${stacked && stacked.length > 1 ? `<ul class="k-chart__legend" aria-hidden="true">${stacked.map((s, i) => `<li style="--_c: ${chartColor(i)}"><span class="k-chart__key k-chart__key--box"></span>${esc(s.name)}</li>`).join("")}</ul>` : ""}</figcaption><div class="k-chart__plot"><div class="k-bars__grid" aria-hidden="true">${ticks.map((t) => `<span data-v="${esc(compact(t))}"></span>`).join("")}</div><div class="k-bars" role="list" aria-label="${esc(this.heading)}">${cats.map((label, i) => `<div class="k-bars__col" role="listitem" tabindex="0" data-i="${i}" aria-label="${esc(describe(i))}" style="--_d: ${i * 40}ms">${labelMax && i === maxI ? `<span class="k-bars__value" aria-hidden="true">${esc(f(totals[i]))}</span>` : ""}${stacked ? `<span class="k-bars__stack" style="block-size: ${(totals[i] / top) * 100}%" aria-hidden="true">${stacked.map((s, si) => `<span class="k-bars__seg" style="--_c: ${chartColor(si)}; flex-grow: ${s.values[i] ?? 0}"></span>`).join("")}</span>` : `<span class="k-bars__bar" style="block-size: ${(totals[i] / top) * 100}%" aria-hidden="true"></span>`}<span class="k-bars__label" aria-hidden="true">${esc(label)}</span></div>`).join("")}</div></div></figure>`;
119
+ const plot = this.querySelector(".k-chart__plot");
120
+ const show = (col) => {
121
+ const i = Number(col.dataset.i);
122
+ plot.querySelector("[data-active]")?.removeAttribute("data-active");
123
+ col.setAttribute("data-active", "");
124
+ const p = this.at(col.querySelector(".k-bars__bar, .k-bars__stack") ?? col, plot);
125
+ this.tip(plot, p.x, p.y, cats[i], stacked
126
+ ? [...stacked.map((s, si) => ({ name: s.name, value: f(s.values[i] ?? 0), color: chartColor(si) })), { name: "جمع", value: f(totals[i]) }]
127
+ : [{ value: f(totals[i]) }]);
128
+ };
129
+ const hide = () => {
130
+ plot.querySelector("[data-active]")?.removeAttribute("data-active");
131
+ this.tip(plot, null);
132
+ };
133
+ plot.querySelectorAll(".k-bars__col").forEach((col) => {
134
+ col.addEventListener("pointerenter", () => show(col));
135
+ col.addEventListener("focus", () => show(col));
136
+ col.addEventListener("pointerleave", hide);
137
+ col.addEventListener("blur", hide);
138
+ });
139
+ }
140
+ }
141
+ KashiBarChart.observedAttributes = ["label", "data", "labels", "series", "unit", "label-max", "height"];
142
+ /** میله‌های افقی برای رتبه‌بندی: data='[{"label":"…","value":…,"href":"…"}]' */
143
+ export class KashiBarList extends Chart {
144
+ get data() { return this.read("data", []); }
145
+ set data(v) { this.write("data", v); }
146
+ draw() {
147
+ const f = this.format;
148
+ const data = this.data;
149
+ const max = Math.max(1, ...data.map((d) => d.value));
150
+ const total = data.reduce((n, d) => n + d.value, 0);
151
+ this.innerHTML = `<figure class="k-chart" aria-labelledby="${this.key}-t" style="margin: 0"><figcaption class="k-chart__head"><p class="k-chart__title" id="${this.key}-t">${esc(this.heading)}</p></figcaption><ol class="k-barlist" style="position: relative">${data.map((d, i) => `<li class="k-barlist__row" data-i="${i}" style="--_d: ${i * 60}ms"><span class="k-barlist__track"><span class="k-barlist__fill" style="inline-size: ${(d.value / max) * 100}%" aria-hidden="true"></span>${d.href ? `<a class="k-barlist__label" href="${esc(d.href)}">${esc(d.label)}</a>` : `<span class="k-barlist__label">${esc(d.label)}</span>`}</span><span class="k-barlist__value">${esc(f(d.value))} <span class="k-barlist__share">${pctOf(d.value, total)}</span></span></li>`).join("")}</ol></figure>`;
152
+ const box = this.querySelector(".k-barlist");
153
+ box.querySelectorAll(".k-barlist__row").forEach((row) => {
154
+ row.addEventListener("pointerenter", () => {
155
+ const d = data[Number(row.dataset.i)];
156
+ row.setAttribute("data-active", "");
157
+ const p = this.at(row.querySelector(".k-barlist__fill"), box);
158
+ this.tip(box, p.x, p.y, d.label, [{ name: "مقدار", value: f(d.value) }, { name: "سهم از کل", value: pctOf(d.value, total) }]);
159
+ });
160
+ row.addEventListener("pointerleave", () => {
161
+ row.removeAttribute("data-active");
162
+ this.tip(box, null);
163
+ });
164
+ });
165
+ }
166
+ }
167
+ KashiBarList.observedAttributes = ["label", "data", "unit"];
168
+ /** سهم هر بخش از کل در یک میلهٔ افقی (تا چهار بخش): data='[{"label":"…","value":…}]' */
169
+ export class KashiProportionBar extends Chart {
170
+ get data() { return this.read("data", []); }
171
+ set data(v) { this.write("data", v); }
172
+ draw() {
173
+ const f = this.format;
174
+ const parts = this.data.slice(0, 4);
175
+ const total = parts.reduce((n, d) => n + d.value, 0);
176
+ const percent = this.getAttribute("percent") !== "false";
177
+ this.innerHTML = `<figure class="k-chart" aria-labelledby="${this.key}-t" style="margin: 0"><figcaption class="k-chart__head"><p class="k-chart__title" id="${this.key}-t">${esc(this.heading)}</p></figcaption><div class="k-proportion__wrap"><div class="k-proportion" aria-hidden="true">${parts.map((d, i) => `<span class="k-proportion__seg" data-i="${i}" style="--_c: ${chartColor(i)}; flex-grow: ${d.value}"></span>`).join("")}</div></div><ul class="k-proportion__legend">${parts.map((d, i) => `<li data-i="${i}" style="--_c: ${chartColor(i)}"><span class="k-chart__key k-chart__key--box" aria-hidden="true"></span><span>${esc(d.label)}</span><strong>${esc(percent ? pctOf(d.value, total) : f(d.value))}</strong>${percent ? `<span class="k-proportion__abs">${esc(f(d.value))}</span>` : ""}</li>`).join("")}</ul></figure>`;
178
+ const box = this.querySelector(".k-proportion__wrap");
179
+ const mark = (i) => this.querySelectorAll("[data-i]").forEach((el) => el.toggleAttribute("data-active", Number(el.dataset.i) === i));
180
+ box.querySelectorAll(".k-proportion__seg").forEach((seg) => {
181
+ seg.addEventListener("pointerenter", () => {
182
+ const i = Number(seg.dataset.i);
183
+ mark(i);
184
+ const p = this.at(seg, box);
185
+ this.tip(box, p.x, p.y, undefined, [{ name: parts[i].label, value: pctOf(parts[i].value, total), color: chartColor(i) }, { name: "تعداد", value: f(parts[i].value) }]);
186
+ });
187
+ seg.addEventListener("pointerleave", () => {
188
+ mark(null);
189
+ this.tip(box, null);
190
+ });
191
+ });
192
+ }
193
+ }
194
+ KashiProportionBar.observedAttributes = ["label", "data", "unit", "percent"];
195
+ /** نمودار دایره‌ای یا دونات تا پنج بخش (بقیه در «سایر»): data='[{"label":"…","value":…}]'، variant="pie" */
196
+ export class KashiPieChart extends Chart {
197
+ get data() { return this.read("data", []); }
198
+ set data(v) { this.write("data", v); }
199
+ draw() {
200
+ const f = this.format;
201
+ const sorted = [...this.data].sort((a, b) => b.value - a.value);
202
+ const parts = sorted.length > 5 ? [...sorted.slice(0, 4), { label: "سایر", value: sorted.slice(4).reduce((n, d) => n + d.value, 0) }] : sorted;
203
+ const total = parts.reduce((n, d) => n + d.value, 0) || 1;
204
+ const slot = (i) => (i < 4 ? chartColor(i) : "var(--k-text-quaternary)");
205
+ const donut = this.getAttribute("variant") !== "pie";
206
+ const size = Number(this.getAttribute("size")) || 176;
207
+ const totalLabel = this.getAttribute("total-label") ?? "جمع";
208
+ // هر بخش یک کمان با stroke-dasharray؛ برای pie، شعاع نصف و ضخامت برابر قطر
209
+ const R = 50, r = donut ? 40 : 25, w = donut ? 16 : 50;
210
+ const C = 2 * Math.PI * r;
211
+ const gap = parts.length > 1 ? (donut ? 1.6 : 0.8) : 0;
212
+ let acc = 0;
213
+ const arcs = parts.map((d) => {
214
+ const len = (d.value / total) * C;
215
+ const a = { start: acc, len };
216
+ acc += len;
217
+ return a;
218
+ });
219
+ this.innerHTML = `<figure class="k-chart k-pie" aria-labelledby="${this.key}-t" style="margin: 0"><figcaption class="k-chart__head"><p class="k-chart__title" id="${this.key}-t">${esc(this.heading)}</p></figcaption><div class="k-pie__body"><div class="k-pie__plot" style="inline-size: ${size}px; block-size: ${size}px"><svg viewBox="0 0 100 100" class="k-pie__svg" aria-hidden="true">${donut ? `<circle cx="${R}" cy="${R}" r="${r}" fill="none" stroke="var(--k-bg-tertiary)" stroke-width="${w}"></circle>` : ""}${parts.map((d, i) => `<circle class="k-pie__slice" data-i="${i}" cx="${R}" cy="${R}" r="${r}" fill="none" stroke="${slot(i)}" stroke-width="${w}" stroke-dasharray="${Math.max(0, arcs[i].len - gap)} ${C}" stroke-dashoffset="${-arcs[i].start}" transform="rotate(-90 ${R} ${R})" style="--_d: ${i * 80}ms"></circle>`).join("")}</svg>${donut ? `<div class="k-pie__center" aria-hidden="true"><strong>${esc(f(total))}</strong><span>${esc(totalLabel)}</span></div>` : ""}</div><ul class="k-pie__legend">${parts.map((d, i) => `<li data-i="${i}" style="--_c: ${slot(i)}"><span class="k-chart__key k-chart__key--box" aria-hidden="true"></span><span class="k-pie__name">${esc(d.label)}</span><strong>${pctOf(d.value, total)}</strong><span class="k-pie__abs">${esc(f(d.value))}</span></li>`).join("")}</ul></div></figure>`;
220
+ const plot = this.querySelector(".k-pie__plot");
221
+ const center = this.querySelector(".k-pie__center");
222
+ const mid = (i) => {
223
+ const ang = ((arcs[i].start + arcs[i].len / 2) / C) * 2 * Math.PI - Math.PI / 2;
224
+ const rr = donut ? r : r * 1.3;
225
+ return { x: ((R + rr * Math.cos(ang)) / (2 * R)) * size, y: ((R + rr * Math.sin(ang)) / (2 * R)) * size - 8 };
226
+ };
227
+ const activate = (i) => {
228
+ this.querySelectorAll("[data-i]").forEach((el) => el.toggleAttribute("data-active", Number(el.dataset.i) === i));
229
+ if (center)
230
+ center.innerHTML = i === null ? `<strong>${esc(f(total))}</strong><span>${esc(totalLabel)}</span>` : `<strong>${pctOf(parts[i].value, total)}</strong><span>${esc(parts[i].label)}</span>`;
231
+ if (!donut) {
232
+ if (i === null)
233
+ this.tip(plot, null);
234
+ else
235
+ this.tip(plot, mid(i).x, mid(i).y, undefined, [{ name: parts[i].label, value: pctOf(parts[i].value, total), color: slot(i) }, { name: "تعداد", value: f(parts[i].value) }]);
236
+ }
237
+ };
238
+ this.querySelectorAll(".k-pie__slice, .k-pie__legend li").forEach((el) => el.addEventListener("pointerenter", () => activate(Number(el.dataset.i))));
239
+ plot.addEventListener("pointerleave", () => activate(null));
240
+ this.querySelector(".k-pie__legend").addEventListener("pointerleave", () => activate(null));
241
+ }
242
+ }
243
+ KashiPieChart.observedAttributes = ["label", "data", "unit", "variant", "total-label", "size"];
244
+ /** خط روند کوچک برای کارت آمار و جدول: values="12, 18, 15, 22" */
245
+ export class KashiSparkline extends Chart {
246
+ get values() {
247
+ const v = this.props.values;
248
+ return v ?? (this.getAttribute("values") ?? "").split(/[,،\s]+/).filter(Boolean).map(Number).filter((n) => !Number.isNaN(n));
249
+ }
250
+ set values(v) { this.write("values", v); }
251
+ draw() {
252
+ const values = this.values;
253
+ if (!values.length) {
254
+ this.innerHTML = "";
255
+ return;
256
+ }
257
+ const hi = Math.max(...values), lo = Math.min(...values);
258
+ // حداقل دامنه ۲۵٪ مقدار تا تغییر کوچک، زیگزاگ بزرگ دیده نشود
259
+ const span = Math.max(hi - lo, Math.abs(hi) * 0.25) || 1;
260
+ const min = (hi + lo) / 2 - span / 2;
261
+ const d = values.map((v, i) => `${i ? "L" : "M"}${(i / Math.max(1, values.length - 1)) * 100},${28 - ((v - min) / span) * 24}`).join(" ");
262
+ const tone = this.getAttribute("tone") ?? "brand";
263
+ const c = tone === "neutral" ? "var(--k-text-tertiary)" : tone === "brand" ? "var(--k-text-brand)" : `var(--k-text-${tone})`;
264
+ const label = this.getAttribute("label");
265
+ this.innerHTML = `<svg class="k-sparkline" viewBox="0 0 100 30" preserveAspectRatio="none"${label ? ` role="img" aria-label="${esc(label)}"` : ' aria-hidden="true"'} style="color: ${c}">${this.getAttribute("area") !== "false" ? `<path d="${d} L100,30 L0,30 Z" fill="currentColor" opacity="0.1"></path>` : ""}<path d="${d}" fill="none" stroke="currentColor" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round"></path></svg>`;
266
+ }
267
+ }
268
+ KashiSparkline.observedAttributes = ["values", "tone", "area", "label"];
269
+ /**
270
+ * نمودار خطی تا چهار سری: labels='["فروردین", …]' و series='[{"name":"…","values":[…]}]'.
271
+ * راهنمای عمودی با موس و ←→ روی کیبورد؛ محور زمان در RTL از راست شروع می‌شود.
272
+ */
273
+ export class KashiLineChart extends Chart {
274
+ constructor() {
275
+ super(...arguments);
276
+ this.ro = null;
277
+ this.active = null;
278
+ }
279
+ get labels() { return this.read("labels", []); }
280
+ set labels(v) { this.write("labels", v); }
281
+ get series() { return this.read("series", []); }
282
+ set series(v) { this.write("series", v); }
283
+ connectedCallback() {
284
+ super.connectedCallback();
285
+ if (typeof ResizeObserver !== "undefined" && !this.ro) {
286
+ let w = 0;
287
+ this.ro = new ResizeObserver(() => {
288
+ const nw = this.clientWidth;
289
+ if (nw && nw !== w) {
290
+ w = nw;
291
+ this.paintSvg();
292
+ }
293
+ });
294
+ this.ro.observe(this);
295
+ }
296
+ }
297
+ disconnectedCallback() {
298
+ this.ro?.disconnect();
299
+ this.ro = null;
300
+ }
301
+ draw() {
302
+ const shown = this.series.slice(0, 4);
303
+ const f = this.format;
304
+ const labels = this.labels;
305
+ const height = this.getAttribute("height");
306
+ this.innerHTML = `<figure class="k-chart" aria-labelledby="${this.key}-t" style="margin: 0${height ? `; --k-chart-height: ${esc(height)}` : ""}"><figcaption class="k-chart__head"><p class="k-chart__title" id="${this.key}-t">${esc(this.heading)}</p>${shown.length > 1 ? `<ul class="k-chart__legend" aria-hidden="true">${shown.map((s, i) => `<li style="--_c: ${chartColor(i)}"><span class="k-chart__key"></span>${esc(s.name)}</li>`).join("")}</ul>` : ""}</figcaption><div class="k-chart__plot" tabindex="0" role="img" aria-label="${esc(`${this.heading}. با کلیدهای جهت بین نقطه‌ها حرکت کنید.`)}" aria-describedby="${this.key}-live"><svg class="k-chart__svg" aria-hidden="true" direction="ltr"></svg></div><span class="k-sr-only" aria-live="polite" id="${this.key}-live"></span><table class="k-sr-only"><caption>${esc(this.heading)}</caption><thead><tr><th scope="col">دوره</th>${shown.map((s) => `<th scope="col">${esc(s.name)}</th>`).join("")}</tr></thead><tbody>${labels.map((l, i) => `<tr><th scope="row">${esc(l)}</th>${shown.map((s) => `<td>${esc(f(s.values[i]))}</td>`).join("")}</tr>`).join("")}</tbody></table></figure>`;
307
+ const plot = this.querySelector(".k-chart__plot");
308
+ plot.addEventListener("pointermove", (e) => this.pick(e.clientX));
309
+ plot.addEventListener("pointerleave", () => this.setActive(null));
310
+ plot.addEventListener("blur", () => this.setActive(null));
311
+ plot.addEventListener("keydown", (e) => {
312
+ const rtl = getComputedStyle(plot).direction === "rtl";
313
+ const n = labels.length;
314
+ const next = { ArrowLeft: rtl ? 1 : -1, ArrowRight: rtl ? -1 : 1 }[e.key];
315
+ if (next) {
316
+ e.preventDefault();
317
+ this.setActive(Math.min(n - 1, Math.max(0, (this.active ?? (next > 0 ? -1 : n)) + next)));
318
+ }
319
+ else if (e.key === "Home")
320
+ this.setActive(0);
321
+ else if (e.key === "End")
322
+ this.setActive(n - 1);
323
+ else if (e.key === "Escape")
324
+ this.setActive(null);
325
+ });
326
+ this.active = null;
327
+ this.paintSvg();
328
+ }
329
+ geometry() {
330
+ const plot = this.querySelector(".k-chart__plot");
331
+ const shown = this.series.slice(0, 4);
332
+ const labels = this.labels;
333
+ const w = plot.clientWidth || 600, h = plot.clientHeight || 220;
334
+ const rtl = getComputedStyle(plot).direction === "rtl";
335
+ const pad = { top: 12, bottom: 24, start: 52, end: shown.length > 1 ? 64 : 16 };
336
+ const ticks = niceTicks(Math.max(0, ...shown.flatMap((s) => s.values)));
337
+ const top = ticks.at(-1) || 1;
338
+ const innerW = Math.max(1, w - pad.start - pad.end), innerH = Math.max(1, h - pad.top - pad.bottom);
339
+ // محور زمان در جهت خواندن صفحه: در RTL اولین نقطه سمت راست
340
+ const x = (i) => {
341
+ const t = labels.length > 1 ? i / (labels.length - 1) : 0.5;
342
+ return rtl ? w - pad.start - t * innerW : pad.start + t * innerW;
343
+ };
344
+ const y = (v) => pad.top + innerH - (v / top) * innerH;
345
+ return { plot, shown, labels, w, h, rtl, pad, ticks, innerW, innerH, x, y };
346
+ }
347
+ paintSvg() {
348
+ if (!this.querySelector(".k-chart__plot"))
349
+ return;
350
+ const { plot, shown, labels, w, h, rtl, pad, ticks, innerW, innerH, x, y } = this.geometry();
351
+ const svg = plot.querySelector("svg");
352
+ const tickX = rtl ? w - pad.start + 8 : pad.start - 8;
353
+ const anchor = rtl ? "start" : "end";
354
+ const last = labels.length - 1;
355
+ // برچسب انتهای خط‌ها روی هم نیفتند: دست‌کم ۱۴ پیکسل فاصلهٔ عمودی
356
+ const order = shown.map((s, i) => ({ i, y: y(s.values[last]) })).sort((a, b) => a.y - b.y);
357
+ for (let k = 1; k < order.length; k++)
358
+ if (order[k].y - order[k - 1].y < 14)
359
+ order[k].y = order[k - 1].y + 14;
360
+ const endY = Object.fromEntries(order.map((o) => [o.i, o.y]));
361
+ const step = Math.max(1, Math.ceil(labels.length / Math.max(2, Math.floor(innerW / 64))));
362
+ const fade = `${this.key}-fade`;
363
+ const a = this.active;
364
+ svg.innerHTML = `<g class="k-chart__grid">${ticks.map((t) => `<line x1="${pad.start}" x2="${w - pad.end}" y1="${y(t)}" y2="${y(t)}"${rtl ? ` transform="translate(${pad.end - pad.start} 0)"` : ""}></line>`).join("")}</g>${ticks.map((t) => `<text class="k-chart__tick" x="${tickX}" y="${y(t) + 4}" text-anchor="${anchor}">${esc(compact(t))}</text>`).join("")}${labels.map((l, i) => (i % step === 0 || i === last ? `<text class="k-chart__tick" x="${x(i)}" y="${h - 6}" text-anchor="middle">${esc(l)}</text>` : "")).join("")}<defs><linearGradient id="${fade}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color: var(--k-chart-1); stop-opacity: 0.28"></stop><stop offset="1" style="stop-color: var(--k-chart-1); stop-opacity: 0"></stop></linearGradient></defs>${shown.length === 1 && this.getAttribute("area") !== "false" ? `<path class="k-chart__area" fill="url(#${fade})" d="M${x(0)},${y(0)} ${shown[0].values.map((v, i) => `L${x(i)},${y(v)}`).join(" ")} L${x(last)},${y(0)} Z"></path>` : ""}${a !== null ? `<line class="k-chart__cross" x1="${x(a)}" x2="${x(a)}" y1="${pad.top}" y2="${pad.top + innerH}"></line>` : ""}${shown.map((s, si) => `<g style="--_c: ${chartColor(si)}"><path class="k-chart__line" pathLength="1" d="${s.values.map((v, i) => `${i ? "L" : "M"}${x(i)},${y(v)}`).join(" ")}"></path><circle class="k-chart__dot" cx="${x(s.values.length - 1)}" cy="${y(s.values.at(-1) ?? 0)}" r="4"></circle>${shown.length > 1 ? `<text class="k-chart__end" x="${x(last) + (rtl ? -8 : 8)}" y="${endY[si] + 4}" text-anchor="${rtl ? "end" : "start"}">${esc(s.name)}</text>` : ""}${a !== null ? `<circle class="k-chart__dot" cx="${x(a)}" cy="${y(s.values[a])}" r="4.5"></circle>` : ""}</g>`).join("")}`;
365
+ const f = this.format;
366
+ if (a === null)
367
+ this.tip(plot, null);
368
+ else
369
+ this.tip(plot, x(a), Math.min(...shown.map((s) => y(s.values[a]))) - 6, labels[a], shown.map((s, si) => ({ name: s.name, value: f(s.values[a]), color: chartColor(si) })));
370
+ this.querySelector(`#${this.key}-live`).textContent = a === null ? "" : `${labels[a]}: ${shown.map((s) => `${s.name} ${f(s.values[a])}`).join("، ")}`;
371
+ }
372
+ pick(clientX) {
373
+ const { plot, labels, x } = this.geometry();
374
+ const px = clientX - plot.getBoundingClientRect().left;
375
+ let best = 0;
376
+ labels.forEach((_, i) => {
377
+ if (Math.abs(x(i) - px) < Math.abs(x(best) - px))
378
+ best = i;
379
+ });
380
+ if (best !== this.active)
381
+ this.setActive(best);
382
+ }
383
+ setActive(i) {
384
+ this.active = i;
385
+ this.paintSvg();
386
+ }
387
+ }
388
+ KashiLineChart.observedAttributes = ["label", "labels", "series", "unit", "area", "height"];
389
+ const WEEK = ["ش", "", "د", "", "چ", "", "ج"];
390
+ /** نقشهٔ فعالیت سالانه با ماه‌های شمسی: data='{"2026-03-21": 3, …}'، year="1405"، unit="فعالیت" */
391
+ export class KashiActivityHeatmap extends Chart {
392
+ get data() { return this.read("data", {}); }
393
+ set data(v) { this.write("data", v); }
394
+ draw() {
395
+ const data = this.data;
396
+ const year = Number(this.getAttribute("year")) || toJalali(new Date()).jy;
397
+ const unit = this.getAttribute("unit") ?? "فعالیت";
398
+ const days = [];
399
+ for (let m = 1; m <= 12; m++)
400
+ for (let d = 1; d <= jalaliMonthLength(year, m); d++) {
401
+ const date = toGregorian(year, m, d);
402
+ days.push({ date, value: data[toISODate(date)] ?? 0 });
403
+ }
404
+ const max = Math.max(0, ...days.map((d) => d.value));
405
+ const level = (v) => (v <= 0 || max === 0 ? 0 : Math.max(1, Math.ceil((v / max) * 4)));
406
+ // شنبه سطر اول
407
+ const offset = (days[0].date.getDay() + 1) % 7;
408
+ const col = (i) => Math.floor((i + offset) / 7);
409
+ const months = JALALI_MONTHS.map((name, m) => ({ name, col: col(days.findIndex((d) => toJalali(d.date).jm === m + 1)) }));
410
+ const total = days.reduce((s, d) => s + d.value, 0);
411
+ const monthly = JALALI_MONTHS.map((name, m) => ({ name, sum: days.filter((d) => toJalali(d.date).jm === m + 1).reduce((s, d) => s + d.value, 0) }));
412
+ const yearText = formatNumber(year).replace(/٬/g, "");
413
+ const name = this.getAttribute("label") ?? `${unit} در سال ${yearText}`;
414
+ this.innerHTML = `<figure class="k-heatmap" style="margin: 0"><div class="k-heatmap__scroll"><div role="img" aria-label="${esc(`${name}: ${formatNumber(total)} ${unit}`)}"><div class="k-heatmap__months" aria-hidden="true">${months.map((m) => `<span style="grid-column: ${m.col + 1}">${m.name}</span>`).join("")}</div><div class="k-heatmap__body" aria-hidden="true"><div class="k-heatmap__days">${WEEK.map((w) => `<span>${w}</span>`).join("")}</div><div class="k-heatmap__grid">${'<span class="k-heatmap__cell" data-empty=""></span>'.repeat(offset)}${days.map((d, i) => `<span class="k-heatmap__cell" data-l="${level(d.value)}" data-i="${i}"></span>`).join("")}</div></div></div></div><figcaption class="k-heatmap__foot"><span>${formatNumber(total)} ${esc(unit)} در ${yearText}</span><span class="k-heatmap__scale" aria-hidden="true">کمتر ${[0, 1, 2, 3, 4].map((l) => `<span class="k-heatmap__cell" data-l="${l}"></span>`).join("")} بیشتر</span></figcaption><table class="k-sr-only"><caption>${esc(name)}، به تفکیک ماه</caption><tbody>${monthly.map((m) => `<tr><th scope="row">${m.name}</th><td>${formatNumber(m.sum)}</td></tr>`).join("")}</tbody></table></figure>`;
415
+ const fig = this.querySelector("figure");
416
+ this.querySelector(".k-heatmap__grid").addEventListener("pointerover", (e) => {
417
+ const cell = e.target.closest("[data-i]");
418
+ fig.querySelector(".k-heatmap__cell[data-active]")?.removeAttribute("data-active");
419
+ if (!cell)
420
+ return this.tip(fig, null);
421
+ cell.setAttribute("data-active", "");
422
+ const p = this.at(cell, fig);
423
+ const d = days[Number(cell.dataset.i)];
424
+ this.tip(fig, p.x, p.y - 2, formatJalali(d.date, { style: "full" }), [{ value: `${formatNumber(d.value)} ${unit}` }]);
425
+ });
426
+ fig.addEventListener("pointerleave", () => this.tip(fig, null));
427
+ }
428
+ }
429
+ KashiActivityHeatmap.observedAttributes = ["data", "year", "unit", "label"];
430
+ define("kashi-bar-chart", KashiBarChart, "block");
431
+ define("kashi-line-chart", KashiLineChart, "block");
432
+ define("kashi-pie-chart", KashiPieChart, "block");
433
+ define("kashi-bar-list", KashiBarList, "block");
434
+ define("kashi-proportion-bar", KashiProportionBar, "block");
435
+ define("kashi-sparkline", KashiSparkline);
436
+ define("kashi-activity-heatmap", KashiActivityHeatmap, "block");
@@ -0,0 +1,106 @@
1
+ import { HTMLBase } from "./base.js";
2
+ export interface ComboboxOption {
3
+ value: string;
4
+ label: string;
5
+ description?: string;
6
+ disabled?: boolean;
7
+ /** نام گروه برای دسته‌بندی گزینه‌ها */
8
+ group?: string;
9
+ /** کلمه‌های اضافه برای جستجو، مثل نام انگلیسی */
10
+ keywords?: string[];
11
+ }
12
+ /** متن یکدست برای جستجو: ي/ی، ك/ک، ارقام، حروف کوچک */
13
+ export declare const searchText: (s: string) => string;
14
+ /**
15
+ * جستجو و انتخاب با الگوی Combobox در WAI-ARIA: تایپ فهرست را فیلتر می‌کند، ↑↓ حرکت، Enter انتخاب،
16
+ * Esc بستن (و در حالت بسته، پاک کردن). جستجو «علي» و «علی» را یکی می‌داند.
17
+ * گزینه‌ها از فرزندان <option> (با data-description و <optgroup>) یا ویژگی options با JavaScript.
18
+ */
19
+ export declare class KashiCombobox extends HTMLBase {
20
+ static observedAttributes: string[];
21
+ protected selected: ComboboxOption | null;
22
+ protected query: string;
23
+ private list;
24
+ private filtered;
25
+ private typing;
26
+ private isOpen;
27
+ private active;
28
+ private rendered;
29
+ protected valueSet: boolean;
30
+ protected key: string;
31
+ protected input: HTMLInputElement;
32
+ private listbox;
33
+ private live;
34
+ /** برچسب پیش‌فرض وقتی label داده نشود */
35
+ protected defaultLabel: string | null;
36
+ protected defaultPlaceholder: string;
37
+ connectedCallback(): void;
38
+ disconnectedCallback(): void;
39
+ attributeChangedCallback(name: string, old: string | null, next: string | null): void;
40
+ /** گزینه‌ها با JavaScript: [{ value, label, description?, group?, keywords?, disabled? }] */
41
+ get options(): ComboboxOption[];
42
+ set options(list: ComboboxOption[]);
43
+ /** مقدار گزینهٔ انتخاب‌شده، یا رشتهٔ خالی */
44
+ get value(): string;
45
+ set value(v: string);
46
+ /** گزینهٔ انتخاب‌شده با همهٔ اطلاعاتش */
47
+ get selectedOption(): ComboboxOption | null;
48
+ focus(options?: FocusOptions): void;
49
+ private readOptions;
50
+ /** گزینه‌های قابل نمایش برای این جستجو؛ زیرکلاس‌ها (شهر) گزینهٔ «شهر دیگر» اضافه می‌کنند */
51
+ protected source(_query: string): ComboboxOption[];
52
+ protected accepts(o: ComboboxOption, query: string): boolean;
53
+ protected find(value: string): ComboboxOption | null;
54
+ /** اطلاعات رویداد kashi-change */
55
+ protected detail(): Record<string, unknown>;
56
+ private render;
57
+ /** متن فیلد، فیلد پنهان، خطا و فهرست را با وضعیت فعلی هماهنگ می‌کند */
58
+ protected sync(): void;
59
+ protected syncHidden(): void;
60
+ private enabled;
61
+ private open;
62
+ close(): void;
63
+ private onOutside;
64
+ private filter;
65
+ private renderList;
66
+ private setActive;
67
+ private move;
68
+ private onKey;
69
+ protected commit(o: ComboboxOption | null): void;
70
+ }
71
+ /** انتخاب یکی از ۳۱ استان با جستجو؛ با نام مرکز استان هم پیدا می‌شود (مثلاً «مشهد») */
72
+ export declare class KashiProvinceSelect extends KashiCombobox {
73
+ static observedAttributes: string[];
74
+ protected defaultLabel: string;
75
+ protected defaultPlaceholder: string;
76
+ protected source(): ComboboxOption[];
77
+ protected detail(): {
78
+ value: string;
79
+ province: import("@kashi-ui/persian").Province | null;
80
+ };
81
+ }
82
+ /**
83
+ * انتخاب شهر با جستجو؛ با province فقط شهرهای همان استان، بدون آن همهٔ ۱۲۰۰+ شهر ایران با نام استان.
84
+ * شهری که در فهرست نیست (شهرهای تازه‌تأسیس) با همان نام تایپ‌شده پذیرفته می‌شود، مگر allow-custom="false".
85
+ */
86
+ export declare class KashiCitySelect extends KashiCombobox {
87
+ static observedAttributes: string[];
88
+ protected defaultLabel: string;
89
+ private custom;
90
+ get province(): string | null;
91
+ set province(p: string | null);
92
+ /** نام شهر */
93
+ get value(): string;
94
+ set value(city: string);
95
+ protected find(value: string): ComboboxOption | null;
96
+ attributeChangedCallback(name: string, old: string | null, next: string | null): void;
97
+ connectedCallback(): void;
98
+ private placeholderText;
99
+ protected source(query: string): ComboboxOption[];
100
+ protected accepts(o: ComboboxOption, q: string): boolean;
101
+ protected commit(o: ComboboxOption | null): void;
102
+ protected detail(): {
103
+ value: string;
104
+ province: string | null;
105
+ };
106
+ }