@kashi-ui/elements 0.26.0 → 1.0.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 (72) hide show
  1. package/README.md +9 -0
  2. package/dist/appointment.js +13 -12
  3. package/dist/base.d.ts +408 -0
  4. package/dist/base.js +29 -0
  5. package/dist/basic.js +8 -9
  6. package/dist/calendar.js +21 -20
  7. package/dist/cdn/appointment-CrIZi8Bp.js +1 -0
  8. package/dist/cdn/base-DTQ50VdG.js +1 -0
  9. package/dist/cdn/basic-CrEW2RKF.js +1 -0
  10. package/dist/cdn/calendar-CYRrdGIf.js +16 -0
  11. package/dist/cdn/charts-D6KM26Vp.js +1 -0
  12. package/dist/cdn/combobox-6EFI2qqN.js +1 -0
  13. package/dist/cdn/controls-BwebEIlf.js +1 -0
  14. package/dist/cdn/display-DFuMkcj5.js +1 -0
  15. package/dist/cdn/fields-DhLIhHQ8.js +1 -0
  16. package/dist/cdn/forms-BHivKDdC.js +1 -0
  17. package/dist/cdn/holidays-DgbIAdlt.js +1 -0
  18. package/dist/cdn/inputs-R7nEHfbV.js +1 -0
  19. package/dist/cdn/kashi-elements.js +1 -1
  20. package/dist/cdn/map-H3xiZd8Y.js +12 -0
  21. package/dist/cdn/{menu-Cga7QOfs.js → menu-BAUSubXk.js} +1 -1
  22. package/dist/cdn/nav-NK-v48B5.js +1 -0
  23. package/dist/cdn/overlay-CBv8grz3.js +1 -0
  24. package/dist/cdn/pickers-BHzNt_X8.js +1 -0
  25. package/dist/cdn/table-Bt9WVLoo.js +1 -0
  26. package/dist/cdn/text-D4a_hlwL.js +1 -0
  27. package/dist/cdn/time-DJ3p1wvd.js +1 -0
  28. package/dist/cdn/words-BdADG_9U.js +1 -0
  29. package/dist/cdn.d.ts +1 -0
  30. package/dist/cdn.js +2 -0
  31. package/dist/charts.js +27 -24
  32. package/dist/combobox.d.ts +6 -5
  33. package/dist/combobox.js +29 -20
  34. package/dist/controls.js +15 -15
  35. package/dist/display.js +17 -14
  36. package/dist/fields.js +3 -3
  37. package/dist/forms.d.ts +3 -3
  38. package/dist/forms.js +54 -47
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +3 -0
  41. package/dist/inputs.d.ts +3 -3
  42. package/dist/inputs.js +31 -30
  43. package/dist/map.js +16 -13
  44. package/dist/nav.js +18 -16
  45. package/dist/overlay.js +5 -5
  46. package/dist/pickers.d.ts +1 -1
  47. package/dist/pickers.js +39 -38
  48. package/dist/table.js +7 -7
  49. package/dist/toast.js +3 -3
  50. package/package.json +4 -4
  51. package/dist/cdn/appointment-DkMxtSWi.js +0 -1
  52. package/dist/cdn/base-DfzNTdQk.js +0 -1
  53. package/dist/cdn/basic-B5n1okZO.js +0 -1
  54. package/dist/cdn/calendar-oMlnK1_D.js +0 -16
  55. package/dist/cdn/charts-SHKq2S4I.js +0 -1
  56. package/dist/cdn/combobox-DiGy7lAB.js +0 -1
  57. package/dist/cdn/controls-C6zwNYWs.js +0 -1
  58. package/dist/cdn/digits-DE1YGT94.js +0 -1
  59. package/dist/cdn/display-DZFW9EEX.js +0 -1
  60. package/dist/cdn/fields-D2hmrvVR.js +0 -1
  61. package/dist/cdn/forms-CeItYHFx.js +0 -1
  62. package/dist/cdn/holidays-BwsAEOI1.js +0 -1
  63. package/dist/cdn/inputs-DuP7P3ka.js +0 -1
  64. package/dist/cdn/jalali-BiM5Kfep.js +0 -1
  65. package/dist/cdn/map-w5X2fTQI.js +0 -12
  66. package/dist/cdn/nav-CwPZ20z-.js +0 -1
  67. package/dist/cdn/overlay-BDzwsBN5.js +0 -1
  68. package/dist/cdn/pickers-BOpaZ-7q.js +0 -1
  69. package/dist/cdn/table-BdF43L5q.js +0 -1
  70. package/dist/cdn/text-D1U32D6K.js +0 -1
  71. package/dist/cdn/time-DXw4vOne.js +0 -1
  72. package/dist/cdn/words-9OdzG7y6.js +0 -1
package/dist/map.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { EMPTY_ADDRESS, PROVINCES, formatNumber, getCities } from "@kashi-ui/persian";
2
- import { HTMLBase, afterParse, bool, define, emit, esc, uid } from "./base.js";
2
+ import { afterParse, bool, define, digitsOf, emit, esc, HTMLBase, msg, uid } from "./base.js";
3
3
  import { IRAN_PROVINCES, IRAN_VIEWBOX } from "./iranMapData.js";
4
4
  // فرم نشانی از این عنصرها ساخته شده؛ با import همین فایل به‌تنهایی هم ثبت شوند
5
5
  import "./combobox.js";
@@ -83,7 +83,7 @@ export class KashiIranMap extends HTMLBase {
83
83
  return this.multiple || bool(this, "selectable") || this.hasAttribute("value") || this.hasAttribute("name");
84
84
  }
85
85
  fmt(n) {
86
- return formatNumber(n);
86
+ return formatNumber(n, { digits: digitsOf(this) });
87
87
  }
