@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
@@ -0,0 +1,655 @@
1
+ import { JALALI_MONTHS, formatJalali, fromISODate, parseJalali, toFaDigits, toGregorian, toISODate, toJalali } from "@kashi-ui/persian";
2
+ import { HTMLBase, ICONS, bool, define, emit, esc, fieldHTML, setMessages, uid } from "./base.js";
3
+ import "./calendar.js";
4
+ const parseDate = (t) => (t ? (fromISODate(t.trim()) ?? parseJalali(t)) : null);
5
+ const day = (d, add = 0) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + add);
6
+ const same = (a, b) => (!a && !b) || (!!a && !!b && a.getTime() === b.getTime());
7
+ const PRESETS = [
8
+ ["۷ روز اخیر", (t) => ({ start: day(t, -6), end: t })],
9
+ ["۳۰ روز اخیر", (t) => ({ start: day(t, -29), end: t })],
10
+ ["این ماه", (t) => {
11
+ const { jy, jm } = toJalali(t);
12
+ return { start: toGregorian(jy, jm, 1), end: t };
13
+ }],
14
+ ["ماه قبل", (t) => {
15
+ const { jy, jm } = toJalali(t);
16
+ const [py, pm] = jm === 1 ? [jy - 1, 12] : [jy, jm - 1];
17
+ return { start: toGregorian(py, pm, 1), end: day(toGregorian(jy, jm, 1), -1) };
18
+ }],
19
+ ];
20
+ /**
21
+ * انتخاب بازهٔ تاریخ شمسی با تقویم و میان‌برها (۷ روز اخیر، این ماه، …).
22
+ * با name دو فیلد name_start و name_end با تاریخ میلادی YYYY-MM-DD ارسال می‌شوند.
23
+ */
24
+ export class KashiDateRangePicker extends HTMLBase {
25
+ constructor() {
26
+ super(...arguments);
27
+ this.current = { start: null, end: null };
28
+ this.draft = { start: null, end: null };
29
+ this.valueSet = false;
30
+ this.rendered = false;
31
+ this.key = uid("kashi-range");
32
+ this.pop = null;
33
+ this.onOutside = (e) => {
34
+ if (!this.contains(e.target))
35
+ this.close(false);
36
+ };
37
+ }
38
+ connectedCallback() {
39
+ if (!this.rendered && !this.valueSet)
40
+ this.current = { start: parseDate(this.getAttribute("start")), end: parseDate(this.getAttribute("end")) };
41
+ this.render();
42
+ }
43
+ disconnectedCallback() {
44
+ this.close(false);
45
+ }
46
+ attributeChangedCallback(name, old, next) {
47
+ if (!this.rendered || old === next)
48
+ return;
49
+ if (name === "start" || name === "end")
50
+ this.current = { ...this.current, [name]: parseDate(next) };
51
+ this.render();
52
+ }
53
+ /** { start, end } با Date */
54
+ get value() {
55
+ return { ...this.current };
56
+ }
57
+ set value(r) {
58
+ this.valueSet = true;
59
+ this.current = { start: r?.start ?? null, end: r?.end ?? null };
60
+ if (this.rendered)
61
+ this.render();
62
+ }
63
+ fmt(d) {
64
+ return formatJalali(d, { digits: this.getAttribute("digits") === "latin" ? "latin" : "fa" });
65
+ }
66
+ text(r) {
67
+ return r.start && r.end ? `${this.fmt(r.start)} تا ${this.fmt(r.end)}` : null;
68
+ }
69
+ render() {
70
+ this.close(false);
71
+ const id = this.key;
72
+ const label = this.getAttribute("label") ?? "بازهٔ تاریخ";
73
+ const text = this.text(this.current);
74
+ const control = `<div class="k-datepicker k-daterange"><div class="k-select"><button id="${id}" type="button" class="k-select__control k-daterange__trigger" aria-haspopup="dialog" aria-expanded="false"${text ? "" : ' data-placeholder=""'}${this.hasAttribute("disabled") ? " disabled" : ""}>${ICONS.calendar}<span>${esc(text ?? this.getAttribute("placeholder") ?? "انتخاب بازهٔ تاریخ")}</span></button></div></div>`;
75
+ this.querySelector(":scope > .k-field")?.remove();
76
+ this.insertAdjacentHTML("afterbegin", fieldHTML({ id, label, control }));
77
+ this.trigger = this.querySelector(".k-daterange__trigger");
78
+ this.trigger.addEventListener("click", () => (this.pop ? this.close() : this.open()));
79
+ this.rendered = true;
80
+ setMessages(this.querySelector(":scope > .k-field"), this.trigger, id, this.getAttribute("error"), this.getAttribute("hint"));
81
+ const name = this.getAttribute("name");
82
+ this.querySelectorAll(":scope > input[type=hidden]").forEach((el) => el.remove());
83
+ if (name) {
84
+ for (const [k, d] of [["start", this.current.start], ["end", this.current.end]])
85
+ this.insertAdjacentHTML("beforeend", `<input type="hidden" name="${esc(`${name}_${k}`)}" value="${d ? toISODate(d) : ""}">`);
86
+ }
87
+ }
88
+ open() {
89
+ if (this.pop || this.hasAttribute("disabled"))
90
+ return;
91
+ this.draft = { ...this.current };
92
+ const wrap = this.querySelector(".k-daterange");
93
+ const pop = document.createElement("div");
94
+ pop.id = `${this.key}-popover`;
95
+ pop.className = "k-datepicker__popover k-daterange__popover";
96
+ pop.setAttribute("role", "dialog");
97
+ pop.setAttribute("aria-label", this.getAttribute("label") ?? "انتخاب بازهٔ تاریخ");
98
+ const presets = this.getAttribute("presets") === "false" ? [] : PRESETS;
99
+ pop.innerHTML = `${presets.length ? `<ul class="k-daterange__presets" role="list" aria-label="میان‌برها">${presets.map(([l], i) => `<li><button type="button" class="k-daterange__preset" data-preset="${i}" aria-pressed="false">${l}</button></li>`).join("")}</ul>` : ""}<div class="k-daterange__main"><div class="k-daterange__footer"><span class="k-daterange__summary" aria-live="polite"></span><button type="button" class="k-btn k-btn--secondary k-btn--sm" data-cancel>انصراف</button><button type="button" class="k-btn k-btn--primary k-btn--sm" data-apply>تأیید</button></div></div>`;
100
+ const cal = document.createElement("kashi-calendar");
101
+ for (const a of ["min", "max", "digits", "holidays"]) {
102
+ const v = this.getAttribute(a);
103
+ if (v !== null)
104
+ cal.setAttribute(a, v);
105
+ }
106
+ cal.range = this.draft;
107
+ pop.querySelector(".k-daterange__main").prepend(cal);
108
+ cal.addEventListener("kashi-change", (e) => {
109
+ e.stopPropagation();
110
+ const d = e.detail.date;
111
+ const r = this.draft;
112
+ this.draft = !r.start || r.end ? { start: d, end: null } : d < r.start ? { start: d, end: r.start } : { start: r.start, end: d };
113
+ cal.range = this.draft;
114
+ this.paintDraft();
115
+ });
116
+ pop.addEventListener("click", (e) => {
117
+ const t = e.target;
118
+ const preset = t.closest("[data-preset]");
119
+ if (preset) {
120
+ this.draft = presets[Number(preset.dataset.preset)][1](day(new Date()));
121
+ cal.range = this.draft;
122
+ this.paintDraft();
123
+ }
124
+ else if (t.closest("[data-cancel]"))
125
+ this.close();
126
+ else if (t.closest("[data-apply]") && this.draft.start && this.draft.end) {
127
+ const changed = !same(this.draft.start, this.current.start) || !same(this.draft.end, this.current.end);
128
+ this.current = { ...this.draft };
129
+ this.close();
130
+ this.render();
131
+ this.trigger.focus();
132
+ if (changed)
133
+ emit(this, "kashi-change", { start: this.current.start, end: this.current.end, value: { start: toISODate(this.current.start), end: toISODate(this.current.end) } });
134
+ }
135
+ });
136
+ pop.addEventListener("keydown", (e) => {
137
+ if (e.key === "Escape") {
138
+ e.stopPropagation();
139
+ this.close();
140
+ }
141
+ });
142
+ wrap.append(pop);
143
+ this.pop = pop;
144
+ this.trigger.setAttribute("aria-expanded", "true");
145
+ this.trigger.setAttribute("aria-controls", pop.id);
146
+ document.addEventListener("pointerdown", this.onOutside);
147
+ this.paintDraft();
148
+ cal.focus();
149
+ }
150
+ paintDraft() {
151
+ const pop = this.pop;
152
+ const r = this.draft;
153
+ pop.querySelector(".k-daterange__summary").textContent = this.text(r) ?? (r.start ? "حالا تاریخ پایان را انتخاب کنید." : "");
154
+ pop.querySelector("[data-apply]").disabled = !r.start || !r.end;
155
+ const today = day(new Date());
156
+ pop.querySelectorAll("[data-preset]").forEach((b, i) => {
157
+ const p = PRESETS[i][1](today);
158
+ b.setAttribute("aria-pressed", String(same(r.start, p.start) && same(r.end, p.end)));
159
+ });
160
+ }
161
+ close(focus = true) {
162
+ if (!this.pop)
163
+ return;
164
+ this.pop.remove();
165
+ this.pop = null;
166
+ document.removeEventListener("pointerdown", this.onOutside);
167
+ this.trigger.setAttribute("aria-expanded", "false");
168
+ this.trigger.removeAttribute("aria-controls");
169
+ if (focus)
170
+ this.trigger.focus();
171
+ }
172
+ }
173
+ KashiDateRangePicker.observedAttributes = ["label", "hint", "error", "start", "end", "min", "max", "holidays", "digits", "name", "disabled", "presets", "placeholder"];
174
+ const ARROW = (next) => `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-k-mirror=""><path d="${next ? "m8 5 5 5-5 5" : "m12 5-5 5 5 5"}"/></svg>`;
175
+ /** انتخاب ماه شمسی برای گزارش‌ها و فیلترها؛ مقدار «1405-01» (سال-ماه شمسی) */
176
+ export class KashiMonthPicker extends HTMLBase {
177
+ constructor() {
178
+ super(...arguments);
179
+ this.selected = null;
180
+ this.year = toJalali(new Date()).jy;
181
+ this.focusMonth = 1;
182
+ this.valueSet = false;
183
+ this.rendered = false;
184
+ }
185
+ connectedCallback() {
186
+ if (!this.rendered) {
187
+ if (!this.valueSet)
188
+ this.selected = this.parse(this.getAttribute("value"));
189
+ const t = toJalali(new Date());
190
+ this.year = this.selected?.year ?? t.jy;
191
+ this.focusMonth = this.selected?.month ?? (this.year === t.jy ? t.jm : 1);
192
+ }
193
+ this.render();
194
+ }
195
+ attributeChangedCallback(name, old, next) {
196
+ if (!this.rendered || old === next)
197
+ return;
198
+ if (name === "value")
199
+ this.value = next ?? "";
200
+ else
201
+ this.render();
202
+ }
203
+ parse(v) {
204
+ const m = (v ?? "").trim().match(/^(\d{4})[-/](\d{1,2})$/);
205
+ return m ? { year: Number(m[1]), month: Number(m[2]) } : null;
206
+ }
207
+ /** «1405-01» */
208
+ get value() {
209
+ return this.selected ? `${this.selected.year}-${String(this.selected.month).padStart(2, "0")}` : "";
210
+ }
211
+ set value(v) {
212
+ this.valueSet = true;
213
+ this.selected = this.parse(v);
214
+ if (this.selected)
215
+ [this.year, this.focusMonth] = [this.selected.year, this.selected.month];
216
+ if (this.rendered)
217
+ this.render();
218
+ }
219
+ out(y, m) {
220
+ const k = y * 12 + m;
221
+ const min = this.parse(this.getAttribute("min")), max = this.parse(this.getAttribute("max"));
222
+ return (!!min && k < min.year * 12 + min.month) || (!!max && k > max.year * 12 + max.month);
223
+ }
224
+ render(focus = false) {
225
+ const f = (n) => (this.getAttribute("digits") === "latin" ? String(n) : toFaDigits(String(n)));
226
+ const t = toJalali(new Date());
227
+ const min = this.parse(this.getAttribute("min")), max = this.parse(this.getAttribute("max"));
228
+ const y = this.year;
229
+ this.innerHTML = `<div class="k-monthpicker" role="group" aria-label="${esc(this.getAttribute("label") ?? "انتخاب ماه")}"><div class="k-monthpicker__header"><button type="button" class="k-btn k-btn--tertiary k-btn--icon k-btn--sm" data-year="-1" aria-label="سال قبل"${min && y <= min.year ? " disabled" : ""}>${ARROW(false)}</button><span class="k-monthpicker__year" aria-live="polite">${f(y)}</span><button type="button" class="k-btn k-btn--tertiary k-btn--icon k-btn--sm" data-year="1" aria-label="سال بعد"${max && y >= max.year ? " disabled" : ""}>${ARROW(true)}</button></div><div class="k-monthpicker__grid">${JALALI_MONTHS.map((name, i) => `<button type="button" data-month="${i + 1}" class="k-monthpicker__month" tabindex="${this.focusMonth === i + 1 ? 0 : -1}" aria-pressed="${this.selected?.year === y && this.selected.month === i + 1}" aria-label="${name} ${f(y)}"${t.jy === y && t.jm === i + 1 ? ' aria-current="date"' : ""}${this.out(y, i + 1) ? " disabled" : ""}>${name}</button>`).join("")}</div></div>`;
230
+ const name = this.getAttribute("name");
231
+ if (name)
232
+ this.insertAdjacentHTML("beforeend", `<input type="hidden" name="${esc(name)}" value="${this.value}">`);
233
+ this.rendered = true;
234
+ this.querySelectorAll("[data-year]").forEach((b) => b.addEventListener("click", () => {
235
+ this.year += Number(b.dataset.year);
236
+ this.render();
237
+ }));
238
+ const grid = this.querySelector(".k-monthpicker__grid");
239
+ grid.addEventListener("click", (e) => {
240
+ const b = e.target.closest("[data-month]");
241
+ if (!b || b.disabled)
242
+ return;
243
+ this.selected = { year: this.year, month: Number(b.dataset.month) };
244
+ this.focusMonth = this.selected.month;
245
+ this.render(true);
246
+ emit(this, "kashi-change", { value: this.value, year: this.selected.year, month: this.selected.month });
247
+ });
248
+ grid.addEventListener("keydown", (e) => {
249
+ const rtl = getComputedStyle(grid).direction === "rtl";
250
+ const delta = { ArrowRight: rtl ? -1 : 1, ArrowLeft: rtl ? 1 : -1, ArrowDown: 3, ArrowUp: -3, PageUp: -12, PageDown: 12 };
251
+ let idx = this.year * 12 + this.focusMonth - 1;
252
+ if (e.key in delta)
253
+ idx += delta[e.key];
254
+ else if (e.key === "Home")
255
+ idx = this.year * 12;
256
+ else if (e.key === "End")
257
+ idx = this.year * 12 + 11;
258
+ else
259
+ return;
260
+ e.preventDefault();
261
+ this.year = Math.floor(idx / 12);
262
+ this.focusMonth = (idx % 12) + 1;
263
+ this.render(true);
264
+ });
265
+ if (focus)
266
+ this.querySelector(`[data-month="${this.focusMonth}"]`)?.focus();
267
+ }
268
+ }
269
+ KashiMonthPicker.observedAttributes = ["value", "min", "max", "digits", "label", "name"];
270
+ /** حجم فایل به فارسی: «۱٫۲ مگابایت» */
271
+ export function formatBytes(bytes) {
272
+ const units = ["بایت", "کیلوبایت", "مگابایت", "گیگابایت"];
273
+ let v = bytes, i = 0;
274
+ while (v >= 1024 && i < units.length - 1) {
275
+ v /= 1024;
276
+ i++;
277
+ }
278
+ const n = i === 0 ? String(v) : v.toFixed(v < 10 ? 1 : 0).replace(/\.0$/, "");
279
+ return `${toFaDigits(n).replace(".", "٫")} ${units[i]}`;
280
+ }
281
+ const UPLOAD = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13V3.5m-4 4 4-4 4 4M4 13v2.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V13"/></svg>';
282
+ const TRASH = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 5.5h13M8 5.5V4h4v1.5M5 5.5l.8 10.2a1.5 1.5 0 0 0 1.5 1.3h5.4a1.5 1.5 0 0 0 1.5-1.3L15 5.5"/></svg>';
283
+ /**
284
+ * بارگذاری فایل با کشیدن و رها کردن، بررسی نوع و حجم و فهرست فایل‌ها.
285
+ * با name فایل‌ها با فرم HTML ارسال می‌شوند؛ برای بارگذاری فوری، element.uploader = (file, onProgress) => Promise بدهید.
286
+ */
287
+ export class KashiFileUpload extends HTMLBase {
288
+ constructor() {
289
+ super(...arguments);
290
+ this.items = [];
291
+ this.rendered = false;
292
+ this.key = uid("kashi-upload");
293
+ }
294
+ connectedCallback() {
295
+ if (!this.rendered)
296
+ this.render();
297
+ }
298
+ attributeChangedCallback(_n, old, next) {
299
+ if (this.rendered && old !== next)
300
+ this.render();
301
+ }
302
+ /** فایل‌های پذیرفته‌شده */
303
+ get files() {
304
+ return this.items.filter((i) => i.status !== "error").map((i) => i.file);
305
+ }
306
+ get multiple() {
307
+ return bool(this, "multiple", true);
308
+ }
309
+ render() {
310
+ const k = this.key;
311
+ const hint = this.getAttribute("hint");
312
+ const off = this.hasAttribute("disabled");
313
+ const name = this.getAttribute("name");
314
+ const accept = this.getAttribute("accept");
315
+ this.innerHTML = `<div class="k-dropzone"${off ? ' data-disabled=""' : ""}><span class="k-dropzone__icon" aria-hidden="true">${UPLOAD}</span><p class="k-dropzone__title" style="margin: 0"><button type="button" class="k-dropzone__browse"${off ? " disabled" : ""}${hint ? ` aria-describedby="${k}-hint"` : ""}>${esc(this.getAttribute("label") ?? "انتخاب فایل")}</button> <span>یا فایل را اینجا رها کنید</span></p>${hint ? `<p class="k-dropzone__hint" id="${k}-hint" style="margin: 0">${esc(hint)}</p>` : ""}<input type="file" tabindex="-1" aria-hidden="true"${accept ? ` accept="${esc(accept)}"` : ""}${this.multiple ? " multiple" : ""}${name ? ` name="${esc(name)}"` : ""}></div><ul class="k-file-list" role="list" hidden></ul><span class="k-sr-only" aria-live="polite"></span>`;
316
+ this.rendered = true;
317
+ this.input = this.querySelector("input[type=file]");
318
+ const zone = this.querySelector(".k-dropzone");
319
+ this.querySelector(".k-dropzone__browse").addEventListener("click", () => this.input.click());
320
+ this.input.addEventListener("change", () => {
321
+ if (this.input.files?.length)
322
+ this.add([...this.input.files]);
323
+ });
324
+ zone.addEventListener("dragover", (e) => {
325
+ e.preventDefault();
326
+ if (!off)
327
+ zone.setAttribute("data-dragging", "");
328
+ });
329
+ zone.addEventListener("dragleave", (e) => {
330
+ if (!zone.contains(e.relatedTarget))
331
+ zone.removeAttribute("data-dragging");
332
+ });
333
+ zone.addEventListener("drop", (e) => {
334
+ e.preventDefault();
335
+ zone.removeAttribute("data-dragging");
336
+ if (!off && e.dataTransfer?.files.length)
337
+ this.add([...e.dataTransfer.files]);
338
+ });
339
+ this.querySelector(".k-file-list").addEventListener("click", (e) => {
340
+ const b = e.target.closest("[data-remove]");
341
+ if (!b)
342
+ return;
343
+ const it = this.items.find((x) => x.id === b.dataset.remove);
344
+ this.items = this.items.filter((x) => x.id !== b.dataset.remove);
345
+ this.announce(`${it?.file.name ?? ""} حذف شد`);
346
+ this.changed();
347
+ });
348
+ this.paint();
349
+ }
350
+ accepts(file) {
351
+ const accept = this.getAttribute("accept");
352
+ if (!accept)
353
+ return true;
354
+ return accept.split(",").map((a) => a.trim().toLowerCase()).some((a) => a.startsWith(".") ? file.name.toLowerCase().endsWith(a) : a.endsWith("/*") ? file.type.startsWith(a.slice(0, -1)) : file.type === a);
355
+ }
356
+ add(files) {
357
+ const list = this.multiple ? files : files.slice(0, 1);
358
+ const maxSize = Number(this.getAttribute("max-size")) || 0;
359
+ const maxFiles = Number(this.getAttribute("max-files")) || 0;
360
+ const kept = this.multiple ? this.items.filter((i) => i.status !== "error").length : 0;
361
+ const added = list.map((file, i) => {
362
+ let error;
363
+ if (!this.accepts(file))
364
+ error = "این نوع فایل پذیرفته نیست.";
365
+ else if (maxSize && file.size > maxSize)
366
+ error = `حجم فایل بیشتر از ${formatBytes(maxSize)} است.`;
367
+ else if (maxFiles && kept + i >= maxFiles)
368
+ error = `حداکثر ${toFaDigits(maxFiles)} فایل.`;
369
+ const up = !error && !!this.uploader;
370
+ return { id: uid("f"), file, status: error ? "error" : up ? "uploading" : "done", progress: error ? 0 : up ? 0 : 100, error };
371
+ });
372
+ this.items = this.multiple ? [...this.items, ...added] : added;
373
+ this.announce(added.map((a) => `${a.file.name}: ${a.error ?? (this.uploader ? "در حال بارگذاری" : "بارگذاری شد")}`).join("، "));
374
+ this.changed();
375
+ for (const it of added) {
376
+ if (it.status !== "uploading" || !this.uploader)
377
+ continue;
378
+ this.uploader(it.file, (p) => {
379
+ it.progress = p;
380
+ this.paint();
381
+ }).then(() => {
382
+ it.status = "done";
383
+ it.progress = 100;
384
+ this.announce(`${it.file.name}: بارگذاری شد`);
385
+ this.changed();
386
+ }, (e) => {
387
+ it.status = "error";
388
+ it.error = e instanceof Error ? e.message : "خطا در بارگذاری";
389
+ this.announce(`${it.file.name}: ${it.error}`);
390
+ this.changed();
391
+ });
392
+ }
393
+ }
394
+ announce(t) {
395
+ this.querySelector(":scope > .k-sr-only").textContent = t;
396
+ }
397
+ changed() {
398
+ // فیلد file فرم همیشه همان فایل‌های پذیرفته‌شدهٔ فهرست را دارد
399
+ try {
400
+ const dt = new DataTransfer();
401
+ for (const f of this.files)
402
+ dt.items.add(f);
403
+ this.input.files = dt.files;
404
+ }
405
+ catch { /* مرورگر قدیمی */ }
406
+ this.paint();
407
+ emit(this, "kashi-change", { files: this.files });
408
+ }
409
+ paint() {
410
+ const list = this.querySelector(".k-file-list");
411
+ list.hidden = !this.items.length;
412
+ const ext = (n) => (n.split(".").pop() ?? "").slice(0, 4).toUpperCase();
413
+ list.innerHTML = this.items.map((it) => `<li class="k-file" data-status="${it.status}"><span class="k-file__icon" aria-hidden="true">${esc(ext(it.file.name))}</span><span class="k-file__name" title="${esc(it.file.name)}">${esc(it.file.name)}</span><button type="button" class="k-btn k-btn--tertiary k-btn--icon k-btn--sm k-file__remove" data-remove="${it.id}" aria-label="حذف ${esc(it.file.name)}">${TRASH}</button><span class="k-file__meta">${esc(it.error ?? `${formatBytes(it.file.size)}${it.status === "uploading" ? ` · ${toFaDigits(Math.round(it.progress))}٪` : it.status === "done" ? " · بارگذاری شد" : ""}`)}</span>${it.status === "uploading" ? `<div role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${Math.round(it.progress)}" aria-label="در حال بارگذاری ${esc(it.file.name)}" class="k-progress k-progress--sm"><div class="k-progress__bar" style="--k-value: ${it.progress}%"></div></div>` : ""}</li>`).join("");
414
+ }
415
+ }
416
+ KashiFileUpload.observedAttributes = ["accept", "multiple", "max-size", "max-files", "hint", "name", "disabled", "label"];
417
+ const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
418
+ const norm = (v) => {
419
+ const m = v.trim().match(HEX);
420
+ if (!m)
421
+ return null;
422
+ return `#${(m[1].length === 3 ? [...m[1]].map((c) => c + c).join("") : m[1]).toLowerCase()}`;
423
+ };
424
+ const rgbOf = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
425
+ const toHex = (r, g, b) => `#${[r, g, b].map((n) => Math.round(Math.min(255, Math.max(0, n))).toString(16).padStart(2, "0")).join("")}`;
426
+ const lum = (hex) => {
427
+ const [r, g, b] = rgbOf(hex).map((n) => n / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
428
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
429
+ };
430
+ /** نسبت کنتراست دو رنگ طبق WCAG */
431
+ export const contrastRatio = (a, b) => {
432
+ const [x, y] = [lum(norm(a) ?? a), lum(norm(b) ?? b)].sort((m, n) => n - m);
433
+ return (x + 0.05) / (y + 0.05);
434
+ };
435
+ function hexToHsv(hex) {
436
+ const [r, g, b] = rgbOf(hex).map((n) => n / 255);
437
+ const max = Math.max(r, g, b), d = max - Math.min(r, g, b);
438
+ let h = 0;
439
+ if (d)
440
+ h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
441
+ return { h: (h * 60 + 360) % 360, s: max ? d / max : 0, v: max };
442
+ }
443
+ function hsvToHex({ h, s, v }) {
444
+ const f = (n) => {
445
+ const k = (n + h / 60) % 6;
446
+ return (v - v * s * Math.max(0, Math.min(k, 4 - k, 1))) * 255;
447
+ };
448
+ return toHex(f(5), f(3), f(1));
449
+ }
450
+ const SWATCHES = [["فیروزه‌ای", "#0b8077"], ["لاجوردی", "#2740ed"], ["بنفش", "#7c3aed"], ["اناری", "#e51d2a"], ["زعفرانی", "#dd5e02"], ["پسته‌ای", "#518b25"], ["شب", "#2a2f3d"]];
451
+ const SW_CHECK = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 10.5 3.2 3L15 7"/></svg>';
452
+ /**
453
+ * انتخاب رنگ: رنگ‌های پیشنهادی، پنل اشباع و رنگ‌مایه، کد HEX و RGB، و کنتراست با متن سفید یا مشکی.
454
+ * رنگ‌های پیشنهادی دلخواه با swatches="#0b8077 فیروزه‌ای, #2740ed لاجوردی".
455
+ */
456
+ export class KashiColorPicker extends HTMLBase {
457
+ constructor() {
458
+ super(...arguments);
459
+ this.current = "#0b8077";
460
+ this.hsv = hexToHsv("#0b8077");
461
+ this.valueSet = false;
462
+ this.rendered = false;
463
+ this.key = uid("kashi-color");
464
+ this.panel = null;
465
+ this.onOutside = (e) => {
466
+ if (!this.querySelector(".k-color__anchor")?.contains(e.target))
467
+ this.closePanel(false);
468
+ };
469
+ }
470
+ connectedCallback() {
471
+ if (!this.rendered && !this.valueSet)
472
+ this.setColor(norm(this.getAttribute("value") ?? "") ?? "#0b8077", false);
473
+ this.render();
474
+ }
475
+ disconnectedCallback() {
476
+ document.removeEventListener("pointerdown", this.onOutside);
477
+ }
478
+ attributeChangedCallback(name, old, next) {
479
+ if (!this.rendered || old === next)
480
+ return;
481
+ if (name === "value")
482
+ this.value = next ?? "";
483
+ else
484
+ this.render();
485
+ }
486
+ /** کد HEX، مثل «#0b8077» */
487
+ get value() {
488
+ return this.current;
489
+ }
490
+ set value(v) {
491
+ this.valueSet = true;
492
+ const h = norm(v ?? "");
493
+ if (h)
494
+ this.setColor(h, false);
495
+ if (this.rendered)
496
+ this.paint();
497
+ }
498
+ setColor(hex, fromPanel) {
499
+ this.current = hex;
500
+ if (!fromPanel) {
501
+ const n = hexToHsv(hex);
502
+ this.hsv = n.s === 0 ? { ...n, h: this.hsv.h } : n;
503
+ }
504
+ }
505
+ swatches() {
506
+ const attr = this.getAttribute("swatches");
507
+ if (attr === null)
508
+ return SWATCHES;
509
+ return attr.split(",").map((s) => s.trim().split(/\s+/)).filter(([h]) => norm(h)).map(([h, ...n]) => [n.join(" ") || h, norm(h)]);
510
+ }
511
+ render() {
512
+ const k = this.key;
513
+ const sw = this.swatches();
514
+ const control = `<div class="k-color">${sw.length ? `<div class="k-color__swatches" role="radiogroup" aria-label="رنگ‌های پیشنهادی">${sw.map(([n, h]) => `<label class="k-color__swatch" style="--_sw: ${h}; --_ink: ${contrastRatio(h, "#ffffff") >= 3 ? "#fff" : "#000"}" title="${esc(n)}"><input type="radio" class="k-sr-only" name="${k}-sw" value="${h}" aria-label="${esc(n)}">${SW_CHECK}</label>`).join("")}</div>` : ""}<div class="k-color__row"><span class="k-color__anchor"><button type="button" class="k-color__preview" aria-label="انتخاب رنگ دلخواه" aria-haspopup="dialog" aria-expanded="false"></button></span><div class="k-input"><input id="${k}-hex" class="k-input__control" dir="ltr" spellcheck="false" autocomplete="off"></div></div>${this.getAttribute("show-contrast") === "false" ? "" : '<p class="k-color__contrast" aria-live="polite"><span class="k-color__sample">متن نمونه</span><span></span></p>'}</div>`;
515
+ this.querySelector(":scope > .k-field")?.remove();
516
+ this.insertAdjacentHTML("afterbegin", fieldHTML({ id: `${k}-hex`, label: this.getAttribute("label") ?? "رنگ", control }));
517
+ this.rendered = true;
518
+ this.querySelector(".k-color__swatches")?.addEventListener("change", (e) => this.pick(e.target.value));
519
+ const hex = this.querySelector(`#${k}-hex`);
520
+ hex.addEventListener("input", () => {
521
+ const h = norm(hex.value);
522
+ this.paintMessages(!h);
523
+ if (h)
524
+ this.pick(h, false);
525
+ });
526
+ hex.addEventListener("blur", () => {
527
+ if (norm(hex.value))
528
+ hex.value = this.current;
529
+ });
530
+ this.querySelector(".k-color__preview").addEventListener("click", () => (this.panel ? this.closePanel() : this.openPanel()));
531
+ this.paint();
532
+ }
533
+ pick(hex, syncText = true, fromPanel = false) {
534
+ const changed = hex !== this.current;
535
+ this.setColor(hex, fromPanel);
536
+ this.paint(syncText);
537
+ if (changed)
538
+ emit(this, "kashi-change", { value: hex });
539
+ }
540
+ paint(syncText = true) {
541
+ const c = this.current;
542
+ const white = contrastRatio(c, "#ffffff"), black = contrastRatio(c, "#000000");
543
+ const ink = white >= black ? "#ffffff" : "#000000";
544
+ const box = this.querySelector(".k-color");
545
+ box.style.setProperty("--_sw", c);
546
+ box.style.setProperty("--_ink", ink);
547
+ this.querySelectorAll(".k-color__swatch input").forEach((r) => (r.checked = r.value === c));
548
+ const hex = this.querySelector(`#${this.key}-hex`);
549
+ if (syncText)
550
+ hex.value = c;
551
+ const best = Math.max(white, black);
552
+ const out = this.querySelector(".k-color__contrast span:last-child");
553
+ if (out)
554
+ out.textContent = `کنتراست با متن ${ink === "#ffffff" ? "سفید" : "مشکی"}: ${toFaDigits((Math.round(best * 10) / 10).toFixed(1)).replace(".", "٫")} به ۱ ${best >= 4.5 ? "(AA)" : "(کمتر از AA)"}`;
555
+ const name = this.getAttribute("name");
556
+ this.querySelectorAll(":scope > input[type=hidden]").forEach((el) => el.remove());
557
+ if (name)
558
+ this.insertAdjacentHTML("beforeend", `<input type="hidden" name="${esc(name)}" value="${c}">`);
559
+ if (this.panel)
560
+ this.paintPanel();
561
+ this.paintMessages(false);
562
+ }
563
+ paintMessages(invalid) {
564
+ const hex = this.querySelector(`#${this.key}-hex`);
565
+ setMessages(this.querySelector(":scope > .k-field"), hex, `${this.key}-hex`, invalid ? "کد رنگ معتبر نیست؛ مثل ‎#0fa89b‎ وارد کنید." : null, this.getAttribute("hint"));
566
+ }
567
+ openPanel() {
568
+ const anchor = this.querySelector(".k-color__anchor");
569
+ const p = document.createElement("div");
570
+ p.className = "k-color__panel";
571
+ p.setAttribute("role", "dialog");
572
+ p.setAttribute("aria-label", "انتخاب رنگ دلخواه");
573
+ p.innerHTML = `<div class="k-color__area" role="slider" tabindex="0" aria-label="اشباع و روشنایی"><span class="k-color__thumb"></span></div><div class="k-color__tools"><span class="k-color__current" aria-hidden="true"></span><input type="range" class="k-color__hue" min="0" max="359" aria-label="رنگ‌مایه"></div><div class="k-color__codes" dir="ltr">${["R", "G", "B"].map((ch, i) => `<label class="k-color__code"><input class="k-color__num" inputmode="numeric" data-ch="${i}" aria-label="${["قرمز", "سبز", "آبی"][i]}"><span aria-hidden="true">${ch}</span></label>`).join("")}</div>`;
574
+ anchor.append(p);
575
+ this.panel = p;
576
+ const area = p.querySelector(".k-color__area");
577
+ const apply = (n) => {
578
+ this.hsv = n;
579
+ this.pick(hsvToHex(n), true, true);
580
+ };
581
+ const fromPointer = (e) => {
582
+ const r = area.getBoundingClientRect();
583
+ apply({ ...this.hsv, s: Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)), v: 1 - Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)) });
584
+ };
585
+ area.addEventListener("pointerdown", (e) => {
586
+ area.setPointerCapture?.(e.pointerId);
587
+ fromPointer(e);
588
+ });
589
+ area.addEventListener("pointermove", (e) => e.buttons === 1 && fromPointer(e));
590
+ area.addEventListener("keydown", (e) => {
591
+ const step = e.shiftKey ? 0.1 : 0.01;
592
+ const h = this.hsv;
593
+ const k = {
594
+ ArrowLeft: { s: Math.max(0, h.s - step) }, ArrowRight: { s: Math.min(1, h.s + step) },
595
+ ArrowUp: { v: Math.min(1, h.v + step) }, ArrowDown: { v: Math.max(0, h.v - step) },
596
+ };
597
+ if (!k[e.key])
598
+ return;
599
+ e.preventDefault();
600
+ apply({ ...h, ...k[e.key] });
601
+ });
602
+ p.querySelector(".k-color__hue").addEventListener("input", (e) => apply({ ...this.hsv, h: Number(e.target.value) }));
603
+ p.querySelectorAll(".k-color__num").forEach((el) => el.addEventListener("input", () => {
604
+ const rgb = rgbOf(this.current);
605
+ rgb[Number(el.dataset.ch)] = Math.min(255, Math.max(0, Number(el.value.replace(/\D/g, "")) || 0));
606
+ this.pick(toHex(...rgb));
607
+ }));
608
+ p.addEventListener("keydown", (e) => {
609
+ if (e.key === "Escape") {
610
+ e.stopPropagation();
611
+ this.closePanel();
612
+ }
613
+ });
614
+ this.querySelector(".k-color__preview").setAttribute("aria-expanded", "true");
615
+ document.addEventListener("pointerdown", this.onOutside);
616
+ this.paintPanel();
617
+ area.focus({ preventScroll: true });
618
+ }
619
+ paintPanel() {
620
+ const p = this.panel;
621
+ const { h, s, v } = this.hsv;
622
+ const pct = (n) => `${toFaDigits(Math.round(n * 100))}٪`;
623
+ const area = p.querySelector(".k-color__area");
624
+ area.style.setProperty("--_hue", `hsl(${h} 100% 50%)`);
625
+ area.setAttribute("aria-valuenow", String(Math.round(s * 100)));
626
+ area.setAttribute("aria-valuetext", `اشباع ${pct(s)}، روشنایی ${pct(v)}`);
627
+ const thumb = p.querySelector(".k-color__thumb");
628
+ thumb.style.left = `${s * 100}%`;
629
+ thumb.style.top = `${(1 - v) * 100}%`;
630
+ const hue = p.querySelector(".k-color__hue");
631
+ hue.value = String(Math.round(h));
632
+ hue.setAttribute("aria-valuetext", `${toFaDigits(Math.round(h))} درجه`);
633
+ const rgb = rgbOf(this.current);
634
+ p.querySelectorAll(".k-color__num").forEach((el, i) => {
635
+ if (document.activeElement !== el)
636
+ el.value = String(rgb[i]);
637
+ });
638
+ }
639
+ closePanel(focus = true) {
640
+ if (!this.panel)
641
+ return;
642
+ this.panel.remove();
643
+ this.panel = null;
644
+ document.removeEventListener("pointerdown", this.onOutside);
645
+ const btn = this.querySelector(".k-color__preview");
646
+ btn.setAttribute("aria-expanded", "false");
647
+ if (focus)
648
+ btn.focus();
649
+ }
650
+ }
651
+ KashiColorPicker.observedAttributes = ["label", "hint", "value", "name", "swatches", "show-contrast"];
652
+ define("kashi-date-range-picker", KashiDateRangePicker, "block");
653
+ define("kashi-month-picker", KashiMonthPicker, "block");
654
+ define("kashi-file-upload", KashiFileUpload, "block");
655
+ define("kashi-color-picker", KashiColorPicker, "block");