@wrrdev/fenix-ui 1.0.14 → 1.0.16

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 (111) hide show
  1. package/README.md +332 -332
  2. package/dist/components/accordion/accordion.d.ts +1 -0
  3. package/dist/components/accordion/accordion.d.ts.map +1 -1
  4. package/dist/components/accordion/accordion.js +18 -13
  5. package/dist/components/accordion/accordion.js.map +1 -1
  6. package/dist/components/accordion/index.js.map +1 -1
  7. package/dist/components/alert/alert.d.ts.map +1 -1
  8. package/dist/components/alert/alert.js +2 -1
  9. package/dist/components/alert/alert.js.map +1 -1
  10. package/dist/components/alert/index.js.map +1 -1
  11. package/dist/components/autocomplete/autocomplete.d.ts +3 -0
  12. package/dist/components/autocomplete/autocomplete.d.ts.map +1 -1
  13. package/dist/components/autocomplete/autocomplete.js +28 -4
  14. package/dist/components/autocomplete/autocomplete.js.map +1 -1
  15. package/dist/components/autocomplete/index.js.map +1 -1
  16. package/dist/components/datepicker/datepicker.d.ts.map +1 -1
  17. package/dist/components/datepicker/datepicker.js +7 -6
  18. package/dist/components/datepicker/datepicker.js.map +1 -1
  19. package/dist/components/dialog/dialog.d.ts +4 -0
  20. package/dist/components/dialog/dialog.d.ts.map +1 -1
  21. package/dist/components/dialog/dialog.js +42 -4
  22. package/dist/components/dialog/dialog.js.map +1 -1
  23. package/dist/components/dialog/index.js.map +1 -1
  24. package/dist/components/drawer/drawer.d.ts +5 -0
  25. package/dist/components/drawer/drawer.d.ts.map +1 -1
  26. package/dist/components/drawer/drawer.js +49 -15
  27. package/dist/components/drawer/drawer.js.map +1 -1
  28. package/dist/components/dropdown/dropdown.d.ts +5 -0
  29. package/dist/components/dropdown/dropdown.d.ts.map +1 -1
  30. package/dist/components/dropdown/dropdown.js +34 -10
  31. package/dist/components/dropdown/dropdown.js.map +1 -1
  32. package/dist/components/input/input.d.ts.map +1 -1
  33. package/dist/components/input/input.js +4 -3
  34. package/dist/components/input/input.js.map +1 -1
  35. package/dist/components/knob/index.js.map +1 -1
  36. package/dist/components/knob/knob.d.ts +2 -0
  37. package/dist/components/knob/knob.d.ts.map +1 -1
  38. package/dist/components/knob/knob.js +15 -3
  39. package/dist/components/knob/knob.js.map +1 -1
  40. package/dist/components/multiselect/multiselect.d.ts +2 -0
  41. package/dist/components/multiselect/multiselect.d.ts.map +1 -1
  42. package/dist/components/multiselect/multiselect.js +38 -8
  43. package/dist/components/multiselect/multiselect.js.map +1 -1
  44. package/dist/components/orderlist/orderlist.d.ts.map +1 -1
  45. package/dist/components/orderlist/orderlist.js +1 -3
  46. package/dist/components/orderlist/orderlist.js.map +1 -1
  47. package/dist/components/pagination/index.js.map +1 -1
  48. package/dist/components/pagination/pagination.d.ts +1 -0
  49. package/dist/components/pagination/pagination.d.ts.map +1 -1
  50. package/dist/components/pagination/pagination.js +19 -8
  51. package/dist/components/pagination/pagination.js.map +1 -1
  52. package/dist/components/picklist/picklist.d.ts.map +1 -1
  53. package/dist/components/picklist/picklist.js +1 -3
  54. package/dist/components/picklist/picklist.js.map +1 -1
  55. package/dist/components/progress/index.js.map +1 -1
  56. package/dist/components/progress/progress.d.ts.map +1 -1
  57. package/dist/components/progress/progress.js +2 -1
  58. package/dist/components/progress/progress.js.map +1 -1
  59. package/dist/components/radio/radio.js +1 -1
  60. package/dist/components/radio/radio.js.map +1 -1
  61. package/dist/components/select/select.d.ts +2 -0
  62. package/dist/components/select/select.d.ts.map +1 -1
  63. package/dist/components/select/select.js +36 -5
  64. package/dist/components/select/select.js.map +1 -1
  65. package/dist/components/skeleton/index.js.map +1 -1
  66. package/dist/components/skeleton/skeleton.d.ts +2 -0
  67. package/dist/components/skeleton/skeleton.d.ts.map +1 -1
  68. package/dist/components/skeleton/skeleton.js +7 -2
  69. package/dist/components/skeleton/skeleton.js.map +1 -1
  70. package/dist/components/table/expr.d.ts +38 -0
  71. package/dist/components/table/expr.d.ts.map +1 -0
  72. package/dist/components/table/expr.js +429 -0
  73. package/dist/components/table/expr.js.map +1 -0
  74. package/dist/components/table/table.d.ts +26 -8
  75. package/dist/components/table/table.d.ts.map +1 -1
  76. package/dist/components/table/table.js +402 -140
  77. package/dist/components/table/table.js.map +1 -1
  78. package/dist/components/tabs/tabs.d.ts.map +1 -1
  79. package/dist/components/tabs/tabs.js +3 -2
  80. package/dist/components/tabs/tabs.js.map +1 -1
  81. package/dist/components/textarea/index.js.map +1 -1
  82. package/dist/components/textarea/textarea.d.ts.map +1 -1
  83. package/dist/components/textarea/textarea.js +4 -3
  84. package/dist/components/textarea/textarea.js.map +1 -1
  85. package/dist/components/toast/index.js.map +1 -1
  86. package/dist/components/tooltip/directive.js.map +1 -1
  87. package/dist/components/tooltip/index.js.map +1 -1
  88. package/dist/components/tooltip/tooltip.d.ts.map +1 -1
  89. package/dist/components/tooltip/tooltip.js +2 -1
  90. package/dist/components/tooltip/tooltip.js.map +1 -1
  91. package/dist/core/jsx.js.map +1 -1
  92. package/dist/core/presets.js.map +1 -1
  93. package/dist/core/sanitize.d.ts +36 -0
  94. package/dist/core/sanitize.d.ts.map +1 -0
  95. package/dist/core/sanitize.js +37 -0
  96. package/dist/core/sanitize.js.map +1 -0
  97. package/dist/core/tokens.js.map +1 -1
  98. package/dist/core/vue.js.map +1 -1
  99. package/dist/docs/app.d.ts.map +1 -1
  100. package/dist/docs/componentes/table.doc.d.ts.map +1 -1
  101. package/dist/fenix-ui.esm.js.map +1 -1
  102. package/dist/fenix-ui.umd.min.js +339 -208
  103. package/dist/fenix-ui2.esm.js +1156 -243
  104. package/dist/fenix-ui2.esm.js.map +1 -1
  105. package/dist/fenix-ui3.esm.js.map +1 -1
  106. package/dist/index.d.ts +2 -1
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +4 -0
  109. package/dist/index.js.map +1 -1
  110. package/dist/plugins/auto-import.js.map +1 -1
  111. package/package.json +212 -212
@@ -305,6 +305,37 @@ function defineElement(tag, ctor) {
305
305
  }
306
306
  return customElements.get(tag) ?? ctor;
307
307
  }