88
88
  level(n) {
89
89
  const data = this.values;
@@ -115,7 +115,7 @@ export class KashiIranMap extends HTMLBase {
115
115
  return typeof v === "number" ? `${n}: ${this.fmt(v)}${unit ? ` ${unit}` : ""}` : n;
116
116
  }
117
117
  render() {
118
- const label = this.getAttribute("label") ?? "نقشهٔ استان‌های ایران";
118
+ const label = this.getAttribute("label") ?? msg().iranMap.label;
119
119
  const sel = this.selectable;
120
120
  const multi = this.multiple;
121
121
  const data = this.values;
@@ -133,7 +133,7 @@ export class KashiIranMap extends HTMLBase {
133
133
  ? `<table class="k-sr-only"><caption>${esc(label)}</caption><tbody>${NAMES.filter((n) => typeof data[n] === "number").sort((a, b) => data[b] - data[a]).map((n) => `<tr><th scope="row">${esc(n)}</th><td>${this.fmt(data[n])}${unit ? ` ${esc(unit)}` : ""}</td></tr>`).join("")}</tbody></table>`
134
134
  : "";
135
135
  // نسبت ابعاد با aspect-ratio، نه padding درصدی: در چیدمان flex ستونی ارتفاع padding درصدی صفر حساب می‌شود
136
- this.innerHTML = `<figure class="k-iran-map${sel ? " k-iran-map--selectable" : ""}"><div class="k-iran-map__canvas" style="block-size: auto; aspect-ratio: ${VW} / ${VH}"><svg viewBox="${IRAN_VIEWBOX}" class="k-iran-map__svg" role="${sel ? (multi ? "group" : "radiogroup") : "img"}" aria-label="${esc(sel ? label : `${label}${data ? "؛ جدول داده در ادامه" : ""}`)}">${paths}</svg><div class="k-chart__tip" aria-hidden="true" style="display: none"></div></div>${legend}${table}</figure>`;
136
+ this.innerHTML = `<figure class="k-iran-map${sel ? " k-iran-map--selectable" : ""}"><div class="k-iran-map__canvas" style="block-size: auto; aspect-ratio: ${VW} / ${VH}"><svg viewBox="${IRAN_VIEWBOX}" class="k-iran-map__svg" role="${sel ? (multi ? "group" : "radiogroup") : "img"}" aria-label="${esc(sel ? label : `${label}${data ? msg().iranMap.tableNote : ""}`)}">${paths}</svg><div class="k-chart__tip" aria-hidden="true" style="display: none"></div></div>${legend}${table}</figure>`;
137
137
  const svg = this.querySelector("svg");
138
138
  const fig = this.querySelector("figure");
139
139
  svg.addEventListener("pointerover", (e) => {
@@ -235,7 +235,7 @@ export class KashiIranMap extends HTMLBase {
235
235
  const w = this.querySelector("figure").clientWidth || VW;
236
236
  const v = this.values?.[n];
237
237
  const unit = this.getAttribute("unit");
238
- const row = typeof v === "number" ? `${this.fmt(v)}${unit ? ` ${unit}` : ""}` : `مرکز: ${CAPITAL[n]}`;
238
+ const row = typeof v === "number" ? `${this.fmt(v)}${unit ? ` ${unit}` : ""}` : msg().province.capital(CAPITAL[n]);
239
239
  box.innerHTML = `<span class="k-chart__tip-title">${esc(n)}</span><span class="k-chart__tip-row"><strong>${esc(row)}</strong></span>`;
240
240
  // داخل ظرف بماند و راهنمای استان‌های شمالی بالای نقشه بیرون نزند
241
241
  box.style.left = `${Math.min(Math.max((p.x / VW) * w, 80), w - 80)}px`;
@@ -244,7 +244,10 @@ export class KashiIranMap extends HTMLBase {
244
244
  }
245
245
  }
246
246
  KashiIranMap.observedAttributes = ["data", "value", "label", "unit", "steps", "scale", "legend", "selectable", "multiple", "name"];
247
- const LABELS = { province: "استان", city: "شهر", street: "نشانی", plaque: "پلاک", unit: "واحد", postalCode: "کد پستی" };
247
+ const LABELS = () => {
248
+ const A = msg().address;
249
+ return { province: A.province, city: A.city, street: A.street, plaque: A.plaque, unit: A.unit, postalCode: A.postalCode };
250
+ };
248
251
  /**
249
252
  * فرم نشانی ایرانی: استان، شهرِ همان استان، نشانی، پلاک، واحد و کد پستی ده‌رقمی؛ با map نقشهٔ ایران برای انتخاب استان.
250
253
  * با انتخاب شهر بدون استان، استان خودکار پر می‌شود و با عوض شدن استان، شهرِ نامربوط پاک می‌شود.
@@ -291,7 +294,7 @@ export class KashiAddressForm extends HTMLBase {
291
294
  }
292
295
  /** خطای هر فیلد، مثلاً از اعتبارسنجی سرور: { plaque: "پلاک را بنویسید." } */
293
296
  set errors(e) {
294
- for (const f of Object.keys(LABELS)) {
297
+ for (const f of Object.keys(LABELS())) {
295
298
  const el = this.querySelector(`[data-field="${f}"]`);
296
299
  if (!el)
297
300
  continue;
@@ -306,7 +309,7 @@ export class KashiAddressForm extends HTMLBase {
306
309
  }
307
310
  }
308
311
  label(f) {
309
- return this.getAttribute(`label-${f === "postalCode" ? "postal-code" : f}`) ?? LABELS[f];
312
+ return this.getAttribute(`label-${f === "postalCode" ? "postal-code" : f}`) ?? LABELS()[f];
310
313
  }
311
314
  render() {
312
315
  const a = this.address;
@@ -317,18 +320,18 @@ export class KashiAddressForm extends HTMLBase {
317
320
  const map = bool(this, "map");
318
321
  const k = this.key;
319
322
  const reqMark = req ? '<span class="k-label__required" aria-hidden="true">*</span>' : "";
320
- const legend = this.getAttribute("legend") ?? "نشانی";
323
+ const legend = this.getAttribute("legend") ?? msg().address.legend;
321
324
  this.innerHTML = `<fieldset class="k-address${map ? " k-address--map" : ""}"${this.hasAttribute("disabled") ? " disabled" : ""}>
322
325
  ${legend ? `<legend class="k-address__legend">${esc(legend)}</legend>` : ""}
323
326
  <div class="k-address__fields">
324
- <kashi-province-select class="k-address__province" data-field="province" label="${esc(this.label("province"))}" show-capital="false" placeholder="جستجوی استان"${n("province")}${r}></kashi-province-select>
327
+ <kashi-province-select class="k-address__province" data-field="province" label="${esc(this.label("province"))}" show-capital="false" placeholder="${esc(msg().address.provinceSearch)}"${n("province")}${r}></kashi-province-select>
325
328
  <kashi-city-select class="k-address__city" data-field="city" label="${esc(this.label("city"))}"${n("city")}${r}></kashi-city-select>
326
- <div class="k-field k-address__street" data-field="street"><div class="k-field__label-row"><label class="k-label" for="${k}-street">${esc(this.label("street"))}${reqMark}</label></div><div class="k-input k-input--textarea k-input--autoresize"><textarea id="${k}-street" class="k-input__control" rows="2" autocomplete="street-address" placeholder="خیابان، کوچه، نام ساختمان"${n("street")}${r}></textarea></div></div>
329
+ <div class="k-field k-address__street" data-field="street"><div class="k-field__label-row"><label class="k-label" for="${k}-street">${esc(this.label("street"))}${reqMark}</label></div><div class="k-input k-input--textarea k-input--autoresize"><textarea id="${k}-street" class="k-input__control" rows="2" autocomplete="street-address" placeholder="${esc(msg().address.streetHint)}"${n("street")}${r}></textarea></div></div>
327
330
  <div class="k-field k-address__plaque" data-field="plaque"><div class="k-field__label-row"><label class="k-label" for="${k}-plaque">${esc(this.label("plaque"))}${reqMark}</label></div><div class="k-input"><input id="${k}-plaque" class="k-input__control" inputmode="numeric" autocomplete="address-line2"${n("plaque")}${r}></div></div>
328
- <div class="k-field k-address__unit" data-field="unit"><div class="k-field__label-row"><label class="k-label" for="${k}-unit">${esc(this.label("unit"))}${req ? '<span class="k-label__optional">اختیاری</span>' : ""}</label></div><div class="k-input"><input id="${k}-unit" class="k-input__control" inputmode="numeric" autocomplete="address-line3"${n("unit")}></div></div>
331
+ <div class="k-field k-address__unit" data-field="unit"><div class="k-field__label-row"><label class="k-label" for="${k}-unit">${esc(this.label("unit"))}${req ? `<span class="k-label__optional">${esc(msg().common.optional)}</span>` : ""}</label></div><div class="k-input"><input id="${k}-unit" class="k-input__control" inputmode="numeric" autocomplete="address-line3"${n("unit")}></div></div>
329
332
  <kashi-postal-code class="k-address__postal" data-field="postalCode" label="${esc(this.label("postalCode"))}"${n("postalCode")}${r}></kashi-postal-code>
330
333
  </div>
331
- ${map ? `<kashi-iran-map class="k-address__map" selectable label="${esc(`${this.label("province")} روی نقشه`)}"></kashi-iran-map>` : ""}
334
+ ${map ? `<kashi-iran-map class="k-address__map" selectable label="${esc(msg().iranMap.onMap(this.label("province")))}"></kashi-iran-map>` : ""}
332
335
  </fieldset>`;
333
336
  this.built = true;
334
337
  const province = this.querySelector("kashi-province-select");
package/dist/nav.js CHANGED
@@ -1,5 +1,5 @@
1
- import { normalizePersian, toFaDigits } from "@kashi-ui/persian";
2
- import { HTMLBase, afterParse, bool, define, emit, esc, uid } from "./base.js";
1
+ import { normalizePersian } from "@kashi-ui/persian";
2
+ import { afterParse, bool, define, emit, esc, HTMLBase, msg, num, uid } from "./base.js";
3
3
  const search = (s) => normalizePersian(s, { digits: "latin" }).toLowerCase();
4
4
  const items = (menu) => [...menu.querySelectorAll('[role^="menuitem"]:not([aria-disabled="true"])')];
5
5
  const SEARCH = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true"><circle cx="9" cy="9" r="5.5"/><path d="m13.5 13.5 3 3"/></svg>';
@@ -68,9 +68,10 @@ export class KashiCommandPalette extends HTMLBase {
68
68
  build() {
69
69
  const d = document.createElement("dialog");
70
70
  d.className = "k-dialog k-command";
71
- const title = this.getAttribute("label") ?? "پالت فرمان";
71
+ const M = msg();
72
+ const title = this.getAttribute("label") ?? M.command.title;
72
73
  d.setAttribute("aria-label", title);
73
- d.innerHTML = `<div class="k-input"><span class="k-input__addon">${SEARCH}</span><input class="k-input__control" type="text" role="combobox" aria-expanded="true" aria-controls="${this.key}-list" aria-autocomplete="list" aria-label="${esc(title)}" placeholder="${esc(this.getAttribute("placeholder") ?? "جستجوی دستور یا صفحه…")}" autocomplete="off"></div><ul id="${this.key}-list" role="listbox" aria-label="${esc(title)}" class="k-listbox k-listbox--inline k-command__list"></ul><div class="k-command__footer" aria-hidden="true"><span><kbd class="k-kbd">↑</kbd><kbd class="k-kbd">↓</kbd>حرکت</span><span><kbd class="k-kbd">Enter</kbd>انتخاب</span><span><kbd class="k-kbd">Esc</kbd>بستن</span></div><span class="k-sr-only" aria-live="polite"></span>`;
74
+ d.innerHTML = `<div class="k-input"><span class="k-input__addon">${SEARCH}</span><input class="k-input__control" type="text" role="combobox" aria-expanded="true" aria-controls="${this.key}-list" aria-autocomplete="list" aria-label="${esc(title)}" placeholder="${esc(this.getAttribute("placeholder") ?? M.command.placeholder)}" autocomplete="off"></div><ul id="${this.key}-list" role="listbox" aria-label="${esc(title)}" class="k-listbox k-listbox--inline k-command__list"></ul><div class="k-command__footer" aria-hidden="true"><span><kbd class="k-kbd">↑</kbd><kbd class="k-kbd">↓</kbd>${esc(M.command.navigate)}</span><span><kbd class="k-kbd">Enter</kbd>${esc(M.command.select)}</span><span><kbd class="k-kbd">Esc</kbd>${esc(M.common.close)}</span></div><span class="k-sr-only" aria-live="polite"></span>`;
74
75
  this.append(d);
75
76
  this.dialog = d;
76
77
  const input = d.querySelector("input");
@@ -152,14 +153,14 @@ export class KashiCommandPalette extends HTMLBase {
152
153
  last = it.group;
153
154
  return `${header}<li id="${this.key}-list-${i}" data-i="${i}" role="option" aria-selected="${i === this.active}"${i === this.active ? " data-active" : ""} class="k-option"><span class="k-option__body"><span>${mark(it.label)}</span>${it.description ? `<span class="k-option__desc">${esc(it.description)}</span>` : ""}</span>${it.shortcut ? `<span class="k-menu__shortcut" aria-hidden="true">${esc(it.shortcut)}</span>` : ""}</li>`;
154
155
  }).join("")
155
- : '<li role="presentation" class="k-listbox__empty">دستوری پیدا نشد.</li>';
156
+ : `<li role="presentation" class="k-listbox__empty">${esc(msg().command.empty)}</li>`;
156
157
  const input = d.querySelector("input");
157
158
  if (list.length)
158
159
  input.setAttribute("aria-activedescendant", `${this.key}-list-${this.active}`);
159
160
  else
160
161
  input.removeAttribute("aria-activedescendant");
161
162
  d.querySelector(`#${this.key}-list-${this.active}`)?.scrollIntoView?.({ block: "nearest" });
162
- d.querySelector(":scope > .k-sr-only").textContent = this.query ? `${toFaDigits(list.length)} نتیجه` : "";
163
+ d.querySelector(":scope > .k-sr-only").textContent = this.query ? msg().common.results(num(this, list.length)) : "";
163
164
  }
164
165
  }
165
166
  KashiCommandPalette.observedAttributes = ["placeholder", "label", "shortcut"];
@@ -192,7 +193,7 @@ export class KashiContextMenu extends HTMLBase {
192
193
  this.setAttribute("aria-describedby", `${this.key}-hint`);
193
194
  const menu = document.createElement("ul");
194
195
  menu.setAttribute("role", "menu");
195
- menu.setAttribute("aria-label", this.getAttribute("label") ?? "گزینه‌ها");
196
+ menu.setAttribute("aria-label", this.getAttribute("label") ?? msg().contextMenu.label);
196
197
  menu.className = "k-menu k-menu--context";
197
198
  menu.hidden = true;
198
199
  for (const el of [...(source?.children ?? [])]) {
@@ -221,7 +222,7 @@ export class KashiContextMenu extends HTMLBase {
221
222
  menu.append(li);
222
223
  }
223
224
  source?.remove();
224
- this.insertAdjacentHTML("beforeend", `<span id="${this.key}-hint" class="k-sr-only">برای گزینه‌ها راست‌کلیک کنید یا Shift+F10 را بزنید.</span>`);
225
+ this.insertAdjacentHTML("beforeend", `<span id="${this.key}-hint" class="k-sr-only">${esc(msg().contextMenu.hint)}</span>`);
225
226
  this.append(menu);
226
227
  this.menu = menu;
227
228
  this.addEventListener("contextmenu", (e) => {
@@ -333,11 +334,12 @@ export class KashiCarousel extends HTMLBase {
333
334
  return Math.max(1, this.slides.length - this.perView + 1);
334
335
  }
335
336
  build() {
336
- const label = this.getAttribute("label") ?? "اسلایدر";
337
+ const M = msg();
338
+ const label = this.getAttribute("label") ?? M.carousel.slider;
337
339
  const children = [...this.children];
338
340
  const section = document.createElement("section");
339
341
  section.className = "k-carousel";
340
- section.setAttribute("aria-roledescription", "اسلایدر");
342
+ section.setAttribute("aria-roledescription", M.carousel.slider);
341
343
  section.setAttribute("aria-label", label);
342
344
  section.style.setProperty("--k-carousel-per-view", String(this.perView));
343
345
  const track = document.createElement("div");
@@ -348,8 +350,8 @@ export class KashiCarousel extends HTMLBase {
348
350
  const s = document.createElement("div");
349
351
  s.className = "k-carousel__slide";
350
352
  s.setAttribute("role", "group");
351
- s.setAttribute("aria-roledescription", "اسلاید");
352
- s.setAttribute("aria-label", `${toFaDigits(i + 1)} از ${toFaDigits(children.length)}`);
353
+ s.setAttribute("aria-roledescription", M.carousel.slide);
354
+ s.setAttribute("aria-label", M.carousel.slideOf(num(this, i + 1), num(this, children.length)));
353
355
  s.id = `${this.key}-s${i}`;
354
356
  s.append(c);
355
357
  track.append(s);
@@ -359,7 +361,7 @@ export class KashiCarousel extends HTMLBase {
359
361
  this.track = track;
360
362
  if (this.pages > 1) {
361
363
  const dots = this.getAttribute("dots") !== "false";
362
- section.insertAdjacentHTML("beforeend", `<div class="k-carousel__bar">${dots ? `<ul class="k-carousel__dots" aria-label="اسلایدهای ${esc(label)}">${Array.from({ length: this.pages }, (_, i) => `<li><button type="button" class="k-carousel__dot" data-go="${i}" aria-label="${toFaDigits(i + 1)} از ${toFaDigits(this.pages)}" aria-controls="${this.key}-s${i}"></button></li>`).join("")}</ul>` : "<span></span>"}<div class="k-carousel__nav"><button type="button" class="k-btn k-btn--secondary k-btn--icon k-btn--sm" data-step="-1" aria-label="قبلی">${ARROW(false)}</button><button type="button" class="k-btn k-btn--secondary k-btn--icon k-btn--sm" data-step="1" aria-label="بعدی">${ARROW(true)}</button></div></div>`);
364
+ section.insertAdjacentHTML("beforeend", `<div class="k-carousel__bar">${dots ? `<ul class="k-carousel__dots" aria-label="${esc(M.carousel.slides(label))}">${Array.from({ length: this.pages }, (_, i) => `<li><button type="button" class="k-carousel__dot" data-go="${i}" aria-label="${esc(M.carousel.slideOf(num(this, i + 1), num(this, this.pages)))}" aria-controls="${this.key}-s${i}"></button></li>`).join("")}</ul>` : "<span></span>"}<div class="k-carousel__nav"><button type="button" class="k-btn k-btn--secondary k-btn--icon k-btn--sm" data-step="-1" aria-label="${esc(M.common.prev)}">${ARROW(false)}</button><button type="button" class="k-btn k-btn--secondary k-btn--icon k-btn--sm" data-step="1" aria-label="${esc(M.common.next)}">${ARROW(true)}</button></div></div>`);
363
365
  section.querySelector(".k-carousel__bar").addEventListener("click", (e) => {
364
366
  const b = e.target.closest("button");
365
367
  if (!b)
@@ -701,7 +703,7 @@ export class KashiTour extends HTMLBase {
701
703
  });
702
704
  document.body.append(this.card);
703
705
  }
704
- this.card.innerHTML = `<span class="k-tour__step">مرحلهٔ ${toFaDigits(this.i + 1)} از ${toFaDigits(n)}</span><p class="k-tour__title" id="${this.key}-t">${esc(s.title)}</p><p class="k-tour__body" id="${this.key}-b">${esc(s.content)}</p><div class="k-tour__actions"><button type="button" class="k-btn k-btn--link k-tour__skip" data-act="skip">رد کردن</button><span class="k-tour__progress" aria-hidden="true">${this.steps.map((_, k) => `<span${k <= this.i ? ' data-on=""' : ""}></span>`).join("")}</span>${this.i > 0 ? '<button type="button" class="k-btn k-btn--secondary k-btn--sm" data-act="prev">قبلی</button>' : ""}<button type="button" class="k-btn k-btn--primary k-btn--sm" data-act="${last ? "done" : "next"}">${last ? "تمام" : "بعدی"}</button></div>`;
706
+ this.card.innerHTML = `<span class="k-tour__step">${esc(msg().tour.step(num(this, this.i + 1), num(this, n)))}</span><p class="k-tour__title" id="${this.key}-t">${esc(s.title)}</p><p class="k-tour__body" id="${this.key}-b">${esc(s.content)}</p><div class="k-tour__actions"><button type="button" class="k-btn k-btn--link k-tour__skip" data-act="skip">${esc(msg().tour.skip)}</button><span class="k-tour__progress" aria-hidden="true">${this.steps.map((_, k) => `<span${k <= this.i ? ' data-on=""' : ""}></span>`).join("")}</span>${this.i > 0 ? '<button type="button" class="k-btn k-btn--secondary k-btn--sm" data-act="prev">${esc(msg().common.prev)}</button>' : ""}<button type="button" class="k-btn k-btn--primary k-btn--sm" data-act="${last ? "done" : "next"}">${esc(last ? msg().tour.done : msg().common.next)}</button></div>`;
705
707
  const el = document.querySelector(s.target);
706
708
  const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
707
709
  el?.scrollIntoView?.({ block: "center", behavior: reduce ? "auto" : "smooth" });
@@ -819,7 +821,7 @@ export class KashiSplitter extends HTMLBase {
819
821
  handle.setAttribute("aria-orientation", this.vertical ? "horizontal" : "vertical");
820
822
  handle.setAttribute("aria-valuemin", String(this.num("min", 15)));
821
823
  handle.setAttribute("aria-valuemax", String(this.num("max", 85)));
822
- handle.setAttribute("aria-label", this.getAttribute("label") ?? "تغییر اندازهٔ پنل‌ها");
824
+ handle.setAttribute("aria-label", this.getAttribute("label") ?? msg().splitter.label);
823
825
  this.paint();
824
826
  }
825
827
  set(p, fire = true) {
@@ -838,7 +840,7 @@ export class KashiSplitter extends HTMLBase {
838
840
  pane.style.blockSize = this.vertical ? `${this.current}%` : "";
839
841
  const handle = box.querySelector(".k-splitter__handle");
840
842
  handle.setAttribute("aria-valuenow", String(this.current));
841
- handle.setAttribute("aria-valuetext", `${toFaDigits(Math.round(this.current))}٪`);
843
+ handle.setAttribute("aria-valuetext", msg().common.percent(num(this, Math.round(this.current))));
842
844
  }
843
845
  }
844
846
  KashiSplitter.observedAttributes = ["size", "min", "max", "orientation", "label", "height"];
package/dist/overlay.js CHANGED
@@ -1,4 +1,4 @@
1
- import { HTMLBase, ICONS, afterParse, bool, define, emit, esc, uid } from "./base.js";
1
+ import { afterParse, bool, define, emit, esc, HTMLBase, ICONS, msg, uid } from "./base.js";
2
2
  /**
3
3
  * پنجرهٔ گفتگو روی <dialog> بومی با showModal: فوکوس داخل پنجره می‌ماند، Esc می‌بندد،
4
4
  * بقیهٔ صفحه از دسترس صفحه‌خوان خارج می‌شود و پس از بستن، فوکوس به دکمهٔ بازکننده برمی‌گردد.
@@ -100,7 +100,7 @@ export class KashiDialog extends HTMLBase {
100
100
  d.setAttribute("aria-describedby", desc.id);
101
101
  else
102
102
  d.removeAttribute("aria-describedby");
103
- d.querySelector(".k-dialog__close").setAttribute("aria-label", this.getAttribute("close-label") ?? "بستن");
103
+ d.querySelector(".k-dialog__close").setAttribute("aria-label", this.getAttribute("close-label") ?? msg().common.close);
104
104
  }
105
105
  sync() {
106
106
  const d = this.dialog;
@@ -387,9 +387,9 @@ export class KashiCopyButton extends HTMLBase {
387
387
  }, Number(this.getAttribute("timeout")) || 2000);
388
388
  }
389
389
  render() {
390
- const copy = this.getAttribute("label") ?? "کپی";
391
- const copied = this.getAttribute("copied-label") ?? "کپی شد";
392
- const text = this.state === "copied" ? copied : this.state === "failed" ? "کپی نشد" : copy;
390
+ const copy = this.getAttribute("label") ?? msg().copy.copy;
391
+ const copied = this.getAttribute("copied-label") ?? msg().copy.copied;
392
+ const text = this.state === "copied" ? copied : this.state === "failed" ? msg().copy.failed : copy;
393
393
  const iconOnly = bool(this, "icon-only");
394
394
  const icon = this.state === "copied" ? ICONS.check : ICONS.copy;
395
395
  const variant = this.getAttribute("variant") ?? "secondary";
package/dist/pickers.d.ts CHANGED
@@ -48,7 +48,7 @@ export declare class KashiMonthPicker extends HTMLBase {
48
48
  private out;
49
49
  private render;
50
50
  }
51
- /** حجم فایل به فارسی: «۱٫۲ مگابایت» */
51
+ /** حجم فایل به زبان فعلی: «۱٫۲ مگابایت» یا «1.2 MB» */
52
52
  export declare function formatBytes(bytes: number): string;
53
53
  /**
54
54
  * بارگذاری فایل با کشیدن و رها کردن، بررسی نوع و حجم و فهرست فایل‌ها.
package/dist/pickers.js CHANGED
@@ -1,17 +1,17 @@
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";
1
+ import { formatJalali, fromISODate, parseJalali, toFaDigits, toGregorian, toISODate, toJalali } from "@kashi-ui/persian";
2
+ import { bool, calendarNames, define, digitsOf, emit, esc, fieldHTML, HTMLBase, ICONS, lang, localeDigits, msg, num, setMessages, uid } from "./base.js";
3
3
  import "./calendar.js";
4
4
  const parseDate = (t) => (t ? (fromISODate(t.trim()) ?? parseJalali(t)) : null);
5
5
  const day = (d, add = 0) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + add);
6
6
  const same = (a, b) => (!a && !b) || (!!a && !!b && a.getTime() === b.getTime());
7
7
  const PRESETS = [
8
- ["۷ روز اخیر", (t) => ({ start: day(t, -6), end: t })],
9
- ["۳۰ روز اخیر", (t) => ({ start: day(t, -29), end: t })],
10
- ["این ماه", (t) => {
8
+ [(m, n) => m.last7(n(7)), (t) => ({ start: day(t, -6), end: t })],
9
+ [(m, n) => m.last30(n(30)), (t) => ({ start: day(t, -29), end: t })],
10
+ [(m) => m.thisMonth, (t) => {
11
11
  const { jy, jm } = toJalali(t);
12
12
  return { start: toGregorian(jy, jm, 1), end: t };
13
13
  }],
14
- ["ماه قبل", (t) => {
14
+ [(m) => m.lastMonth, (t) => {
15
15
  const { jy, jm } = toJalali(t);
16
16
  const [py, pm] = jm === 1 ? [jy - 1, 12] : [jy, jm - 1];
17
17
  return { start: toGregorian(py, pm, 1), end: day(toGregorian(jy, jm, 1), -1) };
@@ -61,17 +61,17 @@ export class KashiDateRangePicker extends HTMLBase {
61
61
  this.render();
62
62
  }
63
63
  fmt(d) {
64
- return formatJalali(d, { digits: this.getAttribute("digits") === "latin" ? "latin" : "fa" });
64
+ return formatJalali(d, { digits: digitsOf(this), locale: lang() });
65
65
  }
66
66
  text(r) {
67
- return r.start && r.end ? `${this.fmt(r.start)} تا ${this.fmt(r.end)}` : null;
67
+ return r.start && r.end ? `${this.fmt(r.start)}${msg().common.range}${this.fmt(r.end)}` : null;
68
68
  }
69
69
  render() {
70
70
  this.close(false);
71
71
  const id = this.key;
72
- const label = this.getAttribute("label") ?? "بازهٔ تاریخ";
72
+ const label = this.getAttribute("label") ?? msg().dateRange.label;
73
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>`;
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") ?? msg().dateRange.placeholder)}</span></button></div></div>`;
75
75
  this.querySelector(":scope > .k-field")?.remove();
76
76
  this.insertAdjacentHTML("afterbegin", fieldHTML({ id, label, control }));
77
77
  this.trigger = this.querySelector(".k-daterange__trigger");
@@ -94,9 +94,9 @@ export class KashiDateRangePicker extends HTMLBase {
94
94
  pop.id = `${this.key}-popover`;
95
95
  pop.className = "k-datepicker__popover k-daterange__popover";
96
96
  pop.setAttribute("role", "dialog");
97
- pop.setAttribute("aria-label", this.getAttribute("label") ?? "انتخاب بازهٔ تاریخ");
97
+ pop.setAttribute("aria-label", this.getAttribute("label") ?? msg().dateRange.placeholder);
98
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>`;
99
+ pop.innerHTML = `${presets.length ? `<ul class="k-daterange__presets" role="list" aria-label="${esc(msg().dateRange.presets)}">${presets.map(([l], i) => `<li><button type="button" class="k-daterange__preset" data-preset="${i}" aria-pressed="false">${esc(l(msg().dateRange, (v) => num(this, v)))}</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>${esc(msg().common.cancel)}</button><button type="button" class="k-btn k-btn--primary k-btn--sm" data-apply>${esc(msg().common.confirm)}</button></div></div>`;
100
100
  const cal = document.createElement("kashi-calendar");
101
101
  for (const a of ["min", "max", "digits", "holidays"]) {
102
102
  const v = this.getAttribute(a);
@@ -150,7 +150,7 @@ export class KashiDateRangePicker extends HTMLBase {
150
150
  paintDraft() {
151
151
  const pop = this.pop;
152
152
  const r = this.draft;
153
- pop.querySelector(".k-daterange__summary").textContent = this.text(r) ?? (r.start ? "حالا تاریخ پایان را انتخاب کنید." : "");
153
+ pop.querySelector(".k-daterange__summary").textContent = this.text(r) ?? (r.start ? msg().dateRange.pickEnd : "");
154
154
  pop.querySelector("[data-apply]").disabled = !r.start || !r.end;
155
155
  const today = day(new Date());
156
156
  pop.querySelectorAll("[data-preset]").forEach((b, i) => {
@@ -222,11 +222,11 @@ export class KashiMonthPicker extends HTMLBase {
222
222
  return (!!min && k < min.year * 12 + min.month) || (!!max && k > max.year * 12 + max.month);
223
223
  }
224
224
  render(focus = false) {
225
- const f = (n) => (this.getAttribute("digits") === "latin" ? String(n) : toFaDigits(String(n)));
225
+ const f = (n) => (digitsOf(this) === "latin" ? String(n) : toFaDigits(String(n)));
226
226
  const t = toJalali(new Date());
227
227
  const min = this.parse(this.getAttribute("min")), max = this.parse(this.getAttribute("max"));
228
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>`;
229
+ this.innerHTML = `<div class="k-monthpicker" role="group" aria-label="${esc(this.getAttribute("label") ?? msg().monthPicker.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="${esc(msg().monthPicker.prevYear)}"${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="${esc(msg().monthPicker.nextYear)}"${max && y >= max.year ? " disabled" : ""}>${ARROW(true)}</button></div><div class="k-monthpicker__grid">${calendarNames().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
230
  const name = this.getAttribute("name");
231
231
  if (name)
232
232
  this.insertAdjacentHTML("beforeend", `<input type="hidden" name="${esc(name)}" value="${this.value}">`);
@@ -267,16 +267,16 @@ export class KashiMonthPicker extends HTMLBase {
267
267
  }
268
268
  }
269
269
  KashiMonthPicker.observedAttributes = ["value", "min", "max", "digits", "label", "name"];
270
- /** حجم فایل به فارسی: «۱٫۲ مگابایت» */
270
+ /** حجم فایل به زبان فعلی: «۱٫۲ مگابایت» یا «1.2 MB» */
271
271
  export function formatBytes(bytes) {
272
- const units = ["بایت", "کیلوبایت", "مگابایت", "گیگابایت"];
272
+ const units = msg().fileUpload.units;
273
273
  let v = bytes, i = 0;
274
274
  while (v >= 1024 && i < units.length - 1) {
275
275
  v /= 1024;
276
276
  i++;
277
277
  }
278
278
  const n = i === 0 ? String(v) : v.toFixed(v < 10 ? 1 : 0).replace(/\.0$/, "");
279
- return `${toFaDigits(n).replace(".", "٫")} ${units[i]}`;
279
+ return `${localeDigits() === "fa" ? toFaDigits(n).replace(".", "٫") : n} ${units[i]}`;
280
280
  }
281
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
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>';
@@ -312,7 +312,7 @@ export class KashiFileUpload extends HTMLBase {
312
312
  const off = this.hasAttribute("disabled");
313
313
  const name = this.getAttribute("name");
314
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>`;
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") ?? msg().fileUpload.browse)}</button> <span>${esc(msg().fileUpload.drop)}</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
316
  this.rendered = true;
317
317
  this.input = this.querySelector("input[type=file]");
318
318
  const zone = this.querySelector(".k-dropzone");
@@ -342,7 +342,7 @@ export class KashiFileUpload extends HTMLBase {
342
342
  return;
343
343
  const it = this.items.find((x) => x.id === b.dataset.remove);
344
344
  this.items = this.items.filter((x) => x.id !== b.dataset.remove);
345
- this.announce(`${it?.file.name ?? ""} حذف شد`);
345
+ this.announce(msg().common.removed(it?.file.name ?? ""));
346
346
  this.changed();
347
347
  });
348
348
  this.paint();
@@ -361,16 +361,16 @@ export class KashiFileUpload extends HTMLBase {
361
361
  const added = list.map((file, i) => {
362
362
  let error;
363
363
  if (!this.accepts(file))
364
- error = "این نوع فایل پذیرفته نیست.";
364
+ error = msg().fileUpload.wrongType;
365
365
  else if (maxSize && file.size > maxSize)
366
- error = `حجم فایل بیشتر از ${formatBytes(maxSize)} است.`;
366
+ error = msg().fileUpload.tooBig(formatBytes(maxSize));
367
367
  else if (maxFiles && kept + i >= maxFiles)
368
- error = `حداکثر ${toFaDigits(maxFiles)} فایل.`;
368
+ error = msg().fileUpload.tooMany(num(this, maxFiles));
369
369
  const up = !error && !!this.uploader;
370
370
  return { id: uid("f"), file, status: error ? "error" : up ? "uploading" : "done", progress: error ? 0 : up ? 0 : 100, error };
371
371
  });
372
372
  this.items = this.multiple ? [...this.items, ...added] : added;
373
- this.announce(added.map((a) => `${a.file.name}: ${a.error ?? (this.uploader ? "در حال بارگذاری" : "بارگذاری شد")}`).join("، "));
373
+ this.announce(added.map((a) => `${a.file.name}: ${a.error ?? (this.uploader ? msg().fileUpload.uploading : msg().fileUpload.done)}`).join(msg().common.list));
374
374
  this.changed();
375
375
  for (const it of added) {
376
376
  if (it.status !== "uploading" || !this.uploader)
@@ -381,11 +381,11 @@ export class KashiFileUpload extends HTMLBase {
381
381
  }).then(() => {
382
382
  it.status = "done";
383
383
  it.progress = 100;
384
- this.announce(`${it.file.name}: بارگذاری شد`);
384
+ this.announce(`${it.file.name}: ${msg().fileUpload.done}`);
385
385
  this.changed();
386
386
  }, (e) => {
387
387
  it.status = "error";
388
- it.error = e instanceof Error ? e.message : "خطا در بارگذاری";
388
+ it.error = e instanceof Error ? e.message : msg().fileUpload.error;
389
389
  this.announce(`${it.file.name}: ${it.error}`);
390
390
  this.changed();
391
391
  });
@@ -410,7 +410,7 @@ export class KashiFileUpload extends HTMLBase {
410
410
  const list = this.querySelector(".k-file-list");
411
411
  list.hidden = !this.items.length;
412
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("");
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(msg().common.remove(it.file.name))}">${TRASH}</button><span class="k-file__meta">${esc(it.error ?? `${formatBytes(it.file.size)}${it.status === "uploading" ? ` · ${msg().common.percent(num(this, Math.round(it.progress)))}` : it.status === "done" ? ` · ${msg().fileUpload.done}` : ""}`)}</span>${it.status === "uploading" ? `<div role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${Math.round(it.progress)}" aria-label="${esc(msg().fileUpload.uploadingFile(it.file.name))}" class="k-progress k-progress--sm"><div class="k-progress__bar" style="--k-value: ${it.progress}%"></div></div>` : ""}</li>`).join("");
414
414
  }
415
415
  }
416
416
  KashiFileUpload.observedAttributes = ["accept", "multiple", "max-size", "max-files", "hint", "name", "disabled", "label"];
@@ -447,7 +447,8 @@ function hsvToHex({ h, s, v }) {
447
447
  };
448
448
  return toHex(f(5), f(3), f(1));
449
449
  }
450
- const SWATCHES = [["فیروزه‌ای", "#0b8077"], ["لاجوردی", "#2740ed"], ["بنفش", "#7c3aed"], ["اناری", "#e51d2a"], ["زعفرانی", "#dd5e02"], ["پسته‌ای", "#518b25"], ["شب", "#2a2f3d"]];
450
+ const SWATCH_KEYS = [["firoozeh", "#0b8077"], ["lajvard", "#2740ed"], ["violet", "#7c3aed"], ["anar", "#e51d2a"], ["zafaran", "#dd5e02"], ["peste", "#518b25"], ["shab", "#2a2f3d"]];
451
+ const SWATCHES = () => SWATCH_KEYS.map(([k, hex]) => [msg().colorPicker.names[k], hex]);
451
452
  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
  /**
453
454
  * انتخاب رنگ: رنگ‌های پیشنهادی، پنل اشباع و رنگ‌مایه، کد HEX و RGB، و کنتراست با متن سفید یا مشکی.
@@ -505,15 +506,15 @@ export class KashiColorPicker extends HTMLBase {
505
506
  swatches() {
506
507
  const attr = this.getAttribute("swatches");
507
508
  if (attr === null)
508
- return SWATCHES;
509
+ return SWATCHES();
509
510
  return attr.split(",").map((s) => s.trim().split(/\s+/)).filter(([h]) => norm(h)).map(([h, ...n]) => [n.join(" ") || h, norm(h)]);
510
511
  }
511
512
  render() {
512
513
  const k = this.key;
513
514
  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
+ const control = `<div class="k-color">${sw.length ? `<div class="k-color__swatches" role="radiogroup" aria-label="${esc(msg().colorPicker.swatches)}">${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="${esc(msg().colorPicker.custom)}" 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">${esc(msg().colorPicker.sample)}</span><span></span></p>'}</div>`;
515
516
  this.querySelector(":scope > .k-field")?.remove();
516
- this.insertAdjacentHTML("afterbegin", fieldHTML({ id: `${k}-hex`, label: this.getAttribute("label") ?? "رنگ", control }));
517
+ this.insertAdjacentHTML("afterbegin", fieldHTML({ id: `${k}-hex`, label: this.getAttribute("label") ?? msg().colorPicker.label, control }));
517
518
  this.rendered = true;
518
519
  this.querySelector(".k-color__swatches")?.addEventListener("change", (e) => this.pick(e.target.value));
519
520
  const hex = this.querySelector(`#${k}-hex`);
@@ -551,7 +552,7 @@ export class KashiColorPicker extends HTMLBase {
551
552
  const best = Math.max(white, black);
552
553
  const out = this.querySelector(".k-color__contrast span:last-child");
553
554
  if (out)
554
- out.textContent = `کنتراست با متن ${ink === "#ffffff" ? "سفید" : "مشکی"}: ${toFaDigits((Math.round(best * 10) / 10).toFixed(1)).replace(".", "٫")} به ۱ ${best >= 4.5 ? "(AA)" : "(کمتر از AA)"}`;
555
+ out.textContent = msg().colorPicker.contrast(ink === "#ffffff" ? "white" : "black", num(this, (Math.round(best * 10) / 10).toFixed(1)).replace(".", msg().common.decimal), best >= 4.5);
555
556
  const name = this.getAttribute("name");
556
557
  this.querySelectorAll(":scope > input[type=hidden]").forEach((el) => el.remove());
557
558
  if (name)
@@ -562,15 +563,15 @@ export class KashiColorPicker extends HTMLBase {
562
563
  }
563
564
  paintMessages(invalid) {
564
565
  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
+ setMessages(this.querySelector(":scope > .k-field"), hex, `${this.key}-hex`, invalid ? msg().colorPicker.invalid : null, this.getAttribute("hint"));
566
567
  }
567
568
  openPanel() {
568
569
  const anchor = this.querySelector(".k-color__anchor");
569
570
  const p = document.createElement("div");
570
571
  p.className = "k-color__panel";
571
572
  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>`;
573
+ p.setAttribute("aria-label", msg().colorPicker.custom);
574
+ p.innerHTML = `<div class="k-color__area" role="slider" tabindex="0" aria-label="${esc(msg().colorPicker.area)}"><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="${esc(msg().colorPicker.hue)}"></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="${esc([msg().colorPicker.red, msg().colorPicker.green, msg().colorPicker.blue][i])}"><span aria-hidden="true">${ch}</span></label>`).join("")}</div>`;
574
575
  anchor.append(p);
575
576
  this.panel = p;
576
577
  const area = p.querySelector(".k-color__area");
@@ -619,17 +620,17 @@ export class KashiColorPicker extends HTMLBase {
619
620
  paintPanel() {
620
621
  const p = this.panel;
621
622
  const { h, s, v } = this.hsv;
622
- const pct = (n) => `${toFaDigits(Math.round(n * 100))}٪`;
623
+ const pct = (n) => msg().common.percent(num(this, Math.round(n * 100)));
623
624
  const area = p.querySelector(".k-color__area");
624
625
  area.style.setProperty("--_hue", `hsl(${h} 100% 50%)`);
625
626
  area.setAttribute("aria-valuenow", String(Math.round(s * 100)));
626
- area.setAttribute("aria-valuetext", `اشباع ${pct(s)}، روشنایی ${pct(v)}`);
627
+ area.setAttribute("aria-valuetext", msg().colorPicker.areaValue(pct(s), pct(v)));
627
628
  const thumb = p.querySelector(".k-color__thumb");
628
629
  thumb.style.left = `${s * 100}%`;
629
630
  thumb.style.top = `${(1 - v) * 100}%`;
630
631
  const hue = p.querySelector(".k-color__hue");
631
632
  hue.value = String(Math.round(h));
632
- hue.setAttribute("aria-valuetext", `${toFaDigits(Math.round(h))} درجه`);
633
+ hue.setAttribute("aria-valuetext", msg().colorPicker.degrees(num(this, Math.round(h))));
633
634
  const rgb = rgbOf(this.current);
634
635
  p.querySelectorAll(".k-color__num").forEach((el, i) => {
635
636
  if (document.activeElement !== el)
package/dist/table.js CHANGED
@@ -1,5 +1,5 @@
1
- import { normalizePersian, toFaDigits } from "@kashi-ui/persian";
2
- import { HTMLBase, afterParse, bool, define, emit, esc, uid } from "./base.js";
1
+ import { normalizePersian } from "@kashi-ui/persian";
2
+ import { afterParse, bool, define, emit, esc, HTMLBase, msg, num, uid } from "./base.js";
3
3
  import { cellNumber } from "./controls.js";
4
4
  const search = (s) => normalizePersian(s, { digits: "latin" }).toLowerCase();
5
5
  /**
@@ -123,7 +123,7 @@ export class KashiDataTable extends HTMLBase {
123
123
  const th = document.createElement("th");
124
124
  th.scope = "col";
125
125
  th.className = "k-table__check";
126
- th.innerHTML = '<input type="checkbox" class="k-checkbox" aria-label="انتخاب همهٔ ردیف‌ها">';
126
+ th.innerHTML = '<input type="checkbox" class="k-checkbox" aria-label="${esc(msg().table.selectAll)}">';
127
127
  head.prepend(th);
128
128
  th.querySelector("input").addEventListener("change", (e) => {
129
129
  const on = e.target.checked;
@@ -162,7 +162,7 @@ export class KashiDataTable extends HTMLBase {
162
162
  const td = document.createElement("td");
163
163
  td.className = "k-table__check";
164
164
  const label = tr.children[0]?.textContent?.trim();
165
- td.innerHTML = `<input type="checkbox" class="k-checkbox" aria-label="${esc(label ? `انتخاب ${label}` : "انتخاب ردیف")}">`;
165
+ td.innerHTML = `<input type="checkbox" class="k-checkbox" aria-label="${esc(label ? msg().table.selectNamed(label) : msg().table.selectRow)}">`;
166
166
  tr.prepend(td);
167
167
  tr.setAttribute("aria-selected", "false");
168
168
  td.querySelector("input").addEventListener("change", (e) => {
@@ -212,13 +212,13 @@ export class KashiDataTable extends HTMLBase {
212
212
  const box = document.createElement("div");
213
213
  box.className = "k-table-search k-field";
214
214
  box.style.marginBlockEnd = "var(--k-space-3)";
215
- box.innerHTML = `<div class="k-input k-input--sm"><input id="${id}" class="k-input__control" type="search" placeholder="جستجو در جدول" aria-label="جستجو در جدول"></div><span class="k-sr-only" aria-live="polite"></span>`;
215
+ box.innerHTML = `<div class="k-input k-input--sm"><input id="${id}" class="k-input__control" type="search" placeholder="${esc(msg().table.search)}" aria-label="${esc(msg().table.search)}"></div><span class="k-sr-only" aria-live="polite"></span>`;
216
216
  this.prepend(box);
217
217
  const input = box.querySelector("input");
218
218
  input.addEventListener("input", () => {
219
219
  this.query = input.value;
220
220
  this.apply();
221
- box.querySelector(".k-sr-only").textContent = `${toFaDigits(this.visibleRows().length)} ردیف`;
221
+ box.querySelector(".k-sr-only").textContent = msg().table.rows(num(this, this.visibleRows().length));
222
222
  });
223
223
  }
224
224
  /** مرتب‌سازی و جستجو را روی ردیف‌ها اعمال می‌کند */
@@ -258,7 +258,7 @@ export class KashiDataTable extends HTMLBase {
258
258
  body.querySelector(".k-table__empty")?.remove();
259
259
  if (q && !this.visibleRows().length) {
260
260
  const cols = ths.length;
261
- body.insertAdjacentHTML("beforeend", `<tr class="k-table__empty"><td colspan="${cols}"><p style="padding: 2rem; margin: 0; text-align: center">نتیجه‌ای پیدا نشد.</p></td></tr>`);
261
+ body.insertAdjacentHTML("beforeend", `<tr class="k-table__empty"><td colspan="${cols}"><p style="padding: 2rem; margin: 0; text-align: center">${esc(msg().common.noResults)}</p></td></tr>`);
262
262
  }
263
263
  this.selectionChanged(false);
264
264
  }