308
+ const HTML_ENTITIES = {
309
+ "&": "&",
310
+ "<": "&lt;",
311
+ ">": "&gt;",
312
+ '"': "&quot;",
313
+ "'": "&#39;",
314
+ "/": "&#x2F;",
315
+ "`": "&#x60;",
316
+ "=": "&#x3D;"
317
+ };
318
+ const SANITIZE_REGEX = /[&<>"'`=/]/g;
319
+ function esc(value) {
320
+ if (value == null) return "";
321
+ return String(value).replace(SANITIZE_REGEX, (char) => HTML_ENTITIES[char] || char);
322
+ }
323
+ function stripTags(html) {
324
+ return html.replace(/<[^>]*>/g, "");
325
+ }
326
+ function sanitizeData(data) {
327
+ if (data == null) return data;
328
+ if (typeof data === "string") return esc(data);
329
+ if (Array.isArray(data)) return data.map(sanitizeData);
330
+ if (typeof data === "object") {
331
+ const result = {};
332
+ for (const [key2, value] of Object.entries(data)) {
333
+ result[key2] = sanitizeData(value);
334
+ }
335
+ return result;
336
+ }
337
+ return data;
338
+ }
308
339
  const FX_JSX_TYPES = true;
309
340
  const _FxButton = class _FxButton extends FxElement {
310
341
  constructor() {
@@ -633,6 +664,23 @@ const _FxSelect = class _FxSelect extends FxElement {
633
664
  new CustomEvent("change", { bubbles: true, composed: true, detail: { value } })
634
665
  );
635
666
  }
667
+ /** Navegação por teclado entre as opções do listbox (WCAG 2.1.1). */
668
+ _navigateOptions(e) {
669
+ e.preventDefault();
670
+ if (!this.hasAttr("open")) {
671
+ this.toggleAttribute("open");
672
+ this.render();
673
+ }
674
+ const opts = Array.from(this.root.querySelectorAll(".opt"));
675
+ if (!opts.length) return;
676
+ const current = opts.indexOf(this.root.activeElement);
677
+ let next = 0;
678
+ if (e.key === "ArrowDown") next = current === -1 ? 0 : Math.min(current + 1, opts.length - 1);
679
+ else if (e.key === "ArrowUp") next = current === -1 ? opts.length - 1 : Math.max(current - 1, 0);
680
+ else if (e.key === "Home") next = 0;
681
+ else if (e.key === "End") next = opts.length - 1;
682
+ opts[next]?.focus();
683
+ }
636
684
  render() {
637
685
  const prevOpen = this.hasAttr("open");
638
686
  const prevSearch = this.root.querySelector(".search");
@@ -650,18 +698,18 @@ const _FxSelect = class _FxSelect extends FxElement {
650
698
  const filtered = search ? opts.filter((o) => o.label.toLowerCase().includes(search.toLowerCase())) : opts;
651
699
  this.setTemplate(`
652
700
  <span class="trigger" part="trigger" role="button" tabindex="${this.disabled ? -1 : 0}" aria-haspopup="listbox" aria-expanded="${prevOpen}">
653
- <span class="${selectedLabel ? "label" : "placeholder"}">${selectedLabel || placeholder}</span>
701
+ <span class="${selectedLabel ? "label" : "placeholder"}">${esc(selectedLabel) || esc(placeholder)}</span>
654
702
  <span class="actions">
655
703
  ${this.hasAttr("clearable") && current ? '<button type="button" class="clear" part="clear" aria-label="Limpar">×</button>' : ""}
656
704
  <span class="caret">▼</span>
657
705
  </span>
658
706
  </span>
659
707
  <div class="panel" part="panel" role="listbox">
660
- ${this.hasAttr("searchable") ? `<input class="search" part="search" type="text" placeholder="${this.getAttr("search-placeholder", "Pesquisar…")}">` : ""}
708
+ ${this.hasAttr("searchable") ? `<input class="search" part="search" type="text" placeholder="${esc(this.getAttr("search-placeholder", "Pesquisar…"))}">` : ""}
661
709
  ${filtered.map((o) => `
662
- <button type="button" class="opt" role="option" data-value="${o.value}"
663
- aria-selected="${o.value === current}">${o.label}</button>`).join("")}
664
- ${filtered.length === 0 ? `<div class="empty">${this.getAttr("no-results", "Nenhum resultado")}</div>` : ""}
710
+ <button type="button" class="opt" role="option" data-value="${esc(o.value)}"
711
+ aria-selected="${o.value === current}">${esc(o.label)}</button>`).join("")}
712
+ ${filtered.length === 0 ? `<div class="empty">${esc(this.getAttr("no-results", "Nenhum resultado"))}</div>` : ""}
665
713
  </div>
666
714
  `);
667
715
  if (prevOpen) this.setAttribute("open", "");
@@ -689,9 +737,22 @@ const _FxSelect = class _FxSelect extends FxElement {
689
737
  this.root.querySelector(".search")?.focus();
690
738
  });
691
739
  trigger.addEventListener("keydown", (e) => {
740
+ if (this.disabled) return;
692
741
  if (e.key === "Enter" || e.key === " ") {
693
742
  e.preventDefault();
694
743
  trigger.click();
744
+ return;
745
+ }
746
+ if (e.key === "Escape") {
747
+ if (this.hasAttr("open")) {
748
+ this.removeAttribute("open");
749
+ this.render();
750
+ this.root.querySelector(".trigger")?.focus();
751
+ }
752
+ return;
753
+ }
754
+ if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Home" || e.key === "End") {
755
+ this._navigateOptions(e);
695
756
  }
696
757
  });
697
758
  this.root.querySelector(".clear")?.addEventListener("click", (e) => {
@@ -901,9 +962,9 @@ const _FxInput = class _FxInput extends FxElement {
901
962
  const step = this.getAttr("step");
902
963
  this.setTemplate(`
903
964
  ${this.hasAttr("clearable") ? '<div class="wrap">' : ""}
904
- <input class="field" part="input" type="${type}"
905
- ${placeholder ? `placeholder="${placeholder}"` : ""}
906
- ${min ? `min="${min}"` : ""} ${max ? `max="${max}"` : ""} ${step ? `step="${step}"` : ""}
965
+ <input class="field" part="input" type="${esc(type)}"
966
+ ${placeholder ? `placeholder="${esc(placeholder)}"` : ""}
967
+ ${min ? `min="${esc(min)}"` : ""} ${max ? `max="${esc(max)}"` : ""} ${step ? `step="${esc(step)}"` : ""}
907
968
  />
908
969
  ${this.hasAttr("clearable") ? '<button type="button" class="clear" part="clear" aria-label="Limpar" tabindex="-1">×</button></div>' : ""}
909
970
  `);
@@ -1189,6 +1250,23 @@ const _FxMultiselect = class _FxMultiselect extends FxElement {
1189
1250
  else this.selected.add(value);
1190
1251
  this.commit();
1191
1252
  }
1253
+ /** Navegação por teclado entre as opções do listbox (WCAG 2.1.1). */
1254
+ _navigateOptions(e) {
1255
+ e.preventDefault();
1256
+ if (!this.hasAttr("open")) {
1257
+ this.toggleAttribute("open");
1258
+ this.render();
1259
+ }
1260
+ const opts = Array.from(this.root.querySelectorAll(".opt"));
1261
+ if (!opts.length) return;
1262
+ const current = opts.indexOf(this.root.activeElement);
1263
+ let next = 0;
1264
+ if (e.key === "ArrowDown") next = current === -1 ? 0 : Math.min(current + 1, opts.length - 1);
1265
+ else if (e.key === "ArrowUp") next = current === -1 ? opts.length - 1 : Math.max(current - 1, 0);
1266
+ else if (e.key === "Home") next = 0;
1267
+ else if (e.key === "End") next = opts.length - 1;
1268
+ opts[next]?.focus();
1269
+ }
1192
1270
  render() {
1193
1271
  const wasOpen = this.hasAttr("open");
1194
1272
  const search = this.root.querySelector(".search")?.value ?? "";
@@ -1203,10 +1281,10 @@ const _FxMultiselect = class _FxMultiselect extends FxElement {
1203
1281
  <span class="trigger" part="trigger" role="button" tabindex="${this.disabled ? -1 : 0}" aria-haspopup="listbox" aria-expanded="${wasOpen}" aria-disabled="${this.disabled}">
1204
1282
  <span class="lead">
1205
1283
  ${chips.length ? chips.map((c) => `
1206
- <span class="chip" data-value="${c.value}">
1207
- ${c.label}
1208
- <span class="chip__x" role="button" aria-label="Remover ${c.label}">×</span>
1209
- </span>`).join("") : `<span class="placeholder">${placeholder}</span>`}
1284
+ <span class="chip" data-value="${esc(c.value)}">
1285
+ ${esc(c.label)}
1286
+ <span class="chip__x" role="button" aria-label="Remover ${esc(c.label)}">×</span>
1287
+ </span>`).join("") : `<span class="placeholder">${esc(placeholder)}</span>`}
1210
1288
  </span>
1211
1289
  <span class="trigger-right">
1212
1290
  ${this.hasAttr("clearable") && chips.length ? '<button type="button" class="icon-btn clear" part="clear" aria-label="Limpar seleção">×</button>' : ""}
@@ -1218,14 +1296,14 @@ const _FxMultiselect = class _FxMultiselect extends FxElement {
1218
1296
  <span class="count">${chips.length} selecionado${chips.length === 1 ? "" : "s"}</span>
1219
1297
  ${this.hasAttr("clearable") && chips.length ? '<button type="button" class="clear-all">Limpar tudo</button>' : ""}
1220
1298
  </div>
1221
- ${this.hasAttr("searchable") ? `<input class="search" part="search" type="text" placeholder="${this.getAttr("search-placeholder", "Pesquisar…")}">` : ""}
1299
+ ${this.hasAttr("searchable") ? `<input class="search" part="search" type="text" placeholder="${esc(this.getAttr("search-placeholder", "Pesquisar…"))}">` : ""}
1222
1300
  <ul class="list">
1223
1301
  ${filtered.map((o) => `
1224
- <li><button type="button" class="opt" role="option" data-value="${o.value}"
1302
+ <li><button type="button" class="opt" role="option" data-value="${esc(o.value)}"
1225
1303
  aria-selected="${this.selected.has(o.value)}">
1226
- <span class="box" aria-hidden="true"></span>${o.label}
1304
+ <span class="box" aria-hidden="true"></span>${esc(o.label)}
1227
1305
  </button></li>`).join("")}
1228
- ${filtered.length === 0 ? `<li class="empty">${this.getAttr("no-results", "Nenhum resultado")}</li>` : ""}
1306
+ ${filtered.length === 0 ? `<li class="empty">${esc(this.getAttr("no-results", "Nenhum resultado"))}</li>` : ""}
1229
1307
  </ul>
1230
1308
  </div>
1231
1309
  `);
@@ -1264,6 +1342,18 @@ const _FxMultiselect = class _FxMultiselect extends FxElement {
1264
1342
  if (e.key === "Enter" || e.key === " ") {
1265
1343
  e.preventDefault();
1266
1344
  trigger.dispatchEvent(new MouseEvent("click", { bubbles: true }));
1345
+ return;
1346
+ }
1347
+ if (e.key === "Escape") {
1348
+ if (this.hasAttr("open")) {
1349
+ this.removeAttribute("open");
1350
+ this.render();
1351
+ this.root.querySelector(".trigger")?.focus();
1352
+ }
1353
+ return;
1354
+ }
1355
+ if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Home" || e.key === "End") {
1356
+ this._navigateOptions(e);
1267
1357
  }
1268
1358
  });
1269
1359
  this.root.querySelector(".clear")?.addEventListener("click", (e) => {
@@ -2047,21 +2137,21 @@ const _FxDatepicker = class _FxDatepicker extends FxElement {
2047
2137
  const hasValue = text.length > 0;
2048
2138
  const showTime = this.hasAttr("show-time");
2049
2139
  this.loadTimeFromValue();
2050
- let calAttrs = `min="${this.getAttr("min")}" max="${this.getAttr("max")}"`;
2140
+ let calAttrs = `min="${esc(this.getAttr("min"))}" max="${esc(this.getAttr("max"))}"`;
2051
2141
  if (this.mode === "range") {
2052
- calAttrs += ` range start="${this.getAttr("start")}" end="${this.getAttr("end")}"`;
2142
+ calAttrs += ` range start="${esc(this.getAttr("start"))}" end="${esc(this.getAttr("end"))}"`;
2053
2143
  } else if (this.mode === "multiple") {
2054
2144
  const dates = this.values.map((v) => v.split("T")[0]).join(",");
2055
- calAttrs += ` mode="multiple" values="${dates}"`;
2145
+ calAttrs += ` mode="multiple" values="${esc(dates)}"`;
2056
2146
  } else {
2057
2147
  const v = this.value.split("T")[0] ?? "";
2058
- calAttrs += ` value="${v}"`;
2148
+ calAttrs += ` value="${esc(v)}"`;
2059
2149
  }
2060
2150
  this.setTemplate(`
2061
2151
  <div class="field" part="field">
2062
2152
  <input class="display" part="display" type="text" ${this.hasAttr("free-text") && !disabled ? "" : "readonly"}
2063
- placeholder="${this.getAttr("placeholder", "dd/mm/aaaa")}"
2064
- value="${text}" ${disabled ? "disabled" : ""}>
2153
+ placeholder="${esc(this.getAttr("placeholder", "dd/mm/aaaa"))}"
2154
+ value="${esc(text)}" ${disabled ? "disabled" : ""}>
2065
2155
  <button type="button" class="clear" part="clear" aria-label="Limpar"
2066
2156
  ${clearable && hasValue ? "" : "hidden"}>
2067
2157
  ×
@@ -2564,7 +2654,7 @@ const _FxRadio = class _FxRadio extends FxElement {
2564
2654
  if (this.disabled || this.checked) return;
2565
2655
  const name = this.getAttr("name");
2566
2656
  if (name) {
2567
- document.querySelectorAll(`fx-radio[name="${name}"]`).forEach((r) => {
2657
+ document.querySelectorAll(`fx-radio[name="${CSS.escape(name)}"]`).forEach((r) => {
2568
2658
  r.checked = false;
2569
2659
  });
2570
2660
  }
@@ -2640,17 +2730,465 @@ function defineFxRadio() {
2640
2730
  return defineElement("fx-radio", FxRadio);
2641
2731
  }
2642
2732
  defineFxRadio();
2733
+ const LOCALE = "pt-BR";
2734
+ const PIPES = {
2735
+ /** Formata como moeda brasileira (R$). */
2736
+ currency: (v) => {
2737
+ if (v == null || v === "") return "";
2738
+ try {
2739
+ return new Intl.NumberFormat(LOCALE, {
2740
+ style: "currency",
2741
+ currency: "BRL"
2742
+ }).format(Number(v));
2743
+ } catch {
2744
+ return String(v);
2745
+ }
2746
+ },
2747
+ /** Formata como data. Estilos: short | medium | long | full (default medium). */
2748
+ date: (v, arg) => {
2749
+ const d = toDate(v);
2750
+ if (!d) return String(v ?? "");
2751
+ const styles = {
2752
+ short: { dateStyle: "short" },
2753
+ medium: { dateStyle: "medium" },
2754
+ long: { dateStyle: "long" },
2755
+ full: { dateStyle: "full" }
2756
+ };
2757
+ const opts = arg && styles[arg] ? styles[arg] : styles.medium;
2758
+ try {
2759
+ return new Intl.DateTimeFormat(LOCALE, opts).format(d);
2760
+ } catch {
2761
+ return d.toLocaleDateString(LOCALE);
2762
+ }
2763
+ },
2764
+ /** Formata como data + hora (medium/short). */
2765
+ dateTime: (v) => {
2766
+ const d = toDate(v);
2767
+ if (!d) return String(v ?? "");
2768
+ try {
2769
+ return new Intl.DateTimeFormat(LOCALE, {
2770
+ dateStyle: "medium",
2771
+ timeStyle: "short"
2772
+ }).format(d);
2773
+ } catch {
2774
+ return d.toLocaleString(LOCALE);
2775
+ }
2776
+ },
2777
+ /** Formata como número (pt-BR). Opcional: `number: 2` (casas decimais). */
2778
+ number: (v, arg) => {
2779
+ if (v == null || v === "") return "";
2780
+ try {
2781
+ const opt = {};
2782
+ if (arg !== void 0) {
2783
+ const dec = Number(arg);
2784
+ opt.minimumFractionDigits = dec;
2785
+ opt.maximumFractionDigits = dec;
2786
+ }
2787
+ return new Intl.NumberFormat(LOCALE, opt).format(Number(v));
2788
+ } catch {
2789
+ return String(v);
2790
+ }
2791
+ }
2792
+ };
2793
+ function toDate(v) {
2794
+ if (v == null || v === "") return null;
2795
+ if (v instanceof Date) return isNaN(v.getTime()) ? null : v;
2796
+ if (typeof v === "number") {
2797
+ const d = new Date(v);
2798
+ return isNaN(d.getTime()) ? null : d;
2799
+ }
2800
+ if (typeof v === "string") {
2801
+ const d = new Date(v);
2802
+ return isNaN(d.getTime()) ? null : d;
2803
+ }
2804
+ return null;
2805
+ }
2806
+ const IDENT_START = (ch) => /[a-z_$]/i.test(ch);
2807
+ const IDENT_PART = (ch) => /[a-z0-9_$]/i.test(ch);
2808
+ function tokenize(expr) {
2809
+ const tokens = [];
2810
+ let i = 0;
2811
+ while (i < expr.length) {
2812
+ const ch = expr[i];
2813
+ if (/\s/.test(ch)) {
2814
+ i++;
2815
+ continue;
2816
+ }
2817
+ if (ch === "'" || ch === '"') {
2818
+ const quote = ch;
2819
+ let str = "";
2820
+ i++;
2821
+ while (i < expr.length && expr[i] !== quote) {
2822
+ if (expr[i] === "\\" && i + 1 < expr.length) {
2823
+ str += expr[i + 1];
2824
+ i += 2;
2825
+ } else {
2826
+ str += expr[i];
2827
+ i++;
2828
+ }
2829
+ }
2830
+ i++;
2831
+ tokens.push({ type: "str", value: str });
2832
+ continue;
2833
+ }
2834
+ if (ch >= "0" && ch <= "9") {
2835
+ let numStr = "";
2836
+ while (i < expr.length && (expr[i] >= "0" && expr[i] <= "9" || expr[i] === ".")) {
2837
+ numStr += expr[i];
2838
+ i++;
2839
+ }
2840
+ tokens.push({ type: "num", value: numStr });
2841
+ continue;
2842
+ }
2843
+ if (IDENT_START(ch)) {
2844
+ let ident = "";
2845
+ while (i < expr.length && IDENT_PART(expr[i])) {
2846
+ ident += expr[i];
2847
+ i++;
2848
+ }
2849
+ if (ident === "true" || ident === "false" || ident === "null") {
2850
+ tokens.push({ type: "kw", value: ident });
2851
+ } else {
2852
+ tokens.push({ type: "id", value: ident });
2853
+ }
2854
+ continue;
2855
+ }
2856
+ const two = expr.slice(i, i + 2);
2857
+ if (two === "==" || two === "!=" || two === ">=" || two === "<=") {
2858
+ tokens.push({ type: "cmp", value: two });
2859
+ i += 2;
2860
+ continue;
2861
+ }
2862
+ if (ch === "+") {
2863
+ tokens.push({ type: "op", value: "+" });
2864
+ i++;
2865
+ continue;
2866
+ }
2867
+ if (ch === "-") {
2868
+ tokens.push({ type: "op", value: "-" });
2869
+ i++;
2870
+ continue;
2871
+ }
2872
+ if (ch === "*") {
2873
+ tokens.push({ type: "op", value: "*" });
2874
+ i++;
2875
+ continue;
2876
+ }
2877
+ if (ch === "/") {
2878
+ tokens.push({ type: "op", value: "/" });
2879
+ i++;
2880
+ continue;
2881
+ }
2882
+ if (ch === ">") {
2883
+ tokens.push({ type: "cmp", value: ">" });
2884
+ i++;
2885
+ continue;
2886
+ }
2887
+ if (ch === "<") {
2888
+ tokens.push({ type: "cmp", value: "<" });
2889
+ i++;
2890
+ continue;
2891
+ }
2892
+ if (ch === "!") {
2893
+ tokens.push({ type: "op", value: "!" });
2894
+ i++;
2895
+ continue;
2896
+ }
2897
+ if (ch === "?") {
2898
+ tokens.push({ type: "qm", value: "?" });
2899
+ i++;
2900
+ continue;
2901
+ }
2902
+ if (ch === ":") {
2903
+ tokens.push({ type: "colon", value: ":" });
2904
+ i++;
2905
+ continue;
2906
+ }
2907
+ if (ch === "|") {
2908
+ tokens.push({ type: "pipe", value: "|" });
2909
+ i++;
2910
+ continue;
2911
+ }
2912
+ if (ch === "(") {
2913
+ tokens.push({ type: "lparen", value: "(" });
2914
+ i++;
2915
+ continue;
2916
+ }
2917
+ if (ch === ")") {
2918
+ tokens.push({ type: "rparen", value: ")" });
2919
+ i++;
2920
+ continue;
2921
+ }
2922
+ if (ch === ".") {
2923
+ tokens.push({ type: "dot", value: "." });
2924
+ i++;
2925
+ continue;
2926
+ }
2927
+ i++;
2928
+ }
2929
+ return tokens;
2930
+ }
2931
+ class Parser {
2932
+ constructor(tokens, ctx) {
2933
+ this.tokens = tokens;
2934
+ this.ctx = ctx;
2935
+ this.pos = 0;
2936
+ }
2937
+ peek() {
2938
+ return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
2939
+ }
2940
+ next() {
2941
+ return this.pos < this.tokens.length ? this.tokens[this.pos++] : null;
2942
+ }
2943
+ matchType(type) {
2944
+ if (this.peek()?.type === type) return this.next();
2945
+ return null;
2946
+ }
2947
+ parse() {
2948
+ return this.parsePipe();
2949
+ }
2950
+ /* pipeExpr := ternary ('|' IDENT (':' pipeArg)? )* */
2951
+ parsePipe() {
2952
+ let val = this.parseTernary();
2953
+ while (this.peek()?.type === "pipe") {
2954
+ this.next();
2955
+ const nameTok = this.matchType("id");
2956
+ if (!nameTok) break;
2957
+ let arg;
2958
+ if (this.peek()?.type === "colon") {
2959
+ this.next();
2960
+ const argTok = this.peek();
2961
+ if (argTok && (argTok.type === "str" || argTok.type === "num" || argTok.type === "id")) {
2962
+ arg = argTok.type === "str" ? argTok.value : String(argTok.value);
2963
+ this.next();
2964
+ }
2965
+ }
2966
+ val = this.applyPipe(nameTok.value, arg, val);
2967
+ }
2968
+ return val;
2969
+ }
2970
+ /* ternary := comparison ('?' pipeExpr ':' pipeExpr)? */
2971
+ parseTernary() {
2972
+ const cond = this.parseComparison();
2973
+ if (this.peek()?.type === "qm") {
2974
+ this.next();
2975
+ const trueVal = this.parsePipe();
2976
+ if (this.peek()?.type === "colon") {
2977
+ this.next();
2978
+ const falseVal = this.parsePipe();
2979
+ return cond ? trueVal : falseVal;
2980
+ }
2981
+ return trueVal;
2982
+ }
2983
+ return cond;
2984
+ }
2985
+ /* comparison := additive (COMP_OP additive)? */
2986
+ parseComparison() {
2987
+ const left = this.parseAdditive();
2988
+ const tok = this.peek();
2989
+ if (tok?.type === "cmp") {
2990
+ this.next();
2991
+ const right = this.parseAdditive();
2992
+ return this.compare(left, tok.value, right);
2993
+ }
2994
+ return left;
2995
+ }
2996
+ compare(left, op, right) {
2997
+ const l = toComparable(left);
2998
+ const r = toComparable(right);
2999
+ switch (op) {
3000
+ case "==":
3001
+ return l == r;
3002
+ case "!=":
3003
+ return l != r;
3004
+ case ">":
3005
+ return l > r;
3006
+ case "<":
3007
+ return l < r;
3008
+ case ">=":
3009
+ return l >= r;
3010
+ case "<=":
3011
+ return l <= r;
3012
+ }
3013
+ return false;
3014
+ }
3015
+ /* additive := multiplicative (('+' | '-') multiplicative)* */
3016
+ parseAdditive() {
3017
+ let left = this.parseMultiplicative();
3018
+ for (; ; ) {
3019
+ const tok = this.peek();
3020
+ if (tok?.type === "op" && (tok.value === "+" || tok.value === "-")) {
3021
+ this.next();
3022
+ const right = this.parseMultiplicative();
3023
+ left = tok.value === "+" ? this.add(left, right) : this.subtract(left, right);
3024
+ } else break;
3025
+ }
3026
+ return left;
3027
+ }
3028
+ add(l, r) {
3029
+ if (typeof l === "string" || typeof r === "string") {
3030
+ return String(l ?? "") + String(r ?? "");
3031
+ }
3032
+ return Number(l) + Number(r);
3033
+ }
3034
+ subtract(l, r) {
3035
+ return Number(l) - Number(r);
3036
+ }
3037
+ /* multiplicative := unary (('*' | '/') unary)* */
3038
+ parseMultiplicative() {
3039
+ let left = this.parseUnary();
3040
+ for (; ; ) {
3041
+ const tok = this.peek();
3042
+ if (tok?.type === "op" && (tok.value === "*" || tok.value === "/")) {
3043
+ this.next();
3044
+ const right = this.parseUnary();
3045
+ left = tok.value === "*" ? Number(left) * Number(right) : Number(left) / Number(right);
3046
+ } else break;
3047
+ }
3048
+ return left;
3049
+ }
3050
+ /* unary := ('+' | '-' | '!') unary | primary */
3051
+ parseUnary() {
3052
+ const tok = this.peek();
3053
+ if (tok?.type === "op" && (tok.value === "+" || tok.value === "-")) {
3054
+ this.next();
3055
+ const val = Number(this.parseUnary());
3056
+ return tok.value === "-" ? -val : val;
3057
+ }
3058
+ if (tok?.type === "op" && tok.value === "!") {
3059
+ this.next();
3060
+ return !this.parseUnary();
3061
+ }
3062
+ return this.parsePrimary();
3063
+ }
3064
+ /* primary := STRING | NUMBER | kw | id ( '.' id )* | '(' pipeExpr ')' */
3065
+ parsePrimary() {
3066
+ const tok = this.peek();
3067
+ if (!tok) return void 0;
3068
+ if (tok.type === "lparen") {
3069
+ this.next();
3070
+ const val = this.parsePipe();
3071
+ this.matchType("rparen");
3072
+ return val;
3073
+ }
3074
+ if (tok.type === "str") {
3075
+ this.next();
3076
+ return tok.value;
3077
+ }
3078
+ if (tok.type === "num") {
3079
+ this.next();
3080
+ return Number(tok.value);
3081
+ }
3082
+ if (tok.type === "kw") {
3083
+ this.next();
3084
+ return tok.value === "true" ? true : tok.value === "false" ? false : null;
3085
+ }
3086
+ if (tok.type === "id") {
3087
+ this.next();
3088
+ let result = this.resolveId(tok.value);
3089
+ while (this.peek()?.type === "dot") {
3090
+ this.next();
3091
+ const propTok = this.peek();
3092
+ if (propTok?.type === "id") {
3093
+ this.next();
3094
+ result = result == null ? void 0 : result[propTok.value];
3095
+ } else break;
3096
+ }
3097
+ return result;
3098
+ }
3099
+ this.next();
3100
+ return void 0;
3101
+ }
3102
+ resolveId(name) {
3103
+ switch (name) {
3104
+ case "value":
3105
+ return this.ctx.value;
3106
+ case "row":
3107
+ return this.ctx.row;
3108
+ case "true":
3109
+ return true;
3110
+ case "false":
3111
+ return false;
3112
+ case "null":
3113
+ return null;
3114
+ default:
3115
+ return this.ctx.row[name];
3116
+ }
3117
+ }
3118
+ applyPipe(name, arg, input) {
3119
+ const pipe = PIPES[name];
3120
+ if (!pipe) return formatDefault(input);
3121
+ try {
3122
+ return pipe(input, arg);
3123
+ } catch {
3124
+ return formatDefault(input);
3125
+ }
3126
+ }
3127
+ }
3128
+ function toComparable(v) {
3129
+ if (typeof v === "number") return v;
3130
+ if (typeof v === "boolean") return v ? 1 : 0;
3131
+ return String(v ?? "");
3132
+ }
3133
+ function formatDefault(v) {
3134
+ if (v == null) return "";
3135
+ if (typeof v === "object") return "[object Object]";
3136
+ return String(v);
3137
+ }
3138
+ function evaluate(expr, ctx) {
3139
+ try {
3140
+ const tokens = tokenize(expr);
3141
+ const parser = new Parser(tokens, ctx);
3142
+ const result = parser.parse();
3143
+ return formatDefault(result);
3144
+ } catch {
3145
+ return "";
3146
+ }
3147
+ }
3148
+ function renderCell(template, row, field) {
3149
+ const ctx = { value: row[field], row };
3150
+ return template.replace(/\{\{([^}]+)\}\}/g, (_, expr) => esc(evaluate(decodeEntities(expr.trim()), ctx)));
3151
+ }
3152
+ function decodeEntities(str) {
3153
+ return str.replace(/&amp;/g, "&").replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
3154
+ }
2643
3155
  const _FxTable = class _FxTable extends FxElement {
2644
3156
  constructor() {
2645
3157
  super(...arguments);
2646
- this.data = [];
3158
+ this._data = [];
2647
3159
  this.page = 0;
2648
3160
  this.sortField = "";
2649
3161
  this.sortDir = 1;
2650
3162
  this.filters = {};
3163
+ this.globalSearch = "";
2651
3164
  }
2652
3165
  static get observedAttributes() {
2653
- return ["pagination", "rows", "rows-options", "striped", "empty-message", "pagination-position"];
3166
+ return [
3167
+ "pagination",
3168
+ "rows",
3169
+ "rows-options",
3170
+ "striped",
3171
+ "empty-message",
3172
+ "pagination-position",
3173
+ "lazy",
3174
+ "total",
3175
+ "loading",
3176
+ "loading-message"
3177
+ ];
3178
+ }
3179
+ get data() {
3180
+ return this._data;
3181
+ }
3182
+ set data(value) {
3183
+ this._data = Array.isArray(value) ? value : [];
3184
+ this.render();
3185
+ }
3186
+ get total() {
3187
+ return this._total ?? this._data.length;
3188
+ }
3189
+ set total(value) {
3190
+ this._total = value;
3191
+ this.setAttribute("total", String(value));
2654
3192
  }
2655
3193
  get rowsPerPage() {
2656
3194
  const r = Number(this.getAttr("rows", "10"));
@@ -2659,57 +3197,101 @@ const _FxTable = class _FxTable extends FxElement {
2659
3197
  set rowsPerPage(value) {
2660
3198
  this.setAttribute("rows", String(value));
2661
3199
  }
2662
- /** Colunas declaradas no light DOM (<fx-column>). */
3200
+ get lazy() {
3201
+ return this.hasAttr("lazy");
3202
+ }
2663
3203
  get columns() {
2664
- return [...this.querySelectorAll("fx-column")].map((c) => ({
2665
- field: c.getAttribute("field") ?? "",
2666
- header: c.getAttribute("header") ?? c.getAttribute("field") ?? "",
2667
- sortable: c.hasAttribute("sortable"),
2668
- filterable: c.hasAttribute("filterable"),
2669
- align: c.getAttribute("align") ?? "left",
2670
- template: c.querySelector("template") ?? void 0
2671
- }));
3204
+ return [...this.querySelectorAll("fx-column")].map((c) => {
3205
+ const tpl = c.querySelector("template");
3206
+ const direct = tpl ? void 0 : c.innerHTML.trim();
3207
+ const align = c.getAttribute("align") ?? "left";
3208
+ return {
3209
+ field: c.getAttribute("field") ?? "",
3210
+ header: c.getAttribute("header") ?? c.getAttribute("field") ?? "",
3211
+ sortable: c.hasAttribute("sortable"),
3212
+ filterable: c.hasAttribute("filterable"),
3213
+ align: ["left", "center", "right", "justify", "start", "end"].includes(align) ? align : "left",
3214
+ template: tpl ?? void 0,
3215
+ directTemplate: direct || void 0
3216
+ };
3217
+ });
3218
+ }
3219
+ get toolbarTemplate() {
3220
+ return this.querySelector(
3221
+ 'template[slot="toolbar"]'
3222
+ );
2672
3223
  }
2673
3224
  connectedCallback() {
3225
+ this._parseDataAttribute();
2674
3226
  super.connectedCallback();
2675
- if (!this.data.length) {
3227
+ this._columnObserver = new MutationObserver(() => this.render());
3228
+ this._columnObserver.observe(this, { childList: true, subtree: true });
3229
+ }
3230
+ disconnectedCallback() {
3231
+ this._columnObserver?.disconnect();
3232
+ super.disconnectedCallback();
3233
+ }
3234
+ _parseDataAttribute() {
3235
+ if (!this._data.length) {
2676
3236
  try {
2677
3237
  const json = this.getAttr("data");
2678
- if (json) this.data = JSON.parse(json);
3238
+ if (json) this._data = JSON.parse(json);
2679
3239
  } catch {
2680
3240
  }
2681
3241
  }
2682
- new MutationObserver(() => this.render()).observe(this, { childList: true, subtree: true });
3242
+ const totalAttr = this.getAttr("total");
3243
+ if (totalAttr) {
3244
+ const n = Number(totalAttr);
3245
+ if (!isNaN(n)) this._total = n;
3246
+ }
2683
3247
  }
2684
- /** Pipeline: filtra → ordena → pagina. */
2685
3248
  computeRows() {
2686
- let out = [...this.data];
3249
+ let out = [...this._data];
2687
3250
  for (const [field, term] of Object.entries(this.filters)) {
2688
3251
  if (!term) continue;
2689
3252
  const t = term.toLowerCase();
2690
- out = out.filter((r) => String(r[field] ?? "").toLowerCase().includes(t));
3253
+ out = out.filter(
3254
+ (r) => String(r[field] ?? "").toLowerCase().includes(t)
3255
+ );
3256
+ }
3257
+ if (this.globalSearch) {
3258
+ const t = this.globalSearch.toLowerCase();
3259
+ const searchFields = this._getSearchFields();
3260
+ out = out.filter((r) => {
3261
+ const fields = searchFields.length ? searchFields : Object.keys(r);
3262
+ return fields.some(
3263
+ (f) => String(r[f] ?? "").toLowerCase().includes(t)
3264
+ );
3265
+ });
2691
3266
  }
2692
- const total = out.length;
3267
+ const total = this.lazy ? this.total : out.length;
2693
3268
  if (this.sortField) {
2694
3269
  out.sort((a, b) => {
2695
3270
  const av = a[this.sortField], bv = b[this.sortField];
2696
- if (typeof av === "number" && typeof bv === "number") return (av - bv) * this.sortDir;
3271
+ if (typeof av === "number" && typeof bv === "number")
3272
+ return (av - bv) * this.sortDir;
2697
3273
  return String(av ?? "").localeCompare(String(bv ?? ""), "pt-BR") * this.sortDir;
2698
3274
  });
2699
3275
  }
2700
- if (this.hasAttr("pagination")) {
3276
+ if (this.hasAttr("pagination") && !this.lazy) {
2701
3277
  const start = this.page * this.rowsPerPage;
2702
3278
  out = out.slice(start, start + this.rowsPerPage);
2703
3279
  }
2704
3280
  return { rows: out, total };
2705
3281
  }
2706
- /** Célula: template customizado ({{value}}, {{campo}}, {{row}}) ou valor direto. */
3282
+ _getSearchFields() {
3283
+ const input = this.root.querySelector(
3284
+ "[data-search-fields]"
3285
+ );
3286
+ if (!input) return [];
3287
+ const attr = input.getAttribute("data-search-fields");
3288
+ if (attr === null) return [];
3289
+ return attr.split(",").map((s) => s.trim()).filter(Boolean);
3290
+ }
2707
3291
  cellHtml(col, row) {
2708
- const value = row[col.field];
2709
- if (col.template) {
2710
- return col.template.innerHTML.replace(/\{\{(\w+)\}\}/g, (_, k) => k === "value" ? String(value ?? "") : k === "row" ? "" : String(row[k] ?? ""));
2711
- }
2712
- return String(value ?? "");
3292
+ const template = col.template?.innerHTML ?? col.directTemplate;
3293
+ if (template) return renderCell(template, row, col.field);
3294
+ return esc(String(row[col.field] ?? ""));
2713
3295
  }
2714
3296
  render() {
2715
3297
  const cols = this.columns.filter((c) => c.field);
@@ -2717,12 +3299,18 @@ const _FxTable = class _FxTable extends FxElement {
2717
3299
  const paginated = this.hasAttr("pagination");
2718
3300
  const pages = Math.max(1, Math.ceil(total / this.rowsPerPage));
2719
3301
  if (this.page >= pages) this.page = pages - 1;
3302
+ const toolbar = this.toolbarTemplate;
3303
+ const toolbarHtml = toolbar ? `<div class="toolbar" part="toolbar">${toolbar.innerHTML}</div>` : "";
2720
3304
  const headHtml = cols.map((c) => {
2721
- const ind = this.sortField === c.field ? this.sortDir === 1 ? "▲" : "▼" : "";
2722
- return `<th part="header" class="${c.sortable ? "sortable" : ""}" style="text-align:${c.align}" data-field="${c.field}">${c.header}${ind ? `<span class="sort-ind">${ind}</span>` : ""}</th>`;
3305
+ const isSorted = this.sortField === c.field;
3306
+ const ind = isSorted ? this.sortDir === 1 ? "▲" : "▼" : c.sortable ? "⇅" : "";
3307
+ const activeClass = isSorted ? "active" : "";
3308
+ return `<th part="header" class="${c.sortable ? "sortable" : ""}" style="text-align:${c.align}" data-field="${esc(c.field)}">${esc(c.header)}${ind ? `<span class="sort-ind ${activeClass}">${ind}</span>` : ""}</th>`;
2723
3309
  }).join("");
2724
- const filterRow = cols.some((c) => c.filterable) ? `<tr class="filter-row">${cols.map((c) => `<th style="text-align:${c.align}">${c.filterable ? `<input class="filter" data-field="${c.field}" placeholder="Filtrar…" value="${this.filters[c.field] ?? ""}">` : ""}</th>`).join("")}</tr>` : "";
2725
- const bodyHtml = rows.length ? rows.map((row, i) => `<tr part="row" data-index="${i}">${cols.map((c) => `<td part="cell" style="text-align:${c.align}">${this.cellHtml(c, row)}</td>`).join("")}</tr>`).join("") : `<tr><td class="empty" colspan="${cols.length || 1}">${this.getAttr("empty-message", "Nenhum registro encontrado")}</td></tr>`;
3310
+ const filterRow = cols.some((c) => c.filterable) ? `<tr class="filter-row">${cols.map((c) => `<th style="text-align:${c.align}">${c.filterable ? `<input class="filter" data-field="${esc(c.field)}" placeholder="${esc("Filtrar…")}" value="${esc(this.filters[c.field] ?? "")}">` : ""}</th>`).join("")}</tr>` : "";
3311
+ const bodyHtml = rows.length ? rows.map(
3312
+ (row, i) => `<tr part="row" data-index="${i}">${cols.map((c) => `<td part="cell" style="text-align:${c.align}">${this.cellHtml(c, row)}</td>`).join("")}</tr>`
3313
+ ).join("") : `<tr><td class="empty" colspan="${cols.length || 1}">${esc(this.getAttr("empty-message", "Nenhum registro encontrado"))}</td></tr>`;
2726
3314
  const pagerHtml = !paginated ? "" : `
2727
3315
  <div class="pager" part="pager">
2728
3316
  <span class="info">Página ${this.page + 1} de ${pages} · ${total} registros</span>
@@ -2731,43 +3319,69 @@ const _FxTable = class _FxTable extends FxElement {
2731
3319
  ${Array.from({ length: pages }, (_, p) => `<button type="button" class="pg-btn" data-pg="${p}" aria-current="${p === this.page}">${p + 1}</button>`).join("")}
2732
3320
  <button type="button" class="pg-btn" data-pg="next" ${this.page >= pages - 1 ? "disabled" : ""}>›</button>
2733
3321
  <button type="button" class="pg-btn" data-pg="last" ${this.page >= pages - 1 ? "disabled" : ""}>»</button>
2734
- <label class="info">Por página:
2735
- <select class="rows-sel">${this.getAttr("rows-options", "5,10,20,50").split(",").map((n) => n.trim()).map((n) => `<option value="${n}" ${Number(n) === this.rowsPerPage ? "selected" : ""}>${n}</option>`).join("")}</select>
3322
+ <label class="info">${esc("Por página:")}
3323
+ <fx-select class="rows-sel" size="sm" value="${this.rowsPerPage}" aria-label="Itens por página">${this.getAttr(
3324
+ "rows-options",
3325
+ "5,10,20,50"
3326
+ ).split(",").map((n) => n.trim()).map((n) => `<option value="${esc(n)}">${esc(n)}</option>`).join("")}</fx-select>
2736
3327
  </label>
2737
3328
  </div>`;
3329
+ const loading = this.hasAttr("loading");
3330
+ const loadingHtml = loading ? `<div class="loading-overlay" part="loading-overlay"><div class="tbl-spinner" part="spinner"></div><span class="loading-text">${esc(this.getAttr("loading-message", "Carregando…"))}</span></div>` : "";
2738
3331
  this.setTemplate(`
2739
- <div class="scroll">
2740
- <table part="table">
2741
- <thead><tr>${headHtml}</tr>${filterRow}</thead>
2742
- <tbody>${bodyHtml}</tbody>
2743
- </table>
3332
+ ${toolbarHtml}
3333
+ <div class="table-wrap">
3334
+ <div class="scroll">
3335
+ <table part="table">
3336
+ <thead><tr>${headHtml}</tr>${filterRow}</thead>
3337
+ <tbody>${bodyHtml}</tbody>
3338
+ </table>
3339
+ </div>
3340
+ ${loadingHtml}
2744
3341
  </div>
2745
3342
  ${pagerHtml}
2746
3343
  `);
2747
3344
  this.root.querySelectorAll("th.sortable").forEach((th) => {
2748
3345
  th.addEventListener("click", () => {
2749
3346
  const f = th.dataset.field;
2750
- if (this.sortField === f) this.sortDir = this.sortDir === 1 ? -1 : 1;
3347
+ if (this.sortField === f)
3348
+ this.sortDir = this.sortDir === 1 ? -1 : 1;
2751
3349
  else {
2752
3350
  this.sortField = f;
2753
3351
  this.sortDir = 1;
2754
3352
  }
2755
3353
  this.render();
2756
- this.dispatchEvent(new CustomEvent("sort-change", {
2757
- bubbles: true,
2758
- composed: true,
2759
- detail: { field: this.sortField, direction: this.sortDir === 1 ? "asc" : "desc" }
2760
- }));
3354
+ this.dispatchEvent(
3355
+ new CustomEvent("sort-change", {
3356
+ bubbles: true,
3357
+ composed: true,
3358
+ detail: {
3359
+ field: this.sortField,
3360
+ direction: this.sortDir === 1 ? "asc" : "desc",
3361
+ lazy: this.lazy
3362
+ }
3363
+ })
3364
+ );
2761
3365
  });
2762
3366
  });
2763
3367
  this.root.querySelectorAll(".filter").forEach((inp) => {
2764
3368
  inp.addEventListener("input", () => {
2765
- this.filters[inp.dataset.field] = inp.value;
2766
- this.page = 0;
2767
3369
  const field = inp.dataset.field;
3370
+ const value = inp.value;
2768
3371
  const pos = inp.selectionStart;
3372
+ this.filters[field] = value;
3373
+ this.page = 0;
2769
3374
  this.render();
2770
- const again = this.root.querySelector(`.filter[data-field="${field}"]`);
3375
+ this.dispatchEvent(
3376
+ new CustomEvent("filter-change", {
3377
+ bubbles: true,
3378
+ composed: true,
3379
+ detail: { field, value, lazy: this.lazy }
3380
+ })
3381
+ );
3382
+ const again = this.root.querySelector(
3383
+ `.filter[data-field="${esc(field)}"]`
3384
+ );
2771
3385
  if (again) {
2772
3386
  again.focus();
2773
3387
  again.setSelectionRange(pos, pos);
@@ -2775,117 +3389,274 @@ const _FxTable = class _FxTable extends FxElement {
2775
3389
  });
2776
3390
  inp.addEventListener("click", (e) => e.stopPropagation());
2777
3391
  });
3392
+ this.root.querySelectorAll("[data-search-fields]").forEach((inp) => {
3393
+ inp.addEventListener("input", () => {
3394
+ this.globalSearch = inp.value;
3395
+ this.page = 0;
3396
+ const pos = inp.selectionStart;
3397
+ this.render();
3398
+ const again = this.root.querySelector(
3399
+ "[data-search-fields]"
3400
+ );
3401
+ if (again) {
3402
+ again.focus();
3403
+ again.setSelectionRange(pos, pos);
3404
+ }
3405
+ });
3406
+ });
2778
3407
  this.root.querySelectorAll(".pg-btn").forEach((btn) => {
2779
3408
  btn.addEventListener("click", () => {
2780
3409
  const pg = btn.dataset.pg;
2781
3410
  this.page = pg === "first" ? 0 : pg === "prev" ? Math.max(0, this.page - 1) : pg === "next" ? Math.min(pages - 1, this.page + 1) : pg === "last" ? pages - 1 : Number(pg);
2782
3411
  this.render();
2783
- this.dispatchEvent(new CustomEvent("page-change", {
2784
- bubbles: true,
2785
- composed: true,
2786
- detail: { page: this.page + 1, pages, rowsPerPage: this.rowsPerPage, total }
2787
- }));
3412
+ this.dispatchEvent(
3413
+ new CustomEvent("page-change", {
3414
+ bubbles: true,
3415
+ composed: true,
3416
+ detail: {
3417
+ page: this.page + 1,
3418
+ pages,
3419
+ rowsPerPage: this.rowsPerPage,
3420
+ total,
3421
+ lazy: this.lazy
3422
+ }
3423
+ })
3424
+ );
2788
3425
  });
2789
3426
  });
2790
- this.root.querySelector(".rows-sel")?.addEventListener("change", (e) => {
2791
- this.rowsPerPage = Number(e.target.value);
3427
+ this.root.querySelector("fx-select.rows-sel")?.addEventListener("change", (e) => {
3428
+ const value = Number(e.detail?.value);
3429
+ if (!value || value === this.rowsPerPage) return;
3430
+ this.rowsPerPage = value;
2792
3431
  this.page = 0;
2793
3432
  this.render();
2794
3433
  });
2795
3434
  this.root.querySelectorAll("tbody tr[data-index]").forEach((tr) => {
2796
3435
  tr.addEventListener("click", () => {
2797
- this.dispatchEvent(new CustomEvent("row-click", {
2798
- bubbles: true,
2799
- composed: true,
2800
- detail: { row: rows[Number(tr.dataset.index)], index: Number(tr.dataset.index) }
2801
- }));
3436
+ this.dispatchEvent(
3437
+ new CustomEvent("row-click", {
3438
+ bubbles: true,
3439
+ composed: true,
3440
+ detail: {
3441
+ row: rows[Number(tr.dataset.index)],
3442
+ index: Number(tr.dataset.index)
3443
+ }
3444
+ })
3445
+ );
2802
3446
  });
2803
3447
  });
3448
+ this._restoreToolbarSearch();
3449
+ }
3450
+ _restoreToolbarSearch() {
3451
+ const input = this.root.querySelector(
3452
+ "[data-search-fields]"
3453
+ );
3454
+ if (input && this.globalSearch) input.value = this.globalSearch;
2804
3455
  }
2805
3456
  };
2806
3457
  _FxTable.styles = css`
2807
- :host {
2808
- display: block;
2809
- font-family: var(--fx-font-family);
2810
- font-size: var(--fx-font-size);
2811
- color: var(--fx-text-default);
2812
- }
2813
- .scroll { overflow-x: auto; border: 1px solid var(--fx-border-default); border-radius: var(--fx-radius-md); }
2814
- table { width: 100%; border-collapse: collapse; }
2815
- th {
2816
- text-align: left;
2817
- padding: var(--fx-space-sm) var(--fx-space-md);
2818
- background: var(--fx-surface-background);
2819
- border-bottom: 2px solid var(--fx-border-default);
2820
- white-space: nowrap;
2821
- user-select: none;
2822
- }
2823
- th.sortable { cursor: pointer; }
2824
- th.sortable:hover { color: var(--fx-color-primary); }
2825
- .sort-ind { font-size: calc(var(--fx-font-size) - 4px); margin-left: var(--fx-space-3xs, 2px); }
2826
- .filter-row th { padding: var(--fx-space-3xs, 4px) var(--fx-space-md) var(--fx-space-sm); border-bottom-width: 1px; }
2827
- .filter {
2828
- width: 100%;
2829
- box-sizing: border-box;
2830
- font: inherit;
2831
- font-size: calc(var(--fx-font-size) - 2px);
2832
- color: var(--fx-text-default);
2833
- background: var(--fx-surface-background);
2834
- border: 1px solid var(--fx-border-default);
2835
- border-radius: var(--fx-radius-sm);
2836
- padding: var(--fx-space-3xs, 4px) var(--fx-space-xs);
2837
- outline: none;
2838
- }
2839
- .filter:focus { border-color: var(--fx-color-primary); box-shadow: var(--fx-effect-focus-ring, none); }
2840
- td {
2841
- padding: var(--fx-space-sm) var(--fx-space-md);
2842
- border-bottom: 1px solid var(--fx-border-default);
2843
- }
2844
- tbody tr { cursor: pointer; transition: background-color var(--fx-motion-duration-fast) var(--fx-motion-easing); }
2845
- tbody tr:hover { background: color-mix(in srgb, var(--fx-color-primary) 8%, transparent); }
2846
- :host([striped]) tbody tr:nth-child(even) { background: var(--fx-surface-background); }
2847
- :host([striped]) tbody tr:hover { background: color-mix(in srgb, var(--fx-color-primary) 8%, transparent); }
2848
- .empty { text-align: center; padding: var(--fx-space-xl); color: var(--fx-text-muted); }
2849
- .pager {
2850
- display: flex;
2851
- align-items: center;
2852
- gap: var(--fx-space-sm);
2853
- flex-wrap: wrap;
2854
- padding: var(--fx-space-sm) 0;
2855
- }
2856
- :host([pagination-position='center']) .pager { justify-content: center; }
2857
- :host([pagination-position='right']) .pager { justify-content: flex-end; }
2858
- .pager .info { color: var(--fx-text-muted); font-size: calc(var(--fx-font-size) - 2px); }
2859
- .pg-btn {
2860
- font: inherit;
2861
- font-size: calc(var(--fx-font-size) - 2px);
2862
- color: var(--fx-text-default);
2863
- background: var(--fx-surface-background);
2864
- border: 1px solid var(--fx-border-default);
2865
- border-radius: var(--fx-radius-sm);
2866
- padding: var(--fx-space-3xs, 4px) var(--fx-space-sm);
2867
- cursor: pointer;
2868
- transition: border-color var(--fx-motion-duration-fast) var(--fx-motion-easing);
2869
- }
2870
- .pg-btn:hover:not(:disabled) { border-color: var(--fx-color-primary); color: var(--fx-color-primary); }
2871
- .pg-btn:disabled { opacity: 0.5; cursor: not-allowed; }
2872
- .pg-btn[aria-current='true'] {
2873
- background: var(--fx-color-primary);
2874
- border-color: var(--fx-color-primary);
2875
- color: #fff;
2876
- font-weight: var(--fx-font-weight);
2877
- }
2878
- .rows-sel {
2879
- font: inherit;
2880
- font-size: calc(var(--fx-font-size) - 2px);
2881
- color: var(--fx-text-default);
2882
- background: var(--fx-surface-background);
2883
- border: 1px solid var(--fx-border-default);
2884
- border-radius: var(--fx-radius-sm);
2885
- padding: 2px var(--fx-space-xs);
2886
- outline: none;
2887
- }
2888
- `;
3458
+ :host {
3459
+ display: block;
3460
+ font-family: var(--fx-font-family);
3461
+ font-size: var(--fx-font-size);
3462
+ color: var(--fx-text-default);
3463
+ background: var(--fx-surface-background);
3464
+ }
3465
+ .table-wrap {
3466
+ position: relative;
3467
+ }
3468
+ .scroll {
3469
+ overflow-x: auto;
3470
+ border: 1px solid var(--fx-border-default);
3471
+ border-radius: var(--fx-radius-md);
3472
+ }
3473
+ table {
3474
+ width: 100%;
3475
+ border-collapse: collapse;
3476
+ }
3477
+ th {
3478
+ text-align: left;
3479
+ padding: var(--fx-space-sm) var(--fx-space-md);
3480
+ background: var(--fx-surface-surface);
3481
+ border-bottom: 2px solid var(--fx-border-default);
3482
+ white-space: nowrap;
3483
+ user-select: none;
3484
+ }
3485
+ th.sortable {
3486
+ cursor: pointer;
3487
+ }
3488
+ th.sortable:hover {
3489
+ color: var(--fx-color-primary);
3490
+ }
3491
+ .sort-ind {
3492
+ font-size: calc(var(--fx-font-size) - 4px);
3493
+ margin-left: var(--fx-space-3xs, 2px);
3494
+ opacity: 0.45;
3495
+ transition: opacity var(--fx-motion-duration-fast)
3496
+ var(--fx-motion-easing);
3497
+ }
3498
+ th.sortable:hover .sort-ind {
3499
+ opacity: 0.85;
3500
+ }
3501
+ .sort-ind.active {
3502
+ opacity: 1;
3503
+ color: var(--fx-color-primary);
3504
+ }
3505
+ .filter-row th {
3506
+ padding: var(--fx-space-3xs, 4px) var(--fx-space-md) var(--fx-space-sm);
3507
+ border-bottom-width: 1px;
3508
+ }
3509
+ .filter {
3510
+ width: 100%;
3511
+ box-sizing: border-box;
3512
+ font: inherit;
3513
+ font-size: calc(var(--fx-font-size) - 2px);
3514
+ color: var(--fx-text-default);
3515
+ background: var(--fx-surface-background);
3516
+ border: 1px solid var(--fx-border-default);
3517
+ border-radius: var(--fx-radius-sm);
3518
+ padding: var(--fx-space-3xs, 4px) var(--fx-space-xs);
3519
+ outline: none;
3520
+ }
3521
+ .filter:focus {
3522
+ border-color: var(--fx-color-primary);
3523
+ box-shadow: var(--fx-effect-focus-ring, none);
3524
+ }
3525
+ td {
3526
+ padding: var(--fx-space-sm) var(--fx-space-md);
3527
+ border-bottom: 1px solid var(--fx-border-default);
3528
+ }
3529
+ tbody tr {
3530
+ cursor: pointer;
3531
+ transition: background-color var(--fx-motion-duration-fast)
3532
+ var(--fx-motion-easing);
3533
+ }
3534
+ tbody tr:hover {
3535
+ background: color-mix(
3536
+ in srgb,
3537
+ var(--fx-color-primary) 8%,
3538
+ transparent
3539
+ );
3540
+ }
3541
+ :host([striped]) tbody tr:nth-child(even) {
3542
+ background: var(--fx-surface-background);
3543
+ }
3544
+ :host([striped]) tbody tr:hover {
3545
+ background: color-mix(
3546
+ in srgb,
3547
+ var(--fx-color-primary) 8%,
3548
+ transparent
3549
+ );
3550
+ }
3551
+ .empty {
3552
+ text-align: center;
3553
+ padding: var(--fx-space-xl);
3554
+ color: var(--fx-text-muted);
3555
+ }
3556
+ .pager {
3557
+ display: flex;
3558
+ align-items: center;
3559
+ gap: var(--fx-space-sm);
3560
+ flex-wrap: wrap;
3561
+ padding: var(--fx-space-sm) 0;
3562
+ }
3563
+ :host([pagination-position="center"]) .pager {
3564
+ justify-content: center;
3565
+ }
3566
+ :host([pagination-position="right"]) .pager {
3567
+ justify-content: flex-end;
3568
+ }
3569
+ .pager .info {
3570
+ color: var(--fx-text-muted);
3571
+ font-size: calc(var(--fx-font-size) - 2px);
3572
+ }
3573
+ .pg-btn {
3574
+ min-width: var(--fx-size-sm);
3575
+ height: var(--fx-size-sm);
3576
+ font: inherit;
3577
+ font-size: calc(var(--fx-font-size) - 2px);
3578
+ color: var(--fx-text-default);
3579
+ background: var(--fx-surface-background);
3580
+ border: 1px solid var(--fx-border-default);
3581
+ border-radius: var(--fx-radius-sm);
3582
+ padding: var(--fx-space-3xs, 4px) var(--fx-space-sm);
3583
+ cursor: pointer;
3584
+ transition: border-color var(--fx-motion-duration-fast)
3585
+ var(--fx-motion-easing);
3586
+ }
3587
+ .pg-btn:hover:not(:disabled):not([aria-current="true"]) {
3588
+ border-color: var(--fx-color-primary);
3589
+ color: var(--fx-color-primary);
3590
+ }
3591
+ .pg-btn:disabled {
3592
+ opacity: 0.5;
3593
+ cursor: not-allowed;
3594
+ }
3595
+ .pg-btn[aria-current="true"] {
3596
+ background: var(--fx-color-primary);
3597
+ border-color: var(--fx-color-primary);
3598
+ color: #fff;
3599
+ font-weight: var(--fx-font-weight);
3600
+ }
3601
+ fx-select {
3602
+ vertical-align: middle;
3603
+ }
3604
+ fx-select::part(trigger) {
3605
+ min-width: 64px !important;
3606
+ min-height: var(--fx-size-sm) !important;
3607
+ padding: 0 var(--fx-space-sm) !important;
3608
+ border-radius: var(--fx-radius-sm) !important;
3609
+ font-size: calc(var(--fx-font-size) - 2px) !important;
3610
+ }
3611
+ .toolbar {
3612
+ display: flex;
3613
+ align-items: center;
3614
+ gap: var(--fx-space-sm);
3615
+ flex-wrap: wrap;
3616
+ padding: var(--fx-space-sm) var(--fx-space-md);
3617
+ border: 1px solid var(--fx-border-default);
3618
+ border-bottom: none;
3619
+ border-radius: var(--fx-radius-md) var(--fx-radius-md) 0 0;
3620
+ background: var(--fx-surface-background);
3621
+ }
3622
+ .toolbar:empty {
3623
+ display: none;
3624
+ }
3625
+ .loading-overlay {
3626
+ position: absolute;
3627
+ inset: 0;
3628
+ display: flex;
3629
+ flex-direction: column;
3630
+ align-items: center;
3631
+ justify-content: center;
3632
+ gap: var(--fx-space-sm);
3633
+ background: color-mix(
3634
+ in srgb,
3635
+ var(--fx-surface-background) 85%,
3636
+ transparent
3637
+ );
3638
+ backdrop-filter: blur(2px);
3639
+ z-index: 10;
3640
+ border-radius: var(--fx-radius-md);
3641
+ }
3642
+ .tbl-spinner {
3643
+ width: 32px;
3644
+ height: 32px;
3645
+ border: 3px solid var(--fx-border-default);
3646
+ border-top-color: var(--fx-color-primary);
3647
+ border-radius: 50%;
3648
+ animation: tbl-spin 1s linear infinite;
3649
+ }
3650
+ @keyframes tbl-spin {
3651
+ to {
3652
+ transform: rotate(360deg);
3653
+ }
3654
+ }
3655
+ .loading-text {
3656
+ color: var(--fx-text-muted);
3657
+ font-size: calc(var(--fx-font-size) - 1px);
3658
+ }
3659
+ `;
2889
3660
  let FxTable = _FxTable;
2890
3661
  function defineFxTable() {
2891
3662
  return defineElement("fx-table", FxTable);
@@ -3178,9 +3949,9 @@ const _FxTextarea = class _FxTextarea extends FxElement {
3178
3949
  const rows = this.getAttr("rows");
3179
3950
  const maxlength = this.getAttr("maxlength");
3180
3951
  this.setTemplate(`<textarea class="field" part="textarea"
3181
- ${placeholder ? `placeholder="${placeholder}"` : ""}
3182
- ${rows ? `rows="${rows}"` : ""}
3183
- ${maxlength ? `maxlength="${maxlength}"` : ""}
3952
+ ${placeholder ? `placeholder="${esc(placeholder)}"` : ""}
3953
+ ${rows ? `rows="${esc(rows)}"` : ""}
3954
+ ${maxlength ? `maxlength="${esc(maxlength)}"` : ""}
3184
3955
  ></textarea>`);
3185
3956
  const field = this.root.querySelector(".field");
3186
3957
  if (!field) return;
@@ -3257,6 +4028,10 @@ function defineFxTextarea() {
3257
4028
  }
3258
4029
  defineFxTextarea();
3259
4030
  const _FxDialog = class _FxDialog extends FxElement {
4031
+ constructor() {
4032
+ super(...arguments);
4033
+ this._previouslyFocused = null;
4034
+ }
3260
4035
  static get observedAttributes() {
3261
4036
  return ["open", "size", "heading"];
3262
4037
  }
@@ -3271,9 +4046,9 @@ const _FxDialog = class _FxDialog extends FxElement {
3271
4046
  const heading = this.getAttr("heading");
3272
4047
  this.setTemplate(`
3273
4048
  <div class="overlay" part="overlay" ${isOpen ? "" : "hidden"}>
3274
- <div class="dialog" role="dialog" aria-modal="true" aria-label="${heading}">
4049
+ <div class="dialog" role="dialog" aria-modal="true" aria-label="${esc(heading)}" tabindex="-1">
3275
4050
  <header>
3276
- <h2>${heading}</h2>
4051
+ <h2>${esc(heading)}</h2>
3277
4052
  <button type="button" class="close" part="close" aria-label="Fechar">×</button>
3278
4053
  </header>
3279
4054
  <div class="body"><slot></slot></div>
@@ -3286,6 +4061,9 @@ const _FxDialog = class _FxDialog extends FxElement {
3286
4061
  const closeBtn = this.root.querySelector(".close");
3287
4062
  const close = () => {
3288
4063
  this.open = false;
4064
+ this._cleanup?.();
4065
+ this._cleanup = void 0;
4066
+ this._restoreFocus();
3289
4067
  this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
3290
4068
  };
3291
4069
  overlay?.addEventListener("click", (e) => {
@@ -3296,16 +4074,46 @@ const _FxDialog = class _FxDialog extends FxElement {
3296
4074
  if (e.key === "Escape") {
3297
4075
  e.stopPropagation();
3298
4076
  close();
4077
+ return;
3299
4078
  }
4079
+ if (e.key === "Tab") this._trapFocus(e);
3300
4080
  };
3301
- document.addEventListener("keydown", onKey, { once: true });
4081
+ document.addEventListener("keydown", onKey);
3302
4082
  this._cleanup = () => document.removeEventListener("keydown", onKey);
3303
- requestAnimationFrame(() => this.root.querySelector(".dialog")?.focus());
4083
+ requestAnimationFrame(() => {
4084
+ this._previouslyFocused = document.activeElement;
4085
+ this.root.querySelector(".dialog")?.focus();
4086
+ });
3304
4087
  this.dispatchEvent(new CustomEvent("open", { bubbles: true, composed: true }));
3305
4088
  }
4089
+ /** Mantém o foco dentro do modal (WCAG 2.4.3 / 2.1.2). */
4090
+ _trapFocus(e) {
4091
+ const dialog = this.root.querySelector(".dialog");
4092
+ if (!dialog) return;
4093
+ const focusables = dialog.querySelectorAll(
4094
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
4095
+ );
4096
+ if (!focusables.length) return;
4097
+ const first = focusables[0];
4098
+ const last = focusables[focusables.length - 1];
4099
+ const active = this.root.activeElement;
4100
+ if (e.shiftKey && (active === first || active === dialog)) {
4101
+ e.preventDefault();
4102
+ last.focus();
4103
+ } else if (!e.shiftKey && active === last) {
4104
+ e.preventDefault();
4105
+ first.focus();
4106
+ }
4107
+ }
4108
+ _restoreFocus() {
4109
+ if (this._previouslyFocused?.isConnected) this._previouslyFocused.focus();
4110
+ this._previouslyFocused = null;
4111
+ }
3306
4112
  disconnectedCallback() {
3307
4113
  super.disconnectedCallback();
3308
4114
  this._cleanup?.();
4115
+ this._cleanup = void 0;
4116
+ this._restoreFocus();
3309
4117
  }
3310
4118
  };
3311
4119
  _FxDialog.styles = css`
@@ -3555,7 +4363,7 @@ const _FxTooltip = class _FxTooltip extends FxElement {
3555
4363
  const content = this.getAttr("content");
3556
4364
  this.setTemplate(`
3557
4365
  <slot></slot>
3558
- <span class="bubble" part="bubble" role="tooltip">${content}</span>
4366
+ <span class="bubble" part="bubble" role="tooltip">${esc(content)}</span>
3559
4367
  `);
3560
4368
  }
3561
4369
  };
@@ -3934,8 +4742,8 @@ const _FxTabs = class _FxTabs extends FxElement {
3934
4742
  const tabId = t.getAttribute("tab") ?? "";
3935
4743
  const disabled = t.hasAttribute("disabled");
3936
4744
  return `<button type="button" class="tab" role="tab" part="tab"
3937
- data-tab="${tabId}" aria-selected="${tabId === active}"
3938
- ${disabled ? 'aria-disabled="true" data-disabled="true"' : ""}>${t.textContent?.trim()}</button>`;
4745
+ data-tab="${esc(tabId)}" aria-selected="${tabId === active}"
4746
+ ${disabled ? 'aria-disabled="true" data-disabled="true"' : ""}>${esc(t.textContent?.trim() ?? "")}</button>`;
3939
4747
  }).join("")}
3940
4748
  </div>
3941
4749
  <slot></slot>
@@ -4095,7 +4903,7 @@ const _FxProgress = class _FxProgress extends FxElement {
4095
4903
  const labelText = this.getAttr("label");
4096
4904
  const showPct = !indeterminate && !this.hasAttr("hide-label");
4097
4905
  this.setTemplate(`
4098
- ${labelText ? `<div class="caption" part="caption">${labelText}</div>` : ""}
4906
+ ${labelText ? `<div class="caption" part="caption">${esc(labelText)}</div>` : ""}
4099
4907
  <div class="row">
4100
4908
  <div class="track" part="track" role="progressbar"
4101
4909
  aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100">
@@ -4171,10 +4979,15 @@ const _FxSkeleton = class _FxSkeleton extends FxElement {
4171
4979
  static get observedAttributes() {
4172
4980
  return ["variant", "width", "height", "lines"];
4173
4981
  }
4982
+ /** Valida um valor de dimensão CSS, retornando-o seguro ou vazio. */
4983
+ safeSize(value) {
4984
+ const v = value.trim();
4985
+ return /^-?\d*\.?\d+(px|em|rem|%|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc)$/.test(v) ? v : "";
4986
+ }
4174
4987
  render() {
4175
4988
  const variant = this.getAttr("variant", "text");
4176
- const width = this.getAttr("width");
4177
- const height = this.getAttr("height");
4989
+ const width = this.safeSize(this.getAttr("width"));
4990
+ const height = this.safeSize(this.getAttr("height"));
4178
4991
  const style = `${width ? `width:${width};` : ""}${height ? `height:${height};` : ""}`;
4179
4992
  if (variant === "circle") {
4180
4993
  const size = height || width || "40px";
@@ -4234,7 +5047,7 @@ const _FxAlert = class _FxAlert extends FxElement {
4234
5047
  <div class="alert" part="alert" role="alert">
4235
5048
  <span class="icon">${icons[variant] ?? "ℹ"}</span>
4236
5049
  <div class="content">
4237
- ${title ? `<div class="title">${title}</div>` : ""}
5050
+ ${title ? `<div class="title">${esc(title)}</div>` : ""}
4238
5051
  <div class="body"><slot></slot></div>
4239
5052
  </div>
4240
5053
  ${this.hasAttr("dismissible") ? '<button type="button" class="close" aria-label="Fechar">×</button>' : ""}
@@ -4286,6 +5099,10 @@ function defineFxAlert() {
4286
5099
  }
4287
5100
  defineFxAlert();
4288
5101
  const _FxDropdown = class _FxDropdown extends FxElement {
5102
+ constructor() {
5103
+ super(...arguments);
5104
+ this._listenersAttached = false;
5105
+ }
4289
5106
  static get observedAttributes() {
4290
5107
  return ["label", "position", "open"];
4291
5108
  }
@@ -4301,7 +5118,7 @@ const _FxDropdown = class _FxDropdown extends FxElement {
4301
5118
  this.setTemplate(`
4302
5119
  <span class="trigger" role="button" tabindex="0" part="trigger"
4303
5120
  aria-haspopup="menu" aria-expanded="${this.open}">
4304
- ${label} ▾
5121
+ ${esc(label)} ▾
4305
5122
  </span>
4306
5123
  <div class="panel" role="menu" part="panel">
4307
5124
  ${items.length ? "" : '<div class="empty">Nenhuma ação</div>'}
@@ -4320,18 +5137,37 @@ const _FxDropdown = class _FxDropdown extends FxElement {
4320
5137
  if (e.key === "Enter" || e.key === " ") {
4321
5138
  e.preventDefault();
4322
5139
  this.open = !this.open;
5140
+ } else if (e.key === "Escape") {
5141
+ this.open = false;
4323
5142
  }
4324
5143
  });
4325
- items.forEach((item) => {
4326
- item.addEventListener("click", () => {
4327
- this.open = false;
4328
- this.dispatchEvent(new CustomEvent("select", {
4329
- bubbles: true,
4330
- composed: true,
4331
- detail: { value: item.getAttribute("value") ?? "" }
4332
- }));
4333
- });
5144
+ this._attachDelegatedListeners();
5145
+ }
5146
+ /** Delegação de cliques nos itens + fechamento ao clicar fora (registrado uma vez). */
5147
+ _attachDelegatedListeners() {
5148
+ if (this._listenersAttached) return;
5149
+ this._listenersAttached = true;
5150
+ this.addEventListener("click", (e) => {
5151
+ const item = e.composedPath().find(
5152
+ (n) => n.tagName?.toLowerCase() === "fx-dropdown-item"
5153
+ );
5154
+ if (!item) return;
5155
+ this.open = false;
5156
+ this.dispatchEvent(new CustomEvent("select", {
5157
+ bubbles: true,
5158
+ composed: true,
5159
+ detail: { value: item.getAttribute("value") ?? "" }
5160
+ }));
4334
5161
  });
5162
+ this.docListener = (e) => {
5163
+ if (!this.contains(e.target)) this.open = false;
5164
+ };
5165
+ document.addEventListener("click", this.docListener);
5166
+ }
5167
+ disconnectedCallback() {
5168
+ super.disconnectedCallback();
5169
+ if (this.docListener) document.removeEventListener("click", this.docListener);
5170
+ this.docListener = void 0;
4335
5171
  }
4336
5172
  };
4337
5173
  _FxDropdown.styles = css`
@@ -4419,6 +5255,15 @@ const _FxDrawer = class _FxDrawer extends FxElement {
4419
5255
  super(...arguments);
4420
5256
  this._onKeydown = () => {
4421
5257
  };
5258
+ this._previouslyFocused = null;
5259
+ this._close = () => {
5260
+ if (!this.open) return;
5261
+ this.open = false;
5262
+ this._restoreFocus();
5263
+ this.dispatchEvent(
5264
+ new CustomEvent("close", { bubbles: true, composed: true })
5265
+ );
5266
+ };
4422
5267
  }
4423
5268
  static get observedAttributes() {
4424
5269
  return ["open", "title", "position"];
@@ -4446,37 +5291,61 @@ const _FxDrawer = class _FxDrawer extends FxElement {
4446
5291
  const title = this.getAttr("title");
4447
5292
  this.setTemplate(`
4448
5293
  <div class="overlay" part="overlay"></div>
4449
- <div class="panel" part="panel" role="dialog" aria-modal="true" aria-label="${title || "Drawer"}">
4450
- ${title !== "" ? `<div class="header" part="header"><span class="title">${title}</span><button type="button" class="close" part="close" aria-label="Fechar">✕</button></div>` : ""}
5294
+ <div class="panel" part="panel" role="dialog" aria-modal="true" aria-label="${esc(title || "Drawer")}" tabindex="-1">
5295
+ ${title !== "" ? `<div class="header" part="header"><span class="title">${esc(title)}</span><button type="button" class="close" part="close" aria-label="Fechar">✕</button></div>` : ""}
4451
5296
  <div class="body" part="body"><slot></slot></div>
4452
5297
  </div>
4453
5298
  `);
4454
- const close = () => {
4455
- if (!this.open) return;
4456
- this.open = false;
4457
- this.dispatchEvent(
4458
- new CustomEvent("close", { bubbles: true, composed: true })
4459
- );
4460
- };
4461
- this.root.querySelector(".overlay")?.addEventListener("click", close);
4462
- this.root.querySelector(".close")?.addEventListener("click", close);
5299
+ this.root.querySelector(".overlay")?.addEventListener("click", this._close);
5300
+ this.root.querySelector(".close")?.addEventListener("click", this._close);
5301
+ if (this.open) {
5302
+ requestAnimationFrame(() => {
5303
+ if (this._previouslyFocused === null) {
5304
+ this._previouslyFocused = document.activeElement;
5305
+ }
5306
+ this.root.querySelector(".panel")?.focus();
5307
+ });
5308
+ }
4463
5309
  }
4464
5310
  connectedCallback() {
4465
5311
  super.connectedCallback?.();
4466
5312
  this._onKeydown = (e) => {
4467
5313
  if (e.key === "Escape" && this.open) {
4468
- this.open = false;
4469
- this.dispatchEvent(
4470
- new CustomEvent("close", { bubbles: true, composed: true })
4471
- );
5314
+ this._close();
5315
+ } else if (e.key === "Tab" && this.open) {
5316
+ this._trapFocus(e);
4472
5317
  }
4473
5318
  };
4474
5319
  document.addEventListener("keydown", this._onKeydown);
4475
5320
  }
4476
5321
  disconnectedCallback() {
4477
5322
  document.removeEventListener("keydown", this._onKeydown);
5323
+ this._restoreFocus();
4478
5324
  super.disconnectedCallback?.();
4479
5325
  }
5326
+ /** Mantém o foco dentro do drawer (WCAG 2.4.3 / 2.1.2). */
5327
+ _trapFocus(e) {
5328
+ const panel = this.root.querySelector(".panel");
5329
+ if (!panel) return;
5330
+ const focusables = panel.querySelectorAll(
5331
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
5332
+ );
5333
+ if (!focusables.length) return;
5334
+ const first = focusables[0];
5335
+ const last = focusables[focusables.length - 1];
5336
+ const active = this.root.activeElement;
5337
+ if (e.shiftKey && (active === first || active === panel)) {
5338
+ e.preventDefault();
5339
+ last.focus();
5340
+ } else if (!e.shiftKey && active === last) {
5341
+ e.preventDefault();
5342
+ first.focus();
5343
+ }
5344
+ }
5345
+ _restoreFocus() {
5346
+ if (this._previouslyFocused?.isConnected) this._previouslyFocused.focus();
5347
+ this._previouslyFocused = null;
5348
+ }
4480
5349
  };
4481
5350
  _FxDrawer.styles = css`
4482
5351
  :host {
@@ -4643,9 +5512,9 @@ const _FxPagination = class _FxPagination extends FxElement {
4643
5512
  ${window_[window_.length - 1] !== pages && pages > 5 ? `<button type="button" class="nav" data-go="${pages}">${pages}</button>` : ""}
4644
5513
  <button type="button" class="nav" part="next" data-go="${current + 1}" ${current >= pages ? "disabled" : ""}>›</button>
4645
5514
  ${opts.length ? `
4646
- <select class="nav" part="rows" aria-label="Itens por página">
4647
- ${opts.map((o) => `<option value="${o}" ${Number(o) === this.rows ? "selected" : ""}>${o}</option>`).join("")}
4648
- </select>` : ""}
5515
+ <fx-select class="rows-sel" part="rows" size="sm" value="${this.rows}" aria-label="Itens por página">
5516
+ ${opts.map((o) => `<option value="${esc(o)}">${esc(o)}</option>`).join("")}
5517
+ </fx-select>` : ""}
4649
5518
  `);
4650
5519
  function total0(page, rows) {
4651
5520
  return (page - 1) * rows + 1;
@@ -4659,9 +5528,11 @@ const _FxPagination = class _FxPagination extends FxElement {
4659
5528
  }
4660
5529
  });
4661
5530
  });
4662
- const sel = this.root.querySelector("select.nav");
4663
- sel?.addEventListener("change", () => {
4664
- this.rows = Number(sel.value);
5531
+ const sel = this.root.querySelector("fx-select.rows-sel");
5532
+ sel?.addEventListener("change", (e) => {
5533
+ const value = Number(e.detail?.value);
5534
+ if (!value || value === this.rows) return;
5535
+ this.rows = value;
4665
5536
  this.page = 1;
4666
5537
  this.emit();
4667
5538
  });
@@ -4700,7 +5571,7 @@ _FxPagination.styles = css`
4700
5571
  border-color var(--fx-motion-duration-fast) var(--fx-motion-easing),
4701
5572
  background var(--fx-motion-duration-fast) var(--fx-motion-easing);
4702
5573
  }
4703
- .nav:hover:not([disabled]) { border-color: var(--fx-color-primary); color: var(--fx-color-primary); }
5574
+ .nav:hover:not([disabled]):not(.active) { border-color: var(--fx-color-primary); color: var(--fx-color-primary); }
4704
5575
  .nav[disabled] { opacity: 0.5; cursor: not-allowed; }
4705
5576
  .nav.active {
4706
5577
  background: var(--fx-color-primary);
@@ -4709,7 +5580,14 @@ _FxPagination.styles = css`
4709
5580
  font-weight: 600;
4710
5581
  }
4711
5582
  .info { color: var(--fx-text-muted); font-size: calc(var(--fx-font-size) - 2px); }
4712
- select.nav { cursor: pointer; }
5583
+ fx-select { vertical-align: middle; }
5584
+ fx-select::part(trigger) {
5585
+ min-width: 64px !important;
5586
+ min-height: var(--fx-size-sm) !important;
5587
+ padding: 0 var(--fx-space-sm) !important;
5588
+ border-radius: var(--fx-radius-sm) !important;
5589
+ font-size: calc(var(--fx-font-size) - 2px) !important;
5590
+ }
4713
5591
  `;
4714
5592
  let FxPagination = _FxPagination;
4715
5593
  function defineFxPagination() {
@@ -4746,11 +5624,23 @@ const _FxAutocomplete = class _FxAutocomplete extends FxElement {
4746
5624
  set source(value) {
4747
5625
  this.setAttribute("source", typeof value === "string" ? value : JSON.stringify(value));
4748
5626
  }
5627
+ connectedCallback() {
5628
+ super.connectedCallback();
5629
+ this.docListener = (e) => {
5630
+ if (!this.contains(e.target)) this.toggleAttr("open", false);
5631
+ };
5632
+ document.addEventListener("click", this.docListener);
5633
+ }
5634
+ disconnectedCallback() {
5635
+ super.disconnectedCallback();
5636
+ if (this.docListener) document.removeEventListener("click", this.docListener);
5637
+ this.docListener = void 0;
5638
+ }
4749
5639
  render() {
4750
5640
  const placeholder = this.getAttr("placeholder");
4751
5641
  this.setTemplate(`
4752
5642
  <input class="field" part="input" type="text"
4753
- ${placeholder ? `placeholder="${placeholder}"` : ""} autocomplete="off"/>
5643
+ ${placeholder ? `placeholder="${esc(placeholder)}"` : ""} autocomplete="off"/>
4754
5644
  <div class="list" part="list" role="listbox"></div>
4755
5645
  `);
4756
5646
  const field = this.root.querySelector(".field");
@@ -4763,7 +5653,7 @@ const _FxAutocomplete = class _FxAutocomplete extends FxElement {
4763
5653
  if (!list) return;
4764
5654
  const q = field.value.toLowerCase();
4765
5655
  const matches = this.source.filter((s) => s.toLowerCase().includes(q)).slice(0, 8);
4766
- list.innerHTML = matches.length ? matches.map((m) => `<button type="button" class="opt" role="option" data-v="${m}">${m}</button>`).join("") : '<div class="empty">Nenhum resultado</div>';
5656
+ list.innerHTML = matches.length ? matches.map((m) => `<button type="button" class="opt" role="option" data-v="${esc(m)}">${esc(m)}</button>`).join("") : '<div class="empty">Nenhum resultado</div>';
4767
5657
  list.querySelectorAll(".opt").forEach((btn) => {
4768
5658
  btn.addEventListener("click", () => {
4769
5659
  field.value = btn.dataset.v ?? "";
@@ -4785,8 +5675,19 @@ const _FxAutocomplete = class _FxAutocomplete extends FxElement {
4785
5675
  field.addEventListener("focus", () => {
4786
5676
  if (field.value.length >= minChars) openList();
4787
5677
  });
4788
- document.addEventListener("click", (e) => {
4789
- if (!this.contains(e.target)) this.toggleAttr("open", false);
5678
+ field.addEventListener("keydown", (e) => {
5679
+ if (e.key === "Escape") {
5680
+ this.toggleAttr("open", false);
5681
+ return;
5682
+ }
5683
+ if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
5684
+ e.preventDefault();
5685
+ if (!this.hasAttr("open")) openList();
5686
+ const opts = Array.from(this.root.querySelectorAll(".opt"));
5687
+ if (!opts.length) return;
5688
+ const current = opts.indexOf(this.root.activeElement);
5689
+ const next = e.key === "ArrowDown" ? current === -1 ? 0 : Math.min(current + 1, opts.length - 1) : current === -1 ? opts.length - 1 : Math.max(current - 1, 0);
5690
+ opts[next]?.focus();
4790
5691
  });
4791
5692
  }
4792
5693
  };
@@ -4981,8 +5882,10 @@ const _FxKnob = class _FxKnob extends FxElement {
4981
5882
  const radius = (sizeNum - strokeW) / 2;
4982
5883
  const circumference = 2 * Math.PI * radius;
4983
5884
  const offset = circumference * (1 - pct);
4984
- const valueColorStyle = this.valueColor ? `--_value-color: ${this.valueColor};` : "";
4985
- const rangeColorStyle = this.rangeColor ? `--_range-color: ${this.rangeColor};` : "";
5885
+ const vc = this.valueColor ? this.safeColor(this.valueColor) : "";
5886
+ const rc = this.rangeColor ? this.safeColor(this.rangeColor) : "";
5887
+ const valueColorStyle = vc ? `--_value-color: ${vc};` : "";
5888
+ const rangeColorStyle = rc ? `--_range-color: ${rc};` : "";
4986
5889
  const displayValue = this.valueTemplate.replace("{value}", String(Math.round(value)));
4987
5890
  this.setTemplate(`
4988
5891
  <div class="knob" part="knob" tabindex="${this.disabled ? "-1" : "0"}"
@@ -4997,7 +5900,7 @@ const _FxKnob = class _FxKnob extends FxElement {
4997
5900
  stroke-dasharray="${circumference}"
4998
5901
  stroke-dashoffset="${offset}" />
4999
5902
  </svg>
5000
- <div class="label" part="label">${displayValue}</div>
5903
+ <div class="label" part="label">${esc(displayValue)}</div>
5001
5904
  </div>
5002
5905
  `);
5003
5906
  this.attachListeners();
@@ -5008,6 +5911,15 @@ const _FxKnob = class _FxKnob extends FxElement {
5008
5911
  if (s === "lg") return 140;
5009
5912
  return 100;
5010
5913
  }
5914
+ /** Valida um valor de cor CSS, retornando-o seguro ou vazio. */
5915
+ safeColor(value) {
5916
+ const v = value.trim();
5917
+ if (/^#[0-9a-fA-F]{3,8}$/.test(v)) return v;
5918
+ if (/^(rgb|rgba|hsl|hsla)\([\d\s.,%()]+\)$/i.test(v)) return v;
5919
+ if (/^var\(--[\w-]+(,\s*[^)]*)?\)$/.test(v)) return v;
5920
+ if (/^[a-zA-Z]+$/.test(v)) return v;
5921
+ return "";
5922
+ }
5011
5923
  clampValue(v) {
5012
5924
  const min = this.min;
5013
5925
  const max = this.max;
@@ -5421,7 +6333,7 @@ const _FxOrderList = class _FxOrderList extends FxElement {
5421
6333
  <div class="filter-wrapper" part="filter-wrapper">
5422
6334
  <input type="text" class="filter-input" part="filter-input"
5423
6335
  placeholder="${this.filterPlaceholder}"
5424
- value="${esc$1(this._filterValue)}"
6336
+ value="${esc(this._filterValue)}"
5425
6337
  aria-label="Filtrar lista" />
5426
6338
  </div>
5427
6339
  ` : ""}
@@ -5467,10 +6379,10 @@ const _FxOrderList = class _FxOrderList extends FxElement {
5467
6379
  const key2 = this._getItemKey(item, index);
5468
6380
  const label = this._getItemLabel(item);
5469
6381
  const isSelected = this._isSelected(item);
5470
- const content = this._optionTemplate ? this._optionTemplate(item) : esc$1(label);
6382
+ const content = this._optionTemplate ? this._optionTemplate(item) : esc(label);
5471
6383
  return `
5472
6384
  <li class="list-item${isSelected ? " selected" : ""}" part="list-item"
5473
- data-index="${index}" data-key="${esc$1(key2)}"
6385
+ data-index="${index}" data-key="${esc(key2)}"
5474
6386
  role="option" aria-selected="${isSelected}"
5475
6387
  ${this.dragdrop ? 'draggable="true"' : ""}
5476
6388
  tabindex="0">
@@ -5478,7 +6390,7 @@ const _FxOrderList = class _FxOrderList extends FxElement {
5478
6390
  ${hasSelection ? `
5479
6391
  <input type="checkbox" class="checkbox" part="checkbox"
5480
6392
  ${isSelected ? "checked" : ""}
5481
- aria-label="Selecionar ${esc$1(label)}" />
6393
+ aria-label="Selecionar ${esc(label)}" />
5482
6394
  ` : ""}
5483
6395
  <span class="item-content" part="item-content">${content}</span>
5484
6396
  </li>
@@ -5626,10 +6538,10 @@ const _FxOrderList = class _FxOrderList extends FxElement {
5626
6538
  const key2 = this._getItemKey(item, index);
5627
6539
  const label = this._getItemLabel(item);
5628
6540
  const isSelected = this._isSelected(item);
5629
- const content = this._optionTemplate ? this._optionTemplate(item) : esc$1(label);
6541
+ const content = this._optionTemplate ? this._optionTemplate(item) : esc(label);
5630
6542
  return `
5631
6543
  <li class="list-item${isSelected ? " selected" : ""}" part="list-item"
5632
- data-index="${index}" data-key="${esc$1(key2)}"
6544
+ data-index="${index}" data-key="${esc(key2)}"
5633
6545
  role="option" aria-selected="${isSelected}"
5634
6546
  ${this.dragdrop ? 'draggable="true"' : ""}
5635
6547
  tabindex="0">
@@ -5637,7 +6549,7 @@ const _FxOrderList = class _FxOrderList extends FxElement {
5637
6549
  ${hasSelection ? `
5638
6550
  <input type="checkbox" class="checkbox" part="checkbox"
5639
6551
  ${isSelected ? "checked" : ""}
5640
- aria-label="Selecionar ${esc$1(label)}" />
6552
+ aria-label="Selecionar ${esc(label)}" />
5641
6553
  ` : ""}
5642
6554
  <span class="item-content" part="item-content">${content}</span>
5643
6555
  </li>
@@ -5825,9 +6737,6 @@ _FxOrderList.styles = css`
5825
6737
  .list-item { animation: slideIn 0.2s ease-out; }
5826
6738
  `;
5827
6739
  let FxOrderList = _FxOrderList;
5828
- function esc$1(s) {
5829
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5830
- }
5831
6740
  function defineFxOrderList() {
5832
6741
  return defineElement("fx-orderlist", FxOrderList);
5833
6742
  }
@@ -6567,9 +7476,6 @@ _FxPickList.styles = css`
6567
7476
  .list-item { animation: slideIn 0.2s ease-out; }
6568
7477
  `;
6569
7478
  let FxPickList = _FxPickList;
6570
- function esc(s) {
6571
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6572
- }
6573
7479
  function defineFxPickList() {
6574
7480
  return defineElement("fx-picklist", FxPickList);
6575
7481
  }
@@ -6578,6 +7484,7 @@ const _FxAccordionPanel = class _FxAccordionPanel extends FxElement {
6578
7484
  constructor() {
6579
7485
  super(...arguments);
6580
7486
  this._mo = null;
7487
+ this._clickListenerAttached = false;
6581
7488
  }
6582
7489
  static get observedAttributes() {
6583
7490
  return ["header", "expanded", "disabled"];
@@ -6608,18 +7515,21 @@ const _FxAccordionPanel = class _FxAccordionPanel extends FxElement {
6608
7515
  }
6609
7516
  connectedCallback() {
6610
7517
  super.connectedCallback();
6611
- this.addEventListener("click", (e) => {
6612
- const path = e.composedPath();
6613
- if (!path.some((el) => el.classList?.contains("header"))) return;
6614
- if (this.disabled) return;
6615
- this.dispatchEvent(
6616
- new CustomEvent("fx-accordion-toggle", {
6617
- bubbles: true,
6618
- composed: true,
6619
- detail: { panel: this }
6620
- })
6621
- );
6622
- });
7518
+ if (!this._clickListenerAttached) {
7519
+ this._clickListenerAttached = true;
7520
+ this.addEventListener("click", (e) => {
7521
+ const path = e.composedPath();
7522
+ if (!path.some((el) => el.classList?.contains("header"))) return;
7523
+ if (this.disabled) return;
7524
+ this.dispatchEvent(
7525
+ new CustomEvent("fx-accordion-toggle", {
7526
+ bubbles: true,
7527
+ composed: true,
7528
+ detail: { panel: this }
7529
+ })
7530
+ );
7531
+ });
7532
+ }
6623
7533
  this._mo = new MutationObserver(() => this.render());
6624
7534
  this._mo.observe(this, { attributes: true, attributeFilter: ["expanded", "header", "disabled"] });
6625
7535
  }
@@ -6634,7 +7544,7 @@ const _FxAccordionPanel = class _FxAccordionPanel extends FxElement {
6634
7544
  aria-expanded="${this.expanded}"
6635
7545
  ${this.disabled ? 'aria-disabled="true"' : ""}>
6636
7546
  <span class="header-text" part="header-text">
6637
- <slot name="header">${this.header}</slot>
7547
+ <slot name="header">${esc(this.header)}</slot>
6638
7548
  </span>
6639
7549
  <span class="chevron" part="chevron" aria-hidden="true">
6640
7550
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none"
@@ -6899,11 +7809,14 @@ export {
6899
7809
  defineElement,
6900
7810
  defineFxTooltipDirective,
6901
7811
  destroyFxTooltipDirective,
7812
+ esc,
6902
7813
  kebabToCamel,
6903
7814
  lightTokens,
6904
7815
  listPresets,
6905
7816
  resetTheme,
7817
+ sanitizeData,
6906
7818
  setTokens,
7819
+ stripTags,
6907
7820
  theme,
6908
7821
  themePresets,
6909
7822
  tokenCssVars