@wrrdev/fenix-ui 1.1.15 → 1.1.17

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 (107) hide show
  1. package/README.md +2 -2
  2. package/dist/components/autocomplete/autocomplete.d.ts +3 -1
  3. package/dist/components/autocomplete/autocomplete.d.ts.map +1 -1
  4. package/dist/components/autocomplete/autocomplete.js +7 -1
  5. package/dist/components/autocomplete/autocomplete.js.map +1 -1
  6. package/dist/components/avatar/avatar.d.ts +1 -0
  7. package/dist/components/avatar/avatar.d.ts.map +1 -1
  8. package/dist/components/avatar/avatar.js +3 -0
  9. package/dist/components/avatar/avatar.js.map +1 -1
  10. package/dist/components/badge/badge.d.ts +4 -0
  11. package/dist/components/badge/badge.d.ts.map +1 -1
  12. package/dist/components/badge/badge.js +12 -0
  13. package/dist/components/badge/badge.js.map +1 -1
  14. package/dist/components/button/button.d.ts +2 -0
  15. package/dist/components/button/button.d.ts.map +1 -1
  16. package/dist/components/button/button.js +6 -0
  17. package/dist/components/button/button.js.map +1 -1
  18. package/dist/components/card/card.d.ts +2 -0
  19. package/dist/components/card/card.d.ts.map +1 -1
  20. package/dist/components/card/card.js +6 -0
  21. package/dist/components/card/card.js.map +1 -1
  22. package/dist/components/checkbox/checkbox.d.ts +5 -1
  23. package/dist/components/checkbox/checkbox.d.ts.map +1 -1
  24. package/dist/components/checkbox/checkbox.js +7 -8
  25. package/dist/components/checkbox/checkbox.js.map +1 -1
  26. package/dist/components/datepicker/datepicker.d.ts +17 -2
  27. package/dist/components/datepicker/datepicker.d.ts.map +1 -1
  28. package/dist/components/datepicker/datepicker.js +113 -42
  29. package/dist/components/datepicker/datepicker.js.map +1 -1
  30. package/dist/components/dialog/dialog.d.ts +4 -1
  31. package/dist/components/dialog/dialog.d.ts.map +1 -1
  32. package/dist/components/dialog/dialog.js +41 -18
  33. package/dist/components/dialog/dialog.js.map +1 -1
  34. package/dist/components/drawer/drawer.d.ts +5 -1
  35. package/dist/components/drawer/drawer.d.ts.map +1 -1
  36. package/dist/components/drawer/drawer.js +39 -17
  37. package/dist/components/drawer/drawer.js.map +1 -1
  38. package/dist/components/fileupload/fileupload.d.ts +13 -1
  39. package/dist/components/fileupload/fileupload.d.ts.map +1 -1
  40. package/dist/components/fileupload/fileupload.js +29 -1
  41. package/dist/components/fileupload/fileupload.js.map +1 -1
  42. package/dist/components/floatlabel/floatlabel.d.ts +15 -0
  43. package/dist/components/floatlabel/floatlabel.d.ts.map +1 -1
  44. package/dist/components/floatlabel/floatlabel.js +47 -1
  45. package/dist/components/floatlabel/floatlabel.js.map +1 -1
  46. package/dist/components/icon/icon.d.ts +4 -0
  47. package/dist/components/icon/icon.d.ts.map +1 -1
  48. package/dist/components/icon/icon.js +12 -0
  49. package/dist/components/icon/icon.js.map +1 -1
  50. package/dist/components/input/input.d.ts +9 -2
  51. package/dist/components/input/input.d.ts.map +1 -1
  52. package/dist/components/input/input.js +56 -8
  53. package/dist/components/input/input.js.map +1 -1
  54. package/dist/components/multiselect/multiselect.d.ts +8 -1
  55. package/dist/components/multiselect/multiselect.d.ts.map +1 -1
  56. package/dist/components/multiselect/multiselect.js +61 -9
  57. package/dist/components/multiselect/multiselect.js.map +1 -1
  58. package/dist/components/orderlist/orderlist.d.ts +7 -0
  59. package/dist/components/orderlist/orderlist.d.ts.map +1 -1
  60. package/dist/components/orderlist/orderlist.js +34 -2
  61. package/dist/components/orderlist/orderlist.js.map +1 -1
  62. package/dist/components/pagination/pagination.d.ts +3 -0
  63. package/dist/components/pagination/pagination.d.ts.map +1 -1
  64. package/dist/components/pagination/pagination.js +19 -9
  65. package/dist/components/pagination/pagination.js.map +1 -1
  66. package/dist/components/radio/radio.d.ts +6 -0
  67. package/dist/components/radio/radio.d.ts.map +1 -1
  68. package/dist/components/radio/radio.js +12 -6
  69. package/dist/components/radio/radio.js.map +1 -1
  70. package/dist/components/select/select.d.ts +19 -1
  71. package/dist/components/select/select.d.ts.map +1 -1
  72. package/dist/components/select/select.js +116 -10
  73. package/dist/components/select/select.js.map +1 -1
  74. package/dist/components/switch/switch.d.ts +4 -1
  75. package/dist/components/switch/switch.d.ts.map +1 -1
  76. package/dist/components/switch/switch.js +2 -1
  77. package/dist/components/switch/switch.js.map +1 -1
  78. package/dist/components/table/table.d.ts +27 -0
  79. package/dist/components/table/table.d.ts.map +1 -1
  80. package/dist/components/table/table.js +126 -24
  81. package/dist/components/table/table.js.map +1 -1
  82. package/dist/components/tabs/tabs.d.ts +6 -0
  83. package/dist/components/tabs/tabs.d.ts.map +1 -1
  84. package/dist/components/tabs/tabs.js +34 -0
  85. package/dist/components/tabs/tabs.js.map +1 -1
  86. package/dist/components/textarea/textarea.d.ts +5 -1
  87. package/dist/components/textarea/textarea.d.ts.map +1 -1
  88. package/dist/components/textarea/textarea.js +25 -6
  89. package/dist/components/textarea/textarea.js.map +1 -1
  90. package/dist/core/jsx.d.ts +11 -5
  91. package/dist/core/jsx.d.ts.map +1 -1
  92. package/dist/core/jsx.js.map +1 -1
  93. package/dist/core/modal.d.ts +31 -0
  94. package/dist/core/modal.d.ts.map +1 -0
  95. package/dist/core/modal.js +61 -0
  96. package/dist/core/modal.js.map +1 -0
  97. package/dist/core/page-range.d.ts +31 -0
  98. package/dist/core/page-range.d.ts.map +1 -0
  99. package/dist/core/page-range.js +41 -0
  100. package/dist/core/page-range.js.map +1 -0
  101. package/dist/docs/componentes/input.doc.d.ts.map +1 -1
  102. package/dist/docs/componentes/orderlist.doc.d.ts.map +1 -1
  103. package/dist/docs/componentes/table.doc.d.ts.map +1 -1
  104. package/dist/fenix-ui.umd.min.js +261 -227
  105. package/dist/fenix-ui4.esm.js +987 -260
  106. package/dist/fenix-ui4.esm.js.map +1 -1
  107. package/package.json +1 -1
@@ -506,6 +506,12 @@ const _FxButton = class _FxButton extends FxElement {
506
506
  set loading(value) {
507
507
  this.toggleAttr("loading", Boolean(value));
508
508
  }
509
+ get full() {
510
+ return this.hasAttr("full");
511
+ }
512
+ set full(value) {
513
+ this.toggleAttr("full", Boolean(value));
514
+ }
509
515
  render() {
510
516
  const loading = this.hasAttr("loading");
511
517
  const disabled = this.hasAttr("disabled") || loading;
@@ -685,6 +691,18 @@ const _FxBadge = class _FxBadge extends FxElement {
685
691
  static get observedAttributes() {
686
692
  return ["variant", "round"];
687
693
  }
694
+ get variant() {
695
+ return this.getAttr("variant", "default");
696
+ }
697
+ set variant(v) {
698
+ this.setAttribute("variant", v);
699
+ }
700
+ get round() {
701
+ return this.hasAttr("round");
702
+ }
703
+ set round(v) {
704
+ this.toggleAttr("round", Boolean(v));
705
+ }
688
706
  render() {
689
707
  this.setTemplate(`<span class="badge" part="badge"><slot></slot></span>`);
690
708
  }
@@ -764,10 +782,21 @@ function defineFxSpinner() {
764
782
  }
765
783
  defineFxSpinner();
766
784
  const _FxSelect = class _FxSelect extends FxFormElement {
767
- // `value` fica FORA da observação: refleti-lo no change não pode
768
- // re-renderizar o template e fechar o dropdown.
785
+ constructor() {
786
+ super(...arguments);
787
+ this._suppressRender = false;
788
+ this._fixedMode = false;
789
+ this._fixedResolved = false;
790
+ }
791
+ // `value` é observado para que `setAttribute('value', ...)` (HTML puro)
792
+ // também atualize o rótulo. Os caminhos internos de change usam
793
+ // `_suppressRender` para renderizar UMA vez, depois de fechar o painel.
769
794
  static get observedAttributes() {
770
- return ["size", "disabled", "placeholder", "searchable", "clearable", "error", "invalid", "success", "valid", "placement"];
795
+ return ["value", "size", "disabled", "placeholder", "searchable", "clearable", "full", "error", "invalid", "success", "valid", "placement"];
796
+ }
797
+ attributeChangedCallback(name, oldValue, newValue) {
798
+ if (this._suppressRender) return;
799
+ super.attributeChangedCallback(name, oldValue, newValue);
771
800
  }
772
801
  /** Tamanho do campo. Padrão: `'md'`. */
773
802
  get size() {
@@ -781,7 +810,9 @@ const _FxSelect = class _FxSelect extends FxFormElement {
781
810
  return this.getAttr("value");
782
811
  }
783
812
  set value(value) {
784
- this.setAttribute("value", value);
813
+ const next = value == null ? "" : String(value);
814
+ const prev = this.getAttr("value");
815
+ if (prev !== next) this.setAttribute("value", next);
785
816
  this.updateFormValue();
786
817
  }
787
818
  get disabled() {
@@ -790,6 +821,24 @@ const _FxSelect = class _FxSelect extends FxFormElement {
790
821
  set disabled(value) {
791
822
  this.toggleAttr("disabled", Boolean(value));
792
823
  }
824
+ get searchable() {
825
+ return this.hasAttr("searchable");
826
+ }
827
+ set searchable(value) {
828
+ this.toggleAttr("searchable", Boolean(value));
829
+ }
830
+ get clearable() {
831
+ return this.hasAttr("clearable");
832
+ }
833
+ set clearable(value) {
834
+ this.toggleAttr("clearable", Boolean(value));
835
+ }
836
+ get full() {
837
+ return this.hasAttr("full");
838
+ }
839
+ set full(value) {
840
+ this.toggleAttr("full", Boolean(value));
841
+ }
793
842
  get options() {
794
843
  return [...this.querySelectorAll("option")].map((o) => ({
795
844
  value: o.getAttribute("value") ?? o.textContent?.trim() ?? "",
@@ -798,8 +847,17 @@ const _FxSelect = class _FxSelect extends FxFormElement {
798
847
  }
799
848
  connectedCallback() {
800
849
  super.connectedCallback();
801
- this.observer = new MutationObserver(() => this.render());
802
- this.observer.observe(this, { childList: true, subtree: true, characterData: true });
850
+ this.observer = new MutationObserver((mutations) => {
851
+ if (mutations.every((m) => m.type === "attributes" && m.target === this)) return;
852
+ this.render();
853
+ });
854
+ this.observer.observe(this, {
855
+ childList: true,
856
+ subtree: true,
857
+ characterData: true,
858
+ attributes: true,
859
+ attributeFilter: ["value", "selected"]
860
+ });
803
861
  this.docListener = (e) => {
804
862
  if (!this.hasAttr("open")) return;
805
863
  if (e.composedPath().includes(this)) return;
@@ -816,6 +874,8 @@ const _FxSelect = class _FxSelect extends FxFormElement {
816
874
  }
817
875
  /** Abre o dropdown, posiciona o painel e passa a reposicioná-lo no reflow. */
818
876
  _abrir() {
877
+ this._fixedMode = _FxSelect._precisaFixed(this);
878
+ this._fixedResolved = true;
819
879
  this.setAttribute("open", "");
820
880
  this._bindReflow();
821
881
  }
@@ -823,6 +883,8 @@ const _FxSelect = class _FxSelect extends FxFormElement {
823
883
  _fechar() {
824
884
  this.removeAttribute("open");
825
885
  this.removeAttribute("data-placement");
886
+ this._fixedMode = false;
887
+ this._fixedResolved = false;
826
888
  this._unbindReflow();
827
889
  }
828
890
  /** Liga o reposicionamento (resize + scroll interno, em capture). */
@@ -865,7 +927,30 @@ const _FxSelect = class _FxSelect extends FxFormElement {
865
927
  _FxSelect.MIN_ALTURA,
866
928
  (paraCima ? acima : abaixo) - _FxSelect.GAP
867
929
  );
868
- painel.style.maxHeight = `${Math.min(desejado || disponivel, disponivel, maxAltura)}px`;
930
+ const altura = Math.min(desejado || disponivel, disponivel, maxAltura);
931
+ painel.style.maxHeight = `${altura}px`;
932
+ if (!this._fixedResolved) {
933
+ this._fixedMode = _FxSelect._precisaFixed(this);
934
+ this._fixedResolved = true;
935
+ }
936
+ if (this._fixedMode) {
937
+ this.setAttribute("data-fixed", "");
938
+ const largura2 = Math.max(r.width, 220);
939
+ const esquerda = Math.max(
940
+ _FxSelect.MARGEM,
941
+ Math.min(r.left, window.innerWidth - largura2 - _FxSelect.MARGEM)
942
+ );
943
+ const topo = paraCima ? Math.max(_FxSelect.MARGEM, r.top - _FxSelect.GAP - altura) : r.bottom + _FxSelect.GAP;
944
+ painel.style.bottom = "auto";
945
+ painel.style.width = `${largura2}px`;
946
+ painel.style.left = `${esquerda}px`;
947
+ painel.style.top = `${topo}px`;
948
+ return;
949
+ }
950
+ this.removeAttribute("data-fixed");
951
+ painel.style.top = "";
952
+ painel.style.bottom = "";
953
+ painel.style.width = "";
869
954
  painel.style.left = "";
870
955
  const largura = painel.offsetWidth;
871
956
  const excesso = r.left + largura - (window.innerWidth - _FxSelect.MARGEM);
@@ -874,6 +959,21 @@ const _FxSelect = class _FxSelect extends FxFormElement {
874
959
  painel.style.right = "auto";
875
960
  }
876
961
  }
962
+ /**
963
+ * Existe ancestral com overflow que cortaria o painel absoluto?
964
+ * `getComputedStyle` é defensivo: em jsdom retorna vazio e o modo
965
+ * histórico (absoluto) é mantido.
966
+ */
967
+ static _precisaFixed(host) {
968
+ let el = host.parentElement;
969
+ while (el && el !== document.body && el !== document.documentElement) {
970
+ const cs = getComputedStyle(el);
971
+ const overflow = `${cs.overflow}${cs.overflowX}${cs.overflowY}`;
972
+ if (/(hidden|clip|auto|scroll|overlay)/.test(overflow)) return true;
973
+ el = el.parentElement;
974
+ }
975
+ return false;
976
+ }
877
977
  /** Altura máxima do painel a partir de `--fx-select-panel-max-height`. */
878
978
  static _maxAltura(host) {
879
979
  const raw = getComputedStyle(host).getPropertyValue("--fx-select-panel-max-height");
@@ -881,7 +981,12 @@ const _FxSelect = class _FxSelect extends FxFormElement {
881
981
  return Number.isFinite(n) && n > 0 ? n : 260;
882
982
  }
883
983
  select(value) {
884
- this.value = value;
984
+ this._suppressRender = true;
985
+ try {
986
+ this.value = value;
987
+ } finally {
988
+ this._suppressRender = false;
989
+ }
885
990
  this._fechar();
886
991
  this.render();
887
992
  this.emitValueEvents();
@@ -908,6 +1013,10 @@ const _FxSelect = class _FxSelect extends FxFormElement {
908
1013
  }
909
1014
  render() {
910
1015
  const prevOpen = this.hasAttr("open");
1016
+ if (!prevOpen) {
1017
+ this._fixedMode = false;
1018
+ this._fixedResolved = false;
1019
+ }
911
1020
  const prevSearch = this.root.querySelector(".search");
912
1021
  const search = prevSearch?.value ?? "";
913
1022
  const searchFocused = prevSearch != null && this.root.activeElement === prevSearch;
@@ -984,7 +1093,12 @@ const _FxSelect = class _FxSelect extends FxFormElement {
984
1093
  });
985
1094
  this.root.querySelector(".clear")?.addEventListener("click", (e) => {
986
1095
  e.stopPropagation();
987
- this.value = "";
1096
+ this._suppressRender = true;
1097
+ try {
1098
+ this.value = "";
1099
+ } finally {
1100
+ this._suppressRender = false;
1101
+ }
988
1102
  this._fechar();
989
1103
  this.render();
990
1104
  this.emitValueEvents();
@@ -999,7 +1113,12 @@ const _FxSelect = class _FxSelect extends FxFormElement {
999
1113
  }
1000
1114
  /** form.reset(): restaura o valor inicial e re-renderiza o rótulo. */
1001
1115
  formResetCallback() {
1002
- super.formResetCallback();
1116
+ this._suppressRender = true;
1117
+ try {
1118
+ super.formResetCallback();
1119
+ } finally {
1120
+ this._suppressRender = false;
1121
+ }
1003
1122
  this.render();
1004
1123
  }
1005
1124
  };
@@ -1122,6 +1241,11 @@ _FxSelect.styles = css`
1122
1241
  border-radius: var(--fx-radius-md);
1123
1242
  box-shadow: var(--fx-shadow-lg);
1124
1243
  }
1244
+ /* Ancestral com overflow (ex.: fx-card) cortaria o painel absoluto:
1245
+ nesse caso o JS posiciona com position:fixed a partir do trigger. */
1246
+ :host([data-fixed]) .panel {
1247
+ position: fixed;
1248
+ }
1125
1249
  /* Padrão: abre para baixo. */
1126
1250
  :host([open]:not([data-placement='top'])) .panel {
1127
1251
  top: calc(100% + var(--fx-select-panel-offset, 4px));
@@ -1187,7 +1311,21 @@ function defineFxSelect() {
1187
1311
  return defineElement("fx-select", FxSelect);
1188
1312
  }
1189
1313
  defineFxSelect();
1190
- const INPUT_TYPES = /* @__PURE__ */ new Set(["text", "number", "password"]);
1314
+ const INPUT_TYPES = /* @__PURE__ */ new Set([
1315
+ "text",
1316
+ "number",
1317
+ "password",
1318
+ "email",
1319
+ "tel",
1320
+ "url",
1321
+ "search",
1322
+ "date",
1323
+ "time",
1324
+ "datetime-local",
1325
+ "month",
1326
+ "week",
1327
+ "color"
1328
+ ]);
1191
1329
  function normalizeInputType(value) {
1192
1330
  const type = (value || "text").toLowerCase();
1193
1331
  return INPUT_TYPES.has(type) ? type : "text";
@@ -1207,6 +1345,8 @@ const _FxInput = class _FxInput extends FxFormElement {
1207
1345
  "min",
1208
1346
  "max",
1209
1347
  "step",
1348
+ "clearable",
1349
+ "full",
1210
1350
  "icon",
1211
1351
  "icon-pos",
1212
1352
  "error",
@@ -1234,7 +1374,7 @@ const _FxInput = class _FxInput extends FxFormElement {
1234
1374
  return this.getAttr("value");
1235
1375
  }
1236
1376
  set value(value) {
1237
- this.setAttribute("value", value);
1377
+ this.setAttribute("value", value == null ? "" : String(value));
1238
1378
  this.syncInternalControl();
1239
1379
  this.updateFormValue();
1240
1380
  }
@@ -1244,6 +1384,24 @@ const _FxInput = class _FxInput extends FxFormElement {
1244
1384
  set disabled(value) {
1245
1385
  this.toggleAttr("disabled", Boolean(value));
1246
1386
  }
1387
+ get readonly() {
1388
+ return this.hasAttr("readonly");
1389
+ }
1390
+ set readonly(value) {
1391
+ this.toggleAttr("readonly", Boolean(value));
1392
+ }
1393
+ get clearable() {
1394
+ return this.hasAttr("clearable");
1395
+ }
1396
+ set clearable(value) {
1397
+ this.toggleAttr("clearable", Boolean(value));
1398
+ }
1399
+ get full() {
1400
+ return this.hasAttr("full");
1401
+ }
1402
+ set full(value) {
1403
+ this.toggleAttr("full", Boolean(value));
1404
+ }
1247
1405
  render() {
1248
1406
  const type = this.type;
1249
1407
  const placeholder = this.getAttr("placeholder");
@@ -1274,20 +1432,27 @@ const _FxInput = class _FxInput extends FxFormElement {
1274
1432
  field.value = this.getAttr("value");
1275
1433
  if (disabled) field.setAttribute("disabled", "");
1276
1434
  if (readonly) field.setAttribute("readonly", "");
1277
- const emit = (event) => {
1435
+ const notifyInput = () => {
1278
1436
  this.value = field.value;
1279
1437
  this.emitValueEvents();
1438
+ };
1439
+ const notifyChange = () => {
1440
+ this.value = field.value;
1280
1441
  this.dispatchEvent(
1281
- new CustomEvent(event, { bubbles: true, composed: true, detail: { value: field.value } })
1442
+ new CustomEvent("change", {
1443
+ bubbles: true,
1444
+ composed: true,
1445
+ detail: { value: field.value }
1446
+ })
1282
1447
  );
1283
1448
  };
1284
1449
  field.addEventListener("input", (e) => {
1285
1450
  e.stopPropagation();
1286
- emit("input");
1451
+ notifyInput();
1287
1452
  });
1288
1453
  field.addEventListener("change", (e) => {
1289
1454
  e.stopPropagation();
1290
- emit("change");
1455
+ notifyChange();
1291
1456
  });
1292
1457
  field.addEventListener("keydown", (e) => {
1293
1458
  if (e.key !== "Enter" || e.isComposing) return;
@@ -1303,8 +1468,15 @@ const _FxInput = class _FxInput extends FxFormElement {
1303
1468
  clearBtn?.addEventListener("click", () => {
1304
1469
  field.value = "";
1305
1470
  syncClear();
1306
- emit("input");
1307
- emit("change");
1471
+ this.value = "";
1472
+ this.emitValueEvents();
1473
+ this.dispatchEvent(
1474
+ new CustomEvent("change", {
1475
+ bubbles: true,
1476
+ composed: true,
1477
+ detail: { value: "" }
1478
+ })
1479
+ );
1308
1480
  field.focus();
1309
1481
  });
1310
1482
  field.addEventListener("input", syncClear);
@@ -1494,7 +1666,8 @@ const _FxSwitch = class _FxSwitch extends FxFormElement {
1494
1666
  return this.getAttr("value");
1495
1667
  }
1496
1668
  set value(v) {
1497
- this.setAttribute("value", v);
1669
+ this.setAttribute("value", v == null ? "" : String(v));
1670
+ this.updateFormValue();
1498
1671
  }
1499
1672
  /** No FormData: `value` (ou 'on') quando ligado; null (não envia) quando não. */
1500
1673
  getFormValue() {
@@ -1612,7 +1785,23 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1612
1785
  this.selected = /* @__PURE__ */ new Set();
1613
1786
  }
1614
1787
  static get observedAttributes() {
1615
- return ["disabled", "placeholder", "error", "invalid", "success", "valid"];
1788
+ return [
1789
+ "values",
1790
+ "size",
1791
+ "disabled",
1792
+ "placeholder",
1793
+ "searchable",
1794
+ "clearable",
1795
+ "full",
1796
+ "error",
1797
+ "invalid",
1798
+ "success",
1799
+ "valid"
1800
+ ];
1801
+ }
1802
+ attributeChangedCallback(name, oldValue, newValue) {
1803
+ if (name === "values") this.selected = new Set(this.values);
1804
+ super.attributeChangedCallback(name, oldValue, newValue);
1616
1805
  }
1617
1806
  /** Valores selecionados (CSV no atributo / array na propriedade). */
1618
1807
  get values() {
@@ -1620,7 +1809,15 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1620
1809
  return csv ? csv.split(",").map((v) => v.trim()).filter(Boolean) : [];
1621
1810
  }
1622
1811
  set values(list) {
1623
- this.setAttribute("values", list.join(","));
1812
+ const values = Array.isArray(list) ? list.map(String) : [];
1813
+ this.selected = new Set(values);
1814
+ const csv = values.join(",");
1815
+ if (this.getAttr("values") !== csv) {
1816
+ this.setAttribute("values", csv);
1817
+ } else if (this.isConnected) {
1818
+ this.render();
1819
+ }
1820
+ this.updateFormValue();
1624
1821
  }
1625
1822
  /** No FormData do form: uma entrada `name` por valor selecionado. */
1626
1823
  getFormValue() {
@@ -1636,6 +1833,24 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1636
1833
  set disabled(value) {
1637
1834
  this.toggleAttr("disabled", Boolean(value));
1638
1835
  }
1836
+ get searchable() {
1837
+ return this.hasAttr("searchable");
1838
+ }
1839
+ set searchable(value) {
1840
+ this.toggleAttr("searchable", Boolean(value));
1841
+ }
1842
+ get clearable() {
1843
+ return this.hasAttr("clearable");
1844
+ }
1845
+ set clearable(value) {
1846
+ this.toggleAttr("clearable", Boolean(value));
1847
+ }
1848
+ get full() {
1849
+ return this.hasAttr("full");
1850
+ }
1851
+ set full(value) {
1852
+ this.toggleAttr("full", Boolean(value));
1853
+ }
1639
1854
  get options() {
1640
1855
  return [...this.querySelectorAll("option")].map((o) => ({
1641
1856
  value: o.getAttribute("value") ?? o.textContent?.trim() ?? "",
@@ -1647,7 +1862,15 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1647
1862
  if (!this._initialValues) this._initialValues = [...this.values];
1648
1863
  super.connectedCallback();
1649
1864
  this.observer = new MutationObserver(() => this.render());
1650
- this.observer.observe(this, { childList: true, subtree: true, characterData: true });
1865
+ this.observer.observe(this, {
1866
+ childList: true,
1867
+ subtree: true,
1868
+ characterData: true,
1869
+ // Vue atualiza `:value`/`:selected` das <option> no light DOM; sem
1870
+ // observar atributos o label/valor antigo permanecia em tela.
1871
+ attributes: true,
1872
+ attributeFilter: ["value", "selected"]
1873
+ });
1651
1874
  this.docListener = (e) => {
1652
1875
  if (!this.hasAttr("open")) return;
1653
1876
  if (e.composedPath().includes(this)) return;
@@ -1663,8 +1886,10 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1663
1886
  }
1664
1887
  /** Aplica a seleção, reflete e emite change. */
1665
1888
  commit() {
1666
- this.values = [...this.selected];
1667
- this.render();
1889
+ const next = [...this.selected];
1890
+ const same = next.join(",") === this.values.join(",");
1891
+ this.values = next;
1892
+ if (same && this.isConnected) this.render();
1668
1893
  this.updateFormValue();
1669
1894
  this.dispatchEvent(
1670
1895
  new CustomEvent("change", {
@@ -1685,8 +1910,6 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1685
1910
  formResetCallback() {
1686
1911
  const initial = this._initialValues ?? [];
1687
1912
  this.values = initial;
1688
- this.selected = new Set(initial);
1689
- this.render();
1690
1913
  this.updateFormValue();
1691
1914
  }
1692
1915
  toggleValue(value) {
@@ -1715,8 +1938,10 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1715
1938
  const wasOpen = this.hasAttr("open");
1716
1939
  const search = this.root.querySelector(".search")?.value ?? "";
1717
1940
  const opts = this.options;
1718
- for (const v of [...this.selected]) {
1719
- if (!opts.some((o) => o.value === v)) this.selected.delete(v);
1941
+ if (opts.length > 0) {
1942
+ for (const v of [...this.selected]) {
1943
+ if (!opts.some((o) => o.value === v)) this.selected.delete(v);
1944
+ }
1720
1945
  }
1721
1946
  const chips = opts.filter((o) => this.selected.has(o.value));
1722
1947
  const placeholder = this.getAttr("placeholder", "Selecione…");
@@ -2496,13 +2721,16 @@ defineFxCalendar();
2496
2721
  const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2497
2722
  constructor() {
2498
2723
  super(...arguments);
2724
+ this._suppressRender = false;
2499
2725
  this.time = { h: 0, m: 0, s: 0 };
2500
2726
  }
2501
2727
  static get observedAttributes() {
2502
- return ["value", "start", "end", "values", "mode", "min", "max", "placeholder", "disabled", "format", "show-time", "free-text", "error", "invalid", "success", "valid"];
2728
+ return ["value", "start", "end", "values", "mode", "min", "max", "placeholder", "disabled", "format", "show-time", "free-text", "clearable", "full", "error", "invalid", "success", "valid"];
2503
2729
  }
2504
2730
  attributeChangedCallback(name, oldValue, newValue) {
2505
- super.attributeChangedCallback(name, oldValue, newValue);
2731
+ if (!this._suppressRender) {
2732
+ super.attributeChangedCallback(name, oldValue, newValue);
2733
+ }
2506
2734
  if (name === "value" || name === "start" || name === "end" || name === "values") {
2507
2735
  this.updateFormValue();
2508
2736
  }
@@ -2538,7 +2766,19 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2538
2766
  return this.getAttr("value");
2539
2767
  }
2540
2768
  set value(v) {
2541
- this.setAttribute("value", v);
2769
+ this.setAttribute("value", v == null ? "" : String(v));
2770
+ }
2771
+ get start() {
2772
+ return this.getAttr("start");
2773
+ }
2774
+ set start(v) {
2775
+ this.setAttribute("start", v == null ? "" : String(v));
2776
+ }
2777
+ get end() {
2778
+ return this.getAttr("end");
2779
+ }
2780
+ set end(v) {
2781
+ this.setAttribute("end", v == null ? "" : String(v));
2542
2782
  }
2543
2783
  /** Datas do modo múltiplo (CSV no atributo). */
2544
2784
  get values() {
@@ -2546,7 +2786,8 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2546
2786
  return csv ? csv.split(",").map((v) => v.trim()).filter(Boolean) : [];
2547
2787
  }
2548
2788
  set values(list) {
2549
- this.setAttribute("values", list.join(","));
2789
+ const values = Array.isArray(list) ? list.map(String) : [];
2790
+ this.setAttribute("values", values.join(","));
2550
2791
  }
2551
2792
  get disabled() {
2552
2793
  return this.hasAttr("disabled");
@@ -2554,6 +2795,31 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2554
2795
  set disabled(value) {
2555
2796
  this.toggleAttr("disabled", Boolean(value));
2556
2797
  }
2798
+ /** `:show-time` procura a propriedade LITERAL no Vue (sem camelização). */
2799
+ get "show-time"() {
2800
+ return this.hasAttr("show-time");
2801
+ }
2802
+ set "show-time"(value) {
2803
+ this.toggleAttr("show-time", Boolean(value));
2804
+ }
2805
+ get "free-text"() {
2806
+ return this.hasAttr("free-text");
2807
+ }
2808
+ set "free-text"(value) {
2809
+ this.toggleAttr("free-text", Boolean(value));
2810
+ }
2811
+ get clearable() {
2812
+ return this.hasAttr("clearable");
2813
+ }
2814
+ set clearable(value) {
2815
+ this.toggleAttr("clearable", Boolean(value));
2816
+ }
2817
+ get full() {
2818
+ return this.hasAttr("full");
2819
+ }
2820
+ set full(value) {
2821
+ this.toggleAttr("full", Boolean(value));
2822
+ }
2557
2823
  connectedCallback() {
2558
2824
  if (!this._initialState) {
2559
2825
  this._initialState = {
@@ -2578,11 +2844,17 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2578
2844
  /** form.reset(): restaura value/start/end/values iniciais (re-render via atributos observados). */
2579
2845
  formResetCallback() {
2580
2846
  const s = this._initialState ?? { value: "", start: "", end: "", values: "" };
2581
- this.setAttribute("value", s.value);
2582
- this.setAttribute("start", s.start);
2583
- this.setAttribute("end", s.end);
2584
- this.setAttribute("values", s.values);
2847
+ this._suppressRender = true;
2848
+ try {
2849
+ this.setAttribute("value", s.value);
2850
+ this.setAttribute("start", s.start);
2851
+ this.setAttribute("end", s.end);
2852
+ this.setAttribute("values", s.values);
2853
+ } finally {
2854
+ this._suppressRender = false;
2855
+ }
2585
2856
  this.updateFormValue();
2857
+ if (this.isConnected) this.render();
2586
2858
  }
2587
2859
  /** Formata uma data ISO ("YYYY-MM-DD" ou com "THH:mm:ss") segundo o atributo format. */
2588
2860
  formatDate(iso) {
@@ -2690,10 +2962,15 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2690
2962
  });
2691
2963
  this.root.querySelector(".clear")?.addEventListener("click", (e) => {
2692
2964
  e.stopPropagation();
2693
- this.removeAttribute("value");
2694
- this.removeAttribute("start");
2695
- this.removeAttribute("end");
2696
- this.removeAttribute("values");
2965
+ this._suppressRender = true;
2966
+ try {
2967
+ this.removeAttribute("value");
2968
+ this.removeAttribute("start");
2969
+ this.removeAttribute("end");
2970
+ this.removeAttribute("values");
2971
+ } finally {
2972
+ this._suppressRender = false;
2973
+ }
2697
2974
  this.removeAttribute("open");
2698
2975
  this.render();
2699
2976
  this.emitChange({});
@@ -2701,19 +2978,24 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2701
2978
  const cal = this.root.querySelector(".pop fx-calendar");
2702
2979
  cal?.addEventListener("change", (e) => {
2703
2980
  const d = e.detail ?? {};
2704
- if (d.start !== void 0 || d.end !== void 0) {
2705
- if (d.start) this.setAttribute("start", this.withTime(d.start));
2706
- else this.removeAttribute("start");
2707
- if (d.end) this.setAttribute("end", this.withTime(d.end));
2708
- else this.removeAttribute("end");
2709
- }
2710
- if (d.values !== void 0) {
2711
- this.values = d.values.map((v) => this.withTime(v));
2712
- }
2713
- if (d.value !== void 0) {
2714
- if (d.value) this.setAttribute("value", this.withTime(d.value));
2715
- else this.removeAttribute("value");
2716
- if (this.mode === "single") this.removeAttribute("open");
2981
+ this._suppressRender = true;
2982
+ try {
2983
+ if (d.start !== void 0 || d.end !== void 0) {
2984
+ if (d.start) this.setAttribute("start", this.withTime(d.start));
2985
+ else this.removeAttribute("start");
2986
+ if (d.end) this.setAttribute("end", this.withTime(d.end));
2987
+ else this.removeAttribute("end");
2988
+ }
2989
+ if (d.values !== void 0) {
2990
+ this.values = d.values.map((v) => this.withTime(v));
2991
+ }
2992
+ if (d.value !== void 0) {
2993
+ if (d.value) this.setAttribute("value", this.withTime(d.value));
2994
+ else this.removeAttribute("value");
2995
+ if (this.mode === "single") this.removeAttribute("open");
2996
+ }
2997
+ } finally {
2998
+ this._suppressRender = false;
2717
2999
  }
2718
3000
  this.render();
2719
3001
  this.emitChange(d);
@@ -2740,11 +3022,16 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2740
3022
  s: clamp(s?.value, 59)
2741
3023
  };
2742
3024
  const reattach = (iso) => iso.split("T")[0] ? `${iso.split("T")[0]}T${this.timeText()}` : iso;
2743
- if (this.value) this.setAttribute("value", reattach(this.value));
2744
- if (this.getAttr("start")) this.setAttribute("start", reattach(this.getAttr("start")));
2745
- if (this.getAttr("end")) this.setAttribute("end", reattach(this.getAttr("end")));
2746
- if (this.values.length) {
2747
- this.values = this.values.map((v) => reattach(v));
3025
+ this._suppressRender = true;
3026
+ try {
3027
+ if (this.value) this.setAttribute("value", reattach(this.value));
3028
+ if (this.getAttr("start")) this.setAttribute("start", reattach(this.getAttr("start")));
3029
+ if (this.getAttr("end")) this.setAttribute("end", reattach(this.getAttr("end")));
3030
+ if (this.values.length) {
3031
+ this.values = this.values.map((v) => reattach(v));
3032
+ }
3033
+ } finally {
3034
+ this._suppressRender = false;
2748
3035
  }
2749
3036
  const display2 = this.root.querySelector(".display");
2750
3037
  if (display2) display2.value = this.displayText();
@@ -2800,23 +3087,32 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2800
3087
  setTimeout(() => input.classList.remove("invalid"), 1500);
2801
3088
  return;
2802
3089
  }
3090
+ this._suppressRender = true;
3091
+ try {
3092
+ if (this.mode === "single") {
3093
+ this.setAttribute("value", this.withTime(parsed));
3094
+ } else if (this.mode === "range") {
3095
+ if (!this.getAttr("start") || this.getAttr("start") && this.getAttr("end")) {
3096
+ this.setAttribute("start", this.withTime(parsed));
3097
+ this.removeAttribute("end");
3098
+ } else {
3099
+ const [a, b] = [this.getAttr("start").split("T")[0], parsed].sort();
3100
+ this.setAttribute("start", this.withTime(a));
3101
+ this.setAttribute("end", this.withTime(b));
3102
+ }
3103
+ } else {
3104
+ const list = this.values.filter((v) => v.split("T")[0] !== parsed);
3105
+ list.push(this.withTime(parsed));
3106
+ this.values = list;
3107
+ }
3108
+ } finally {
3109
+ this._suppressRender = false;
3110
+ }
2803
3111
  if (this.mode === "single") {
2804
- this.setAttribute("value", this.withTime(parsed));
2805
3112
  this.emitChange({ value: this.value });
2806
3113
  } else if (this.mode === "range") {
2807
- if (!this.getAttr("start") || this.getAttr("start") && this.getAttr("end")) {
2808
- this.setAttribute("start", this.withTime(parsed));
2809
- this.removeAttribute("end");
2810
- } else {
2811
- const [a, b] = [this.getAttr("start").split("T")[0], parsed].sort();
2812
- this.setAttribute("start", this.withTime(a));
2813
- this.setAttribute("end", this.withTime(b));
2814
- }
2815
3114
  this.emitChange({ start: this.getAttr("start"), end: this.getAttr("end") });
2816
3115
  } else {
2817
- const list = this.values.filter((v) => v.split("T")[0] !== parsed);
2818
- list.push(this.withTime(parsed));
2819
- this.values = list;
2820
3116
  this.emitChange({ values: this.values });
2821
3117
  }
2822
3118
  this.render();
@@ -2987,7 +3283,7 @@ const _FxCheckbox = class _FxCheckbox extends FxFormElement {
2987
3283
  this._initialCheckedCaptured = false;
2988
3284
  }
2989
3285
  static get observedAttributes() {
2990
- return ["checked", "disabled", "indeterminate"];
3286
+ return ["checked", "disabled", "indeterminate", "size"];
2991
3287
  }
2992
3288
  get checked() {
2993
3289
  return this.hasAttr("checked");
@@ -3016,7 +3312,8 @@ const _FxCheckbox = class _FxCheckbox extends FxFormElement {
3016
3312
  return this.getAttr("value");
3017
3313
  }
3018
3314
  set value(v) {
3019
- this.setAttribute("value", v);
3315
+ this.setAttribute("value", v == null ? "" : String(v));
3316
+ this.updateFormValue();
3020
3317
  }
3021
3318
  /** Tamanho do controle. Padrão: `'md'`. */
3022
3319
  get size() {
@@ -3028,9 +3325,7 @@ const _FxCheckbox = class _FxCheckbox extends FxFormElement {
3028
3325
  }
3029
3326
  render() {
3030
3327
  const px = this.size === "sm" ? "14px" : this.size === "lg" ? "22px" : "18px";
3031
- if (!this.style.getPropertyValue("--fx-size-checkbox")) {
3032
- this.style.setProperty("--fx-size-checkbox", px);
3033
- }
3328
+ this.style.setProperty("--_fx-size-checkbox", px);
3034
3329
  this.setTemplate(`
3035
3330
  <span class="box" part="box" role="checkbox" tabindex="0"
3036
3331
  aria-checked="${this.indeterminate ? "mixed" : this.checked}"
@@ -3102,8 +3397,8 @@ _FxCheckbox.styles = css`
3102
3397
  -webkit-tap-highlight-color: transparent;
3103
3398
  }
3104
3399
  .control {
3105
- width: var(--fx-size-checkbox, 18px);
3106
- height: var(--fx-size-checkbox, 18px);
3400
+ width: var(--fx-size-checkbox, var(--_fx-size-checkbox, 18px));
3401
+ height: var(--fx-size-checkbox, var(--_fx-size-checkbox, 18px));
3107
3402
  flex-shrink: 0;
3108
3403
  display: inline-flex;
3109
3404
  align-items: center;
@@ -3112,7 +3407,7 @@ _FxCheckbox.styles = css`
3112
3407
  border-radius: calc(var(--fx-radius-sm) + 2px);
3113
3408
  background: var(--fx-surface-background);
3114
3409
  color: #fff;
3115
- font-size: calc(var(--fx-size-checkbox, 18px) - 6px);
3410
+ font-size: calc(var(--fx-size-checkbox, var(--_fx-size-checkbox, 18px)) - 6px);
3116
3411
  line-height: 1;
3117
3412
  transition:
3118
3413
  background-color var(--fx-motion-duration-fast) var(--fx-motion-easing),
@@ -3159,7 +3454,7 @@ const _FxRadio = class _FxRadio extends FxFormElement {
3159
3454
  this._initialCheckedCaptured = false;
3160
3455
  }
3161
3456
  static get observedAttributes() {
3162
- return ["checked", "disabled"];
3457
+ return ["checked", "disabled", "size"];
3163
3458
  }
3164
3459
  get checked() {
3165
3460
  return this.hasAttr("checked");
@@ -3192,6 +3487,14 @@ const _FxRadio = class _FxRadio extends FxFormElement {
3192
3487
  getFormValue() {
3193
3488
  return this.checked ? this.getAttr("value") || "on" : null;
3194
3489
  }
3490
+ /** getter/setter `value` — sem o setter o Vue não consegue escrever a propriedade. */
3491
+ get value() {
3492
+ return this.getAttr("value");
3493
+ }
3494
+ set value(v) {
3495
+ this.setAttribute("value", v == null ? "" : String(v));
3496
+ this.updateFormValue();
3497
+ }
3195
3498
  /** Tamanho do controle. Padrão: `'md'`. */
3196
3499
  get size() {
3197
3500
  const s = this.getAttr("size", "md");
@@ -3202,9 +3505,7 @@ const _FxRadio = class _FxRadio extends FxFormElement {
3202
3505
  }
3203
3506
  render() {
3204
3507
  const px = this.size === "sm" ? "14px" : this.size === "lg" ? "22px" : "18px";
3205
- if (!this.style.getPropertyValue("--fx-size-radio")) {
3206
- this.style.setProperty("--fx-size-radio", px);
3207
- }
3508
+ this.style.setProperty("--_fx-size-radio", px);
3208
3509
  this.setTemplate(`
3209
3510
  <span class="box" part="box" role="radio" tabindex="0"
3210
3511
  aria-checked="${this.checked}" aria-disabled="${this.disabled}">
@@ -3292,8 +3593,8 @@ _FxRadio.styles = css`
3292
3593
  -webkit-tap-highlight-color: transparent;
3293
3594
  }
3294
3595
  .control {
3295
- width: var(--fx-size-radio, 18px);
3296
- height: var(--fx-size-radio, 18px);
3596
+ width: var(--fx-size-radio, var(--_fx-size-radio, 18px));
3597
+ height: var(--fx-size-radio, var(--_fx-size-radio, 18px));
3297
3598
  flex-shrink: 0;
3298
3599
  display: inline-flex;
3299
3600
  align-items: center;
@@ -3337,6 +3638,43 @@ function defineFxRadio() {
3337
3638
  return defineElement("fx-radio", FxRadio);
3338
3639
  }
3339
3640
  defineFxRadio();
3641
+ function toInt(value, fallback) {
3642
+ return Number.isFinite(value) ? Math.trunc(value) : fallback;
3643
+ }
3644
+ function range(start, end) {
3645
+ const out = [];
3646
+ for (let p = start; p <= end; p++) out.push(p);
3647
+ return out;
3648
+ }
3649
+ function pageRange(current, count, options = {}) {
3650
+ const pages = Math.max(0, toInt(count, 0));
3651
+ if (pages === 0) return [];
3652
+ const boundaries = Math.max(0, toInt(options.boundaries ?? 1, 1));
3653
+ const siblings = Math.max(0, toInt(options.siblings ?? 1, 1));
3654
+ const page = Math.min(Math.max(1, toInt(current, 1)), pages);
3655
+ if (pages <= boundaries * 2 + siblings * 2 + 3) return range(1, pages);
3656
+ const startPages = range(1, Math.min(boundaries, pages));
3657
+ const endPages = range(
3658
+ Math.max(pages - boundaries + 1, boundaries + 1),
3659
+ pages
3660
+ );
3661
+ const siblingsStart = Math.max(
3662
+ Math.min(page - siblings, pages - boundaries - siblings * 2 - 1),
3663
+ boundaries + 2
3664
+ );
3665
+ const siblingsEnd = Math.min(
3666
+ Math.max(page + siblings, boundaries + siblings * 2 + 2),
3667
+ endPages.length > 0 ? endPages[0] - 2 : pages - 1
3668
+ );
3669
+ const items = [...startPages];
3670
+ if (siblingsStart > boundaries + 2) items.push("ellipsis");
3671
+ else if (boundaries + 1 < pages - boundaries) items.push(boundaries + 1);
3672
+ items.push(...range(siblingsStart, siblingsEnd));
3673
+ if (siblingsEnd < pages - boundaries - 1) items.push("ellipsis");
3674
+ else if (pages - boundaries > boundaries) items.push(pages - boundaries);
3675
+ items.push(...endPages);
3676
+ return items;
3677
+ }
3340
3678
  const LOCALE = "pt-BR";
3341
3679
  const PIPES = {
3342
3680
  /** Formata como moeda brasileira (R$). */
@@ -3768,6 +4106,7 @@ const _FxTable = class _FxTable extends FxElement {
3768
4106
  this.sortDir = 1;
3769
4107
  this.filters = {};
3770
4108
  this.globalSearch = "";
4109
+ this._boundToolbarInputs = /* @__PURE__ */ new WeakSet();
3771
4110
  this._shellKey = "";
3772
4111
  }
3773
4112
  static get observedAttributes() {
@@ -3815,6 +4154,44 @@ const _FxTable = class _FxTable extends FxElement {
3815
4154
  get lazy() {
3816
4155
  return this.hasAttr("lazy");
3817
4156
  }
4157
+ set lazy(value) {
4158
+ this.toggleAttr("lazy", Boolean(value));
4159
+ }
4160
+ get pagination() {
4161
+ return this.hasAttr("pagination");
4162
+ }
4163
+ set pagination(value) {
4164
+ this.toggleAttr("pagination", Boolean(value));
4165
+ }
4166
+ get striped() {
4167
+ return this.hasAttr("striped");
4168
+ }
4169
+ set striped(value) {
4170
+ this.toggleAttr("striped", Boolean(value));
4171
+ }
4172
+ get hover() {
4173
+ return this.hasAttr("hover");
4174
+ }
4175
+ set hover(value) {
4176
+ this.toggleAttr("hover", Boolean(value));
4177
+ }
4178
+ get loading() {
4179
+ return this.hasAttr("loading");
4180
+ }
4181
+ set loading(value) {
4182
+ this.toggleAttr("loading", Boolean(value));
4183
+ }
4184
+ /**
4185
+ * Vue não cameliza atributos de elementos customizados:
4186
+ * `:no-borders-x="false"` procura a propriedade LITERAL
4187
+ * `'no-borders-x'` — o accessor literal evita o contorno `.attr`.
4188
+ */
4189
+ get "no-borders-x"() {
4190
+ return this.hasAttr("no-borders-x");
4191
+ }
4192
+ set "no-borders-x"(value) {
4193
+ this.toggleAttr("no-borders-x", Boolean(value));
4194
+ }
3818
4195
  get columns() {
3819
4196
  return [...this.querySelectorAll("fx-column")].map((c) => {
3820
4197
  const tpl = this._resolveCellTemplate(c);
@@ -3898,7 +4275,14 @@ const _FxTable = class _FxTable extends FxElement {
3898
4275
  loadFenixIconsFont();
3899
4276
  super.connectedCallback();
3900
4277
  this._columnObserver = new MutationObserver(() => this.render());
3901
- this._columnObserver.observe(this, { childList: true, subtree: true });
4278
+ this._columnObserver.observe(this, {
4279
+ childList: true,
4280
+ subtree: true,
4281
+ // Colunas com header/field/align/sortable/filterable alterados
4282
+ // em runtime também precisam re-renderizar.
4283
+ attributes: true,
4284
+ attributeFilter: ["header", "field", "align", "sortable", "filterable", "template"]
4285
+ });
3902
4286
  }
3903
4287
  disconnectedCallback() {
3904
4288
  this._columnObserver?.disconnect();
@@ -3980,7 +4364,14 @@ const _FxTable = class _FxTable extends FxElement {
3980
4364
  const pages = Math.max(1, Math.ceil(total / this.rowsPerPage));
3981
4365
  if (this.page >= pages) this.page = pages - 1;
3982
4366
  const toolbar = this.toolbarTemplate;
3983
- const toolbarHtml = toolbar ? `<div class="toolbar" part="toolbar">${toolbar.innerHTML}</div>` : "";
4367
+ const toolbarSlotted = toolbar?.localName === "fx-toolbar";
4368
+ const toolbarHasContent = Boolean(
4369
+ toolbar && (toolbarSlotted ? toolbar.children.length > 0 || (toolbar.textContent ?? "").trim() !== "" : toolbar.innerHTML.trim() !== "")
4370
+ );
4371
+ if (toolbarSlotted && toolbarHasContent) {
4372
+ toolbar.setAttribute("slot", "fx-toolbar");
4373
+ }
4374
+ const toolbarHtml = toolbarHasContent ? `<div class="toolbar" part="toolbar">${toolbarSlotted ? '<slot name="fx-toolbar"></slot>' : toolbar.innerHTML}</div>` : "";
3984
4375
  const headHtml = cols.map((c) => {
3985
4376
  const isSorted = this.sortField === c.field;
3986
4377
  const ind = isSorted ? this.sortDir === 1 ? "▲" : "▼" : c.sortable ? "⇅" : "";
@@ -3991,16 +4382,19 @@ const _FxTable = class _FxTable extends FxElement {
3991
4382
  const bodyHtml = rows.length ? rows.map(
3992
4383
  (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>`
3993
4384
  ).join("") : `<tr><td class="empty" colspan="${cols.length || 1}">${esc(this.getAttr("empty-message", "Nenhum registro encontrado"))}</td></tr>`;
4385
+ const pageButtons = pageRange(this.page + 1, pages).map(
4386
+ (item) => item === "ellipsis" ? `<span class="ellipsis" part="ellipsis" aria-hidden="true">…</span>` : `<button type="button" class="pg-btn" part="page" data-pg="${item - 1}"${item === this.page + 1 ? ' aria-current="page"' : ""}>${item}</button>`
4387
+ ).join("");
3994
4388
  const pagerHtml = !paginated ? "" : `
3995
4389
  <div class="pager" part="pager">
3996
4390
  <span class="info">Página ${this.page + 1} de ${pages} · ${total} registros</span>
3997
4391
  <button type="button" class="pg-btn" part="first" aria-label="Primeira página" data-pg="first" ${this.page === 0 ? "disabled" : ""}>${fenixIconHtml("first_page")}</button>
3998
4392
  <button type="button" class="pg-btn" part="prev" aria-label="Página anterior" data-pg="prev" ${this.page === 0 ? "disabled" : ""}>${fenixIconHtml("navigate_before")}</button>
3999
- ${Array.from({ length: pages }, (_, p) => `<button type="button" class="pg-btn" data-pg="${p}" aria-current="${p === this.page}">${p + 1}</button>`).join("")}
4393
+ ${pageButtons}
4000
4394
  <button type="button" class="pg-btn" part="next" aria-label="Próxima página" data-pg="next" ${this.page >= pages - 1 ? "disabled" : ""}>${fenixIconHtml("navigate_next")}</button>
4001
4395
  <button type="button" class="pg-btn" part="last" aria-label="Última página" data-pg="last" ${this.page >= pages - 1 ? "disabled" : ""}>${fenixIconHtml("last_page")}</button>
4002
4396
  <label class="info">${esc("Por página:")}
4003
- <fx-select class="rows-sel" size="sm" value="${this.rowsPerPage}" aria-label="Itens por página">${this.getAttr(
4397
+ <fx-select class="rows-sel" size="sm" placement="top" value="${this.rowsPerPage}" aria-label="Itens por página">${this.getAttr(
4004
4398
  "rows-options",
4005
4399
  "5,10,20,50"
4006
4400
  ).split(",").map((n) => n.trim()).map((n) => `<option value="${esc(n)}">${esc(n)}</option>`).join("")}</fx-select>
@@ -4021,13 +4415,15 @@ const _FxTable = class _FxTable extends FxElement {
4021
4415
  if (tbody) {
4022
4416
  tbody.innerHTML = bodyHtml;
4023
4417
  this._bindRowClicks(rows);
4418
+ this._bindSlottedToolbarSearch(toolbar);
4419
+ this._restoreToolbarSearch();
4024
4420
  return;
4025
4421
  }
4026
4422
  }
4027
4423
  this._shellKey = shellKey;
4028
4424
  this.setTemplate(`
4029
4425
  ${toolbarHtml}
4030
- <div class="frame${toolbar ? " has-toolbar" : ""}" part="frame">
4426
+ <div class="frame${toolbarHasContent ? " has-toolbar" : ""}" part="frame">
4031
4427
  <div class="table-wrap">
4032
4428
  <div class="scroll">
4033
4429
  <table part="table">
@@ -4088,21 +4484,10 @@ const _FxTable = class _FxTable extends FxElement {
4088
4484
  });
4089
4485
  inp.addEventListener("click", (e) => e.stopPropagation());
4090
4486
  });
4091
- this.root.querySelectorAll("[data-search-fields]").forEach((inp) => {
4092
- inp.addEventListener("input", () => {
4093
- this.globalSearch = inp.value;
4094
- this.page = 0;
4095
- const pos = inp.selectionStart;
4096
- this.render();
4097
- const again = this.root.querySelector(
4098
- "[data-search-fields]"
4099
- );
4100
- if (again) {
4101
- again.focus();
4102
- again.setSelectionRange(pos, pos);
4103
- }
4104
- });
4105
- });
4487
+ this.root.querySelectorAll("[data-search-fields]").forEach((inp) => this._bindSearchInput(inp, false));
4488
+ if (toolbarSlotted && toolbar) {
4489
+ this._bindSlottedToolbarSearch(toolbar);
4490
+ }
4106
4491
  this.root.querySelectorAll(".pg-btn").forEach((btn) => {
4107
4492
  btn.addEventListener("click", () => {
4108
4493
  const pg = btn.dataset.pg;
@@ -4155,11 +4540,47 @@ const _FxTable = class _FxTable extends FxElement {
4155
4540
  });
4156
4541
  });
4157
4542
  }
4543
+ /** Liga (uma vez) os inputs de busca de um <fx-toolbar> slotted. */
4544
+ _bindSlottedToolbarSearch(toolbar) {
4545
+ if (!toolbar || toolbar.localName !== "fx-toolbar") return;
4546
+ toolbar.querySelectorAll("[data-search-fields]").forEach((inp) => {
4547
+ if (this._boundToolbarInputs.has(inp)) return;
4548
+ this._boundToolbarInputs.add(inp);
4549
+ this._bindSearchInput(inp, true);
4550
+ });
4551
+ }
4552
+ /**
4553
+ * Liga um input da toolbar à busca global.
4554
+ * `persistent` = input slotted (não é recriado pelo render): o foco volta
4555
+ * no próprio nó. Inputs clonados são substituídos, então o foco é
4556
+ * restaurado no clone novo do shadow.
4557
+ */
4558
+ _bindSearchInput(inp, persistent) {
4559
+ inp.addEventListener("input", () => {
4560
+ this.globalSearch = inp.value;
4561
+ this.page = 0;
4562
+ const pos = inp.selectionStart;
4563
+ this.render();
4564
+ const again = persistent && inp.isConnected ? inp : this.root.querySelector("[data-search-fields]");
4565
+ if (again) {
4566
+ again.focus();
4567
+ again.setSelectionRange(pos, pos);
4568
+ }
4569
+ });
4570
+ }
4158
4571
  _restoreToolbarSearch() {
4159
- const input = this.root.querySelector(
4572
+ const shadow = this.root.querySelector(
4160
4573
  "[data-search-fields]"
4161
4574
  );
4162
- if (input && this.globalSearch) input.value = this.globalSearch;
4575
+ if (shadow && this.globalSearch) shadow.value = this.globalSearch;
4576
+ const toolbar = this.toolbarTemplate;
4577
+ if (toolbar?.localName === "fx-toolbar") {
4578
+ toolbar.querySelectorAll("[data-search-fields]").forEach((input) => {
4579
+ if (this.globalSearch && input.value !== this.globalSearch) {
4580
+ input.value = this.globalSearch;
4581
+ }
4582
+ });
4583
+ }
4163
4584
  }
4164
4585
  };
4165
4586
  _FxTable.styles = css`
@@ -4352,7 +4773,7 @@ _FxTable.styles = css`
4352
4773
  .pg-btn .fx-icon {
4353
4774
  font-size: calc(var(--fx-font-size) - 1px);
4354
4775
  }
4355
- .pg-btn:hover:not(:disabled):not([aria-current="true"]) {
4776
+ .pg-btn:hover:not(:disabled):not([aria-current="page"]) {
4356
4777
  border-color: var(--fx-color-primary);
4357
4778
  color: var(--fx-color-primary);
4358
4779
  }
@@ -4360,12 +4781,25 @@ _FxTable.styles = css`
4360
4781
  opacity: 0.5;
4361
4782
  cursor: not-allowed;
4362
4783
  }
4363
- .pg-btn[aria-current="true"] {
4784
+ .pg-btn[aria-current="page"] {
4364
4785
  background: var(--fx-color-primary);
4365
4786
  border-color: var(--fx-color-primary);
4366
4787
  color: #fff;
4367
4788
  font-weight: var(--fx-font-weight);
4368
4789
  }
4790
+ /* Elipse da janela de páginas (1 … 499 [500] 501 … 1000): ocupa o
4791
+ mesmo espaço de um botão para o rodapé não "pular" ao navegar,
4792
+ mas não é clicável. */
4793
+ .pager .ellipsis {
4794
+ display: inline-flex;
4795
+ align-items: center;
4796
+ justify-content: center;
4797
+ min-width: var(--fx-size-sm);
4798
+ height: var(--fx-size-sm);
4799
+ color: var(--fx-text-muted);
4800
+ font-size: calc(var(--fx-font-size) - 2px);
4801
+ user-select: none;
4802
+ }
4369
4803
  /* O seletor "Por página" só mostra números, então não pode herdar a
4370
4804
  largura padrão do <fx-select> (200px / 180px em size="sm"), que
4371
4805
  deixava o rodapé da paginação desproporcional. O limite tem que ser
@@ -4402,6 +4836,11 @@ _FxTable.styles = css`
4402
4836
  .toolbar:empty {
4403
4837
  display: none;
4404
4838
  }
4839
+ /* <fx-toolbar> é apenas um invólucro no light DOM: o slot projeta
4840
+ seus filhos (com listeners do Vue preservados) direto no flex. */
4841
+ ::slotted(fx-toolbar) {
4842
+ display: contents;
4843
+ }
4405
4844
  .loading-overlay {
4406
4845
  position: absolute;
4407
4846
  inset: 0;
@@ -4452,7 +4891,51 @@ const _FxFloatlabel = class _FxFloatlabel extends FxElement {
4452
4891
  this._labelClickBound = false;
4453
4892
  }
4454
4893
  static get observedAttributes() {
4455
- return ["variant", "error", "invalid", "success", "valid", "full", "icon", "icon-pos"];
4894
+ return ["variant", "error", "invalid", "success", "valid", "full", "icon", "icon-pos", "error-text"];
4895
+ }
4896
+ get error() {
4897
+ return this.hasAttr("error");
4898
+ }
4899
+ set error(value) {
4900
+ this.toggleAttr("error", Boolean(value));
4901
+ }
4902
+ get invalid() {
4903
+ return this.hasAttr("invalid");
4904
+ }
4905
+ set invalid(value) {
4906
+ this.toggleAttr("invalid", Boolean(value));
4907
+ }
4908
+ get success() {
4909
+ return this.hasAttr("success");
4910
+ }
4911
+ set success(value) {
4912
+ this.toggleAttr("success", Boolean(value));
4913
+ }
4914
+ get valid() {
4915
+ return this.hasAttr("valid");
4916
+ }
4917
+ set valid(value) {
4918
+ this.toggleAttr("valid", Boolean(value));
4919
+ }
4920
+ get full() {
4921
+ return this.hasAttr("full");
4922
+ }
4923
+ set full(value) {
4924
+ this.toggleAttr("full", Boolean(value));
4925
+ }
4926
+ get errorText() {
4927
+ return this.getAttr("error-text");
4928
+ }
4929
+ set errorText(value) {
4930
+ this.setAttribute("error-text", value == null ? "" : String(value));
4931
+ }
4932
+ connectedCallback() {
4933
+ super.connectedCallback();
4934
+ this._labelObserver = new MutationObserver(() => {
4935
+ const label = this.findLabel();
4936
+ if (label && label !== this._label) this.render();
4937
+ });
4938
+ this._labelObserver.observe(this, { childList: true });
4456
4939
  }
4457
4940
  render() {
4458
4941
  const control = this.findControl();
@@ -4496,6 +4979,8 @@ const _FxFloatlabel = class _FxFloatlabel extends FxElement {
4496
4979
  disconnectedCallback() {
4497
4980
  super.disconnectedCallback();
4498
4981
  this.observer?.disconnect();
4982
+ this._labelObserver?.disconnect();
4983
+ this._labelObserver = void 0;
4499
4984
  }
4500
4985
  /** Campo de controle = qualquer filho direto que não seja label. */
4501
4986
  findControl() {
@@ -4785,7 +5270,7 @@ function defineFxFloatlabel() {
4785
5270
  defineFxFloatlabel();
4786
5271
  const _FxTextarea = class _FxTextarea extends FxFormElement {
4787
5272
  static get observedAttributes() {
4788
- return ["size", "placeholder", "disabled", "readonly", "rows", "maxlength", "error", "invalid", "success", "valid"];
5273
+ return ["size", "placeholder", "disabled", "readonly", "full", "rows", "maxlength", "error", "invalid", "success", "valid"];
4789
5274
  }
4790
5275
  get size() {
4791
5276
  const s = this.getAttr("size", "md");
@@ -4798,10 +5283,22 @@ const _FxTextarea = class _FxTextarea extends FxFormElement {
4798
5283
  return this.getAttr("value");
4799
5284
  }
4800
5285
  set value(value) {
4801
- this.setAttribute("value", value);
5286
+ this.setAttribute("value", value == null ? "" : String(value));
4802
5287
  this.syncInternalControl();
4803
5288
  this.updateFormValue();
4804
5289
  }
5290
+ get readonly() {
5291
+ return this.hasAttr("readonly");
5292
+ }
5293
+ set readonly(value) {
5294
+ this.toggleAttr("readonly", Boolean(value));
5295
+ }
5296
+ get full() {
5297
+ return this.hasAttr("full");
5298
+ }
5299
+ set full(value) {
5300
+ this.toggleAttr("full", Boolean(value));
5301
+ }
4805
5302
  render() {
4806
5303
  const placeholder = this.getAttr("placeholder");
4807
5304
  const readonly = this.hasAttr("readonly");
@@ -4818,20 +5315,27 @@ const _FxTextarea = class _FxTextarea extends FxFormElement {
4818
5315
  field.value = this.getAttr("value");
4819
5316
  if (disabled) field.setAttribute("disabled", "");
4820
5317
  if (readonly) field.setAttribute("readonly", "");
4821
- const emit = (event) => {
5318
+ const notifyInput = () => {
4822
5319
  this.value = field.value;
4823
5320
  this.emitValueEvents();
5321
+ };
5322
+ const notifyChange = () => {
5323
+ this.value = field.value;
4824
5324
  this.dispatchEvent(
4825
- new CustomEvent(event, { bubbles: true, composed: true, detail: { value: field.value } })
5325
+ new CustomEvent("change", {
5326
+ bubbles: true,
5327
+ composed: true,
5328
+ detail: { value: field.value }
5329
+ })
4826
5330
  );
4827
5331
  };
4828
5332
  field.addEventListener("input", (e) => {
4829
5333
  e.stopPropagation();
4830
- emit("input");
5334
+ notifyInput();
4831
5335
  });
4832
5336
  field.addEventListener("change", (e) => {
4833
5337
  e.stopPropagation();
4834
- emit("change");
5338
+ notifyChange();
4835
5339
  });
4836
5340
  this.updateFormValue();
4837
5341
  }
@@ -6118,10 +6622,10 @@ function rootOf(editor) {
6118
6622
  const root = editor.getRootNode();
6119
6623
  return typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot ? root : document;
6120
6624
  }
6121
- function isRangeInside(editor, range) {
6122
- if (!range) return false;
6123
- const start = range.startContainer;
6124
- const end = range.endContainer;
6625
+ function isRangeInside(editor, range2) {
6626
+ if (!range2) return false;
6627
+ const start = range2.startContainer;
6628
+ const end = range2.endContainer;
6125
6629
  return editor.contains(start) && editor.contains(end);
6126
6630
  }
6127
6631
  function readSelectionRange(root) {
@@ -6132,10 +6636,10 @@ function readSelectionRange(root) {
6132
6636
  const ranges = selection.getComposedRanges({ shadowRoots: [root] });
6133
6637
  const first = ranges[0];
6134
6638
  if (first) {
6135
- const range = document.createRange();
6136
- range.setStart(first.startContainer, first.startOffset);
6137
- range.setEnd(first.endContainer, first.endOffset);
6138
- return range;
6639
+ const range2 = document.createRange();
6640
+ range2.setStart(first.startContainer, first.startOffset);
6641
+ range2.setEnd(first.endContainer, first.endOffset);
6642
+ return range2;
6139
6643
  }
6140
6644
  } catch {
6141
6645
  }
@@ -6143,41 +6647,41 @@ function readSelectionRange(root) {
6143
6647
  return selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
6144
6648
  }
6145
6649
  function getRange(editor, fallback) {
6146
- const range = readSelectionRange(rootOf(editor));
6147
- if (isRangeInside(editor, range)) return range;
6650
+ const range2 = readSelectionRange(rootOf(editor));
6651
+ if (isRangeInside(editor, range2)) return range2;
6148
6652
  if (isRangeInside(editor, fallback ?? null)) return fallback ?? null;
6149
6653
  return null;
6150
6654
  }
6151
6655
  function saveRange(editor) {
6152
- const range = getRange(editor);
6153
- return range ? range.cloneRange() : null;
6656
+ const range2 = getRange(editor);
6657
+ return range2 ? range2.cloneRange() : null;
6154
6658
  }
6155
- function restoreRange(editor, range) {
6156
- if (!range || !isRangeInside(editor, range)) return;
6659
+ function restoreRange(editor, range2) {
6660
+ if (!range2 || !isRangeInside(editor, range2)) return;
6157
6661
  editor.focus({ preventScroll: true });
6158
6662
  const selection = document.getSelection();
6159
6663
  if (!selection) return;
6160
6664
  selection.removeAllRanges();
6161
- selection.addRange(range);
6665
+ selection.addRange(range2);
6162
6666
  }
6163
6667
  function placeCaretAtEnd(editor) {
6164
- const range = document.createRange();
6165
- range.selectNodeContents(editor);
6166
- range.collapse(false);
6668
+ const range2 = document.createRange();
6669
+ range2.selectNodeContents(editor);
6670
+ range2.collapse(false);
6167
6671
  const selection = document.getSelection();
6168
6672
  if (!selection) return;
6169
6673
  selection.removeAllRanges();
6170
- selection.addRange(range);
6674
+ selection.addRange(range2);
6171
6675
  }
6172
- function getBlock(editor, range) {
6173
- const active = range ?? getRange(editor);
6676
+ function getBlock(editor, range2) {
6677
+ const active = range2 ?? getRange(editor);
6174
6678
  if (!active) return null;
6175
6679
  const node = active.startContainer;
6176
6680
  const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
6177
6681
  const block = el?.closest(BLOCK_SELECTOR) ?? null;
6178
6682
  return block && editor.contains(block) ? block : null;
6179
6683
  }
6180
- function getSelectedBlocks(editor, range) {
6684
+ function getSelectedBlocks(editor, range2) {
6181
6685
  const active = getRange(editor);
6182
6686
  if (!active) return [];
6183
6687
  if (active.collapsed) {
@@ -6197,11 +6701,11 @@ function closestInEditor(editor, node, selector) {
6197
6701
  const match = el?.closest(selector) ?? null;
6198
6702
  return match && editor.contains(match) ? match : null;
6199
6703
  }
6200
- function setSelection(range) {
6704
+ function setSelection(range2) {
6201
6705
  const selection = document.getSelection();
6202
6706
  if (!selection) return;
6203
6707
  selection.removeAllRanges();
6204
- selection.addRange(range);
6708
+ selection.addRange(range2);
6205
6709
  }
6206
6710
  function nativeExec(command, value) {
6207
6711
  const exec = document.execCommand;
@@ -6214,16 +6718,16 @@ function nativeExec(command, value) {
6214
6718
  }
6215
6719
  function insertHtml(editor, html) {
6216
6720
  if (nativeExec("insertHTML", html)) return true;
6217
- const range = getRange(editor);
6218
- if (!range) {
6721
+ const range2 = getRange(editor);
6722
+ if (!range2) {
6219
6723
  editor.insertAdjacentHTML("beforeend", html);
6220
6724
  placeCaretAtEnd(editor);
6221
6725
  return true;
6222
6726
  }
6223
- range.deleteContents();
6224
- const fragment = range.createContextualFragment(html);
6727
+ range2.deleteContents();
6728
+ const fragment = range2.createContextualFragment(html);
6225
6729
  const last = fragment.lastChild;
6226
- range.insertNode(fragment);
6730
+ range2.insertNode(fragment);
6227
6731
  if (last) {
6228
6732
  const next = document.createRange();
6229
6733
  next.setStartAfter(last);
@@ -6239,12 +6743,12 @@ function unwrap(el) {
6239
6743
  el.remove();
6240
6744
  }
6241
6745
  function wrapSelection(editor, tag) {
6242
- const range = getRange(editor);
6243
- if (!range) return false;
6746
+ const range2 = getRange(editor);
6747
+ if (!range2) return false;
6244
6748
  const el = document.createElement(tag);
6245
- if (range.collapsed) {
6749
+ if (range2.collapsed) {
6246
6750
  el.appendChild(document.createTextNode("​"));
6247
- range.insertNode(el);
6751
+ range2.insertNode(el);
6248
6752
  const caret = document.createRange();
6249
6753
  caret.setStart(el.firstChild ?? el, 1);
6250
6754
  caret.collapse(true);
@@ -6252,10 +6756,10 @@ function wrapSelection(editor, tag) {
6252
6756
  return true;
6253
6757
  }
6254
6758
  try {
6255
- range.surroundContents(el);
6759
+ range2.surroundContents(el);
6256
6760
  } catch {
6257
- el.appendChild(range.extractContents());
6258
- range.insertNode(el);
6761
+ el.appendChild(range2.extractContents());
6762
+ range2.insertNode(el);
6259
6763
  }
6260
6764
  return true;
6261
6765
  }
@@ -6271,9 +6775,9 @@ function toggleInline(editor, tag) {
6271
6775
  };
6272
6776
  const nativeCommand = nativeMap[tag];
6273
6777
  if (nativeCommand && nativeExec(nativeCommand)) return true;
6274
- const range = getRange(editor);
6275
- if (!range) return false;
6276
- const current = (range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer : range.startContainer.parentElement)?.closest(tag);
6778
+ const range2 = getRange(editor);
6779
+ if (!range2) return false;
6780
+ const current = (range2.startContainer.nodeType === Node.ELEMENT_NODE ? range2.startContainer : range2.startContainer.parentElement)?.closest(tag);
6277
6781
  if (current && editor.contains(current)) {
6278
6782
  unwrap(current);
6279
6783
  return true;
@@ -6322,31 +6826,31 @@ function toggleList(editor, ordered) {
6322
6826
  list.appendChild(li);
6323
6827
  block.remove();
6324
6828
  }
6325
- const range = document.createRange();
6326
- range.selectNodeContents(list.lastElementChild ?? list);
6327
- range.collapse(false);
6328
- setSelection(range);
6829
+ const range2 = document.createRange();
6830
+ range2.selectNodeContents(list.lastElementChild ?? list);
6831
+ range2.collapse(false);
6832
+ setSelection(range2);
6329
6833
  return true;
6330
6834
  }
6331
6835
  function removeFormat(editor) {
6332
6836
  if (nativeExec("removeFormat")) return true;
6333
- const range = getRange(editor);
6334
- if (!range) return false;
6335
- const root = range.commonAncestorContainer;
6837
+ const range2 = getRange(editor);
6838
+ if (!range2) return false;
6839
+ const root = range2.commonAncestorContainer;
6336
6840
  const container = root.nodeType === Node.ELEMENT_NODE ? root : root.parentElement;
6337
6841
  if (!container) return false;
6338
6842
  const tags = ["strong", "b", "em", "i", "u", "s", "strike", "span", "mark", "code", "sub", "sup"];
6339
6843
  for (const el of Array.from(container.querySelectorAll(tags.join(",")))) {
6340
- if (range.intersectsNode(el)) unwrap(el);
6844
+ if (range2.intersectsNode(el)) unwrap(el);
6341
6845
  }
6342
6846
  return true;
6343
6847
  }
6344
6848
  const linkHtml = (href, label, target) => `<a href="${escapeAttr(href)}" target="${target}" rel="noopener noreferrer">${escapeText(label)}</a>`;
6345
6849
  function applyLink(editor, href, text, target = "_blank") {
6346
6850
  const label = text?.trim() || href;
6347
- const range = getRange(editor);
6348
- if (!range) return insertHtml(editor, linkHtml(href, label, target));
6349
- const anchor = (range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer : range.startContainer.parentElement)?.closest("a");
6851
+ const range2 = getRange(editor);
6852
+ if (!range2) return insertHtml(editor, linkHtml(href, label, target));
6853
+ const anchor = (range2.startContainer.nodeType === Node.ELEMENT_NODE ? range2.startContainer : range2.startContainer.parentElement)?.closest("a");
6350
6854
  if (anchor && editor.contains(anchor)) {
6351
6855
  anchor.setAttribute("href", href);
6352
6856
  anchor.setAttribute("target", target);
@@ -6354,9 +6858,9 @@ function applyLink(editor, href, text, target = "_blank") {
6354
6858
  if (text && text !== anchor.textContent) anchor.textContent = text;
6355
6859
  return true;
6356
6860
  }
6357
- if (!range.collapsed) {
6861
+ if (!range2.collapsed) {
6358
6862
  if (nativeExec("createLink", href)) {
6359
- const created = (range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer : range.startContainer.parentElement)?.closest("a");
6863
+ const created = (range2.startContainer.nodeType === Node.ELEMENT_NODE ? range2.startContainer : range2.startContainer.parentElement)?.closest("a");
6360
6864
  created?.setAttribute("target", target);
6361
6865
  created?.setAttribute("rel", "noopener noreferrer");
6362
6866
  return true;
@@ -6366,10 +6870,10 @@ function applyLink(editor, href, text, target = "_blank") {
6366
6870
  el.setAttribute("target", target);
6367
6871
  el.setAttribute("rel", "noopener noreferrer");
6368
6872
  try {
6369
- range.surroundContents(el);
6873
+ range2.surroundContents(el);
6370
6874
  } catch {
6371
- el.appendChild(range.extractContents());
6372
- range.insertNode(el);
6875
+ el.appendChild(range2.extractContents());
6876
+ range2.insertNode(el);
6373
6877
  }
6374
6878
  return true;
6375
6879
  }
@@ -6377,9 +6881,9 @@ function applyLink(editor, href, text, target = "_blank") {
6377
6881
  }
6378
6882
  function unlink(editor) {
6379
6883
  if (nativeExec("unlink")) return true;
6380
- const range = getRange(editor);
6381
- if (!range) return false;
6382
- const anchor = (range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer : range.startContainer.parentElement)?.closest("a");
6884
+ const range2 = getRange(editor);
6885
+ if (!range2) return false;
6886
+ const anchor = (range2.startContainer.nodeType === Node.ELEMENT_NODE ? range2.startContainer : range2.startContainer.parentElement)?.closest("a");
6383
6887
  if (anchor && editor.contains(anchor)) {
6384
6888
  unwrap(anchor);
6385
6889
  return true;
@@ -6447,10 +6951,10 @@ function cellPosition(table, cell) {
6447
6951
  }
6448
6952
  return null;
6449
6953
  }
6450
- function selectedCells(table, range) {
6954
+ function selectedCells(table, range2) {
6451
6955
  return Array.from(table.querySelectorAll("td,th")).filter((cell) => {
6452
6956
  try {
6453
- return range.intersectsNode(cell);
6957
+ return range2.intersectsNode(cell);
6454
6958
  } catch {
6455
6959
  return false;
6456
6960
  }
@@ -6462,8 +6966,8 @@ function copyCellAttributes(from, to) {
6462
6966
  to.setAttribute(attr.name, attr.value);
6463
6967
  }
6464
6968
  }
6465
- function mergeSelectedCells(table, range) {
6466
- const cells = selectedCells(table, range);
6969
+ function mergeSelectedCells(table, range2) {
6970
+ const cells = selectedCells(table, range2);
6467
6971
  if (cells.length < 2) return null;
6468
6972
  const { matrix } = tableMatrix(table);
6469
6973
  const positions = cells.map((cell) => ({ cell, position: cellPosition(table, cell) })).filter((item) => Boolean(item.position));
@@ -6846,9 +7350,9 @@ const _FxEditor = class _FxEditor extends FxFormElement {
6846
7350
  mergeCells() {
6847
7351
  const table = __privateMethod(this, _FxEditor_instances, currentTable_fn).call(this);
6848
7352
  const editor = this.editor;
6849
- const range = editor ? getRange(editor, __privateGet(this, _lastRange)) : null;
6850
- if (!table || !range) return false;
6851
- const merged = mergeSelectedCells(table, range);
7353
+ const range2 = editor ? getRange(editor, __privateGet(this, _lastRange)) : null;
7354
+ if (!table || !range2) return false;
7355
+ const merged = mergeSelectedCells(table, range2);
6852
7356
  if (!merged) return false;
6853
7357
  placeCaretAtEnd(merged);
6854
7358
  __privateMethod(this, _FxEditor_instances, afterCommand_fn).call(this);
@@ -6858,8 +7362,8 @@ const _FxEditor = class _FxEditor extends FxFormElement {
6858
7362
  splitCell() {
6859
7363
  const table = __privateMethod(this, _FxEditor_instances, currentTable_fn).call(this);
6860
7364
  const editor = this.editor;
6861
- const range = editor ? getRange(editor, __privateGet(this, _lastRange)) : null;
6862
- const cell = editor ? closestInEditor(editor, range?.startContainer ?? null, "td,th") : null;
7365
+ const range2 = editor ? getRange(editor, __privateGet(this, _lastRange)) : null;
7366
+ const cell = editor ? closestInEditor(editor, range2?.startContainer ?? null, "td,th") : null;
6863
7367
  if (!table || !cell) return false;
6864
7368
  const created = splitCell(table, cell);
6865
7369
  if (!created.length) return false;
@@ -7119,8 +7623,8 @@ fits_fn = function(extra) {
7119
7623
  const max = this.maxlength;
7120
7624
  if (!max) return true;
7121
7625
  const editor = this.editor;
7122
- const range = editor ? getRange(editor, __privateGet(this, _lastRange)) : null;
7123
- const selected = range && !range.collapsed ? range.toString().length : 0;
7626
+ const range2 = editor ? getRange(editor, __privateGet(this, _lastRange)) : null;
7627
+ const selected = range2 && !range2.collapsed ? range2.toString().length : 0;
7124
7628
  return this.text.length - selected + extra <= max;
7125
7629
  };
7126
7630
  limitError_fn = function() {
@@ -7243,9 +7747,9 @@ bindGlobalSelection_fn = function() {
7243
7747
  const handler = () => {
7244
7748
  const editor = this.editor;
7245
7749
  if (!editor) return;
7246
- const range = getRange(editor, __privateGet(this, _lastRange));
7247
- if (!range) return;
7248
- __privateSet(this, _lastRange, range.cloneRange());
7750
+ const range2 = getRange(editor, __privateGet(this, _lastRange));
7751
+ if (!range2) return;
7752
+ __privateSet(this, _lastRange, range2.cloneRange());
7249
7753
  cancelAnimationFrame(__privateGet(this, _selectionRaf));
7250
7754
  __privateSet(this, _selectionRaf, requestAnimationFrame(() => __privateMethod(this, _FxEditor_instances, updateToolbar_fn).call(this)));
7251
7755
  };
@@ -7370,8 +7874,8 @@ renderToolbar_fn = function() {
7370
7874
  updateToolbar_fn = function() {
7371
7875
  const editor = this.editor;
7372
7876
  if (!editor) return;
7373
- const range = getRange(editor, __privateGet(this, _lastRange));
7374
- if (!range) return;
7877
+ const range2 = getRange(editor, __privateGet(this, _lastRange));
7878
+ if (!range2) return;
7375
7879
  const setActive = (cmd, active) => {
7376
7880
  const button = this.root.querySelector(`[data-cmd="${cmd}"]`);
7377
7881
  if (!button) return;
@@ -7379,7 +7883,7 @@ updateToolbar_fn = function() {
7379
7883
  button.setAttribute("aria-pressed", String(active));
7380
7884
  };
7381
7885
  const ancestor = (selector) => {
7382
- const node = range.startContainer;
7886
+ const node = range2.startContainer;
7383
7887
  const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
7384
7888
  const match = el?.closest(selector);
7385
7889
  return Boolean(match && editor.contains(match));
@@ -7391,7 +7895,7 @@ updateToolbar_fn = function() {
7391
7895
  setActive("code", ancestor("code"));
7392
7896
  setActive("insertUnorderedList", ancestor("ul"));
7393
7897
  setActive("insertOrderedList", ancestor("ol"));
7394
- const block = getBlock(editor, range);
7898
+ const block = getBlock(editor, range2);
7395
7899
  const blockTag = block?.tagName.toLowerCase() ?? "p";
7396
7900
  const blockSelect = this.root.querySelector("[data-block]");
7397
7901
  if (blockSelect) {
@@ -7635,9 +8139,9 @@ handleKeydown_fn = function(event) {
7635
8139
  tabInTable_fn = function(backwards) {
7636
8140
  const editor = this.editor;
7637
8141
  if (!editor) return false;
7638
- const range = getRange(editor, __privateGet(this, _lastRange));
7639
- if (!range) return false;
7640
- const cell = closestInEditor(editor, range.startContainer, "td,th");
8142
+ const range2 = getRange(editor, __privateGet(this, _lastRange));
8143
+ if (!range2) return false;
8144
+ const cell = closestInEditor(editor, range2.startContainer, "td,th");
7641
8145
  const table = cell?.closest("table");
7642
8146
  if (!cell || !table) return false;
7643
8147
  const cells = Array.from(table.querySelectorAll("td,th"));
@@ -7807,14 +8311,14 @@ openLinkDialog_fn = async function(existing) {
7807
8311
  const editor = this.editor;
7808
8312
  if (!editor) return;
7809
8313
  if (existing) __privateMethod(this, _FxEditor_instances, selectAnchor_fn).call(this, existing);
7810
- let range = getRange(editor, __privateGet(this, _lastRange));
7811
- if (!range) {
8314
+ let range2 = getRange(editor, __privateGet(this, _lastRange));
8315
+ if (!range2) {
7812
8316
  placeCaretAtEnd(editor);
7813
- range = getRange(editor);
7814
- __privateSet(this, _lastRange, range?.cloneRange() ?? null);
8317
+ range2 = getRange(editor);
8318
+ __privateSet(this, _lastRange, range2?.cloneRange() ?? null);
7815
8319
  }
7816
- const anchor = existing ?? closestInEditor(editor, range?.startContainer ?? null, "a");
7817
- const selected = range && !range.collapsed ? range.toString() : anchor?.textContent ?? "";
8320
+ const anchor = existing ?? closestInEditor(editor, range2?.startContainer ?? null, "a");
8321
+ const selected = range2 && !range2.collapsed ? range2.toString() : anchor?.textContent ?? "";
7818
8322
  const result = await __privateMethod(this, _FxEditor_instances, openDialog_fn).call(this, "Link", [
7819
8323
  { name: "url", label: "URL", type: "url", value: anchor?.getAttribute("href") ?? "", placeholder: "https://..." },
7820
8324
  { name: "text", label: "Texto (quando não há seleção)", value: selected, placeholder: "Texto do link" },
@@ -8037,13 +8541,13 @@ selectAnchor_fn = function(anchor) {
8037
8541
  const editor = this.editor;
8038
8542
  if (!editor) return;
8039
8543
  editor.focus({ preventScroll: true });
8040
- const range = document.createRange();
8041
- range.selectNodeContents(anchor);
8544
+ const range2 = document.createRange();
8545
+ range2.selectNodeContents(anchor);
8042
8546
  const selection = document.getSelection();
8043
8547
  if (!selection) return;
8044
8548
  selection.removeAllRanges();
8045
- selection.addRange(range);
8046
- __privateSet(this, _lastRange, range.cloneRange());
8549
+ selection.addRange(range2);
8550
+ __privateSet(this, _lastRange, range2.cloneRange());
8047
8551
  };
8048
8552
  handleLinkOverlayClick_fn = function(event) {
8049
8553
  const action = event.target.closest("[data-link-action]");
@@ -8314,19 +8818,19 @@ hidePanel_fn = function() {
8314
8818
  currentTable_fn = function() {
8315
8819
  const editor = this.editor;
8316
8820
  if (!editor) return null;
8317
- const range = getRange(editor, __privateGet(this, _lastRange));
8318
- return closestInEditor(editor, range?.startContainer ?? null, "table");
8821
+ const range2 = getRange(editor, __privateGet(this, _lastRange));
8822
+ return closestInEditor(editor, range2?.startContainer ?? null, "table");
8319
8823
  };
8320
8824
  tableAction_fn = function(action) {
8321
8825
  const table = __privateMethod(this, _FxEditor_instances, currentTable_fn).call(this);
8322
8826
  if (!table) return;
8323
8827
  const editor = this.editor;
8324
- const range = editor ? getRange(editor, __privateGet(this, _lastRange)) : null;
8325
- const cell = editor ? closestInEditor(editor, range?.startContainer ?? null, "td,th") : null;
8828
+ const range2 = editor ? getRange(editor, __privateGet(this, _lastRange)) : null;
8829
+ const cell = editor ? closestInEditor(editor, range2?.startContainer ?? null, "td,th") : null;
8326
8830
  const row = cell?.parentElement;
8327
8831
  if (action === "merge-cells") {
8328
- if (!range) return;
8329
- const merged = mergeSelectedCells(table, range);
8832
+ if (!range2) return;
8833
+ const merged = mergeSelectedCells(table, range2);
8330
8834
  if (!merged) return;
8331
8835
  placeCaretAtEnd(merged);
8332
8836
  __privateMethod(this, _FxEditor_instances, afterCommand_fn).call(this);
@@ -8477,10 +8981,61 @@ function defineFxEditor() {
8477
8981
  return defineElement("fx-editor", FxEditor);
8478
8982
  }
8479
8983
  defineFxEditor();
8984
+ const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
8985
+ function usable(nodes) {
8986
+ return [...nodes].filter(
8987
+ (el) => !el.hasAttribute("disabled") && el.getAttribute("aria-hidden") !== "true" && el.getAttribute("tabindex") !== "-1"
8988
+ );
8989
+ }
8990
+ function modalFocusables(panel, host) {
8991
+ const shadow = panel.querySelectorAll(FOCUSABLE_SELECTOR);
8992
+ const light = host.querySelectorAll(FOCUSABLE_SELECTOR);
8993
+ return usable([...shadow, ...light]);
8994
+ }
8995
+ function firstLightFocusable(host) {
8996
+ return usable(host.querySelectorAll(FOCUSABLE_SELECTOR))[0] ?? null;
8997
+ }
8998
+ const modalStack = [];
8999
+ function registerModalOpen(host) {
9000
+ if (!modalStack.includes(host)) modalStack.push(host);
9001
+ }
9002
+ function registerModalClose(host) {
9003
+ const index = modalStack.indexOf(host);
9004
+ if (index !== -1) modalStack.splice(index, 1);
9005
+ }
9006
+ function isTopmostModal(host) {
9007
+ if (modalStack.length === 0) return true;
9008
+ return modalStack[modalStack.length - 1] === host;
9009
+ }
9010
+ let scrollLocks = 0;
9011
+ let previousOverflow = "";
9012
+ let previousPaddingRight = "";
9013
+ function lockBodyScroll() {
9014
+ if (typeof document === "undefined" || !document.body) return;
9015
+ if (scrollLocks === 0) {
9016
+ const body = document.body;
9017
+ previousOverflow = body.style.overflow;
9018
+ previousPaddingRight = body.style.paddingRight;
9019
+ const scrollbar = window.innerWidth - document.documentElement.clientWidth;
9020
+ body.style.overflow = "hidden";
9021
+ if (scrollbar > 0) body.style.paddingRight = `${scrollbar}px`;
9022
+ }
9023
+ scrollLocks += 1;
9024
+ }
9025
+ function unlockBodyScroll() {
9026
+ if (typeof document === "undefined" || !document.body) return;
9027
+ if (scrollLocks === 0) return;
9028
+ scrollLocks -= 1;
9029
+ if (scrollLocks === 0) {
9030
+ document.body.style.overflow = previousOverflow;
9031
+ document.body.style.paddingRight = previousPaddingRight;
9032
+ }
9033
+ }
8480
9034
  const _FxDialog = class _FxDialog extends FxElement {
8481
9035
  constructor() {
8482
9036
  super(...arguments);
8483
9037
  this._previouslyFocused = null;
9038
+ this._locked = false;
8484
9039
  }
8485
9040
  static get observedAttributes() {
8486
9041
  return ["open", "size", "heading"];
@@ -8494,6 +9049,11 @@ const _FxDialog = class _FxDialog extends FxElement {
8494
9049
  render() {
8495
9050
  const isOpen = this.open;
8496
9051
  const heading = this.getAttr("heading");
9052
+ if (!isOpen) {
9053
+ this._release();
9054
+ this.setTemplate("");
9055
+ return;
9056
+ }
8497
9057
  this.setTemplate(`
8498
9058
  <div class="overlay" part="overlay" ${isOpen ? "" : "hidden"}>
8499
9059
  <div class="dialog" role="dialog" aria-modal="true" aria-label="${esc(heading)}" tabindex="-1">
@@ -8512,10 +9072,8 @@ const _FxDialog = class _FxDialog extends FxElement {
8512
9072
  const overlay = this.root.querySelector(".overlay");
8513
9073
  const closeBtn = this.root.querySelector(".close");
8514
9074
  const close = () => {
9075
+ if (!this.open) return;
8515
9076
  this.open = false;
8516
- this._cleanup?.();
8517
- this._cleanup = void 0;
8518
- this._restoreFocus();
8519
9077
  this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
8520
9078
  };
8521
9079
  overlay?.addEventListener("click", (e) => {
@@ -8524,6 +9082,7 @@ const _FxDialog = class _FxDialog extends FxElement {
8524
9082
  closeBtn?.addEventListener("click", close);
8525
9083
  const onKey = (e) => {
8526
9084
  if (e.key === "Escape") {
9085
+ if (!isTopmostModal(this)) return;
8527
9086
  e.stopPropagation();
8528
9087
  close();
8529
9088
  return;
@@ -8532,29 +9091,48 @@ const _FxDialog = class _FxDialog extends FxElement {
8532
9091
  };
8533
9092
  document.addEventListener("keydown", onKey);
8534
9093
  this._cleanup = () => document.removeEventListener("keydown", onKey);
8535
- requestAnimationFrame(() => {
8536
- this._previouslyFocused = document.activeElement;
8537
- this.root.querySelector(".dialog")?.focus();
8538
- });
9094
+ const alreadyOpen = this._locked;
9095
+ if (!this._locked) {
9096
+ lockBodyScroll();
9097
+ registerModalOpen(this);
9098
+ this._locked = true;
9099
+ }
9100
+ if (!alreadyOpen) {
9101
+ requestAnimationFrame(() => {
9102
+ if (!this.open) return;
9103
+ this._previouslyFocused = document.activeElement;
9104
+ const first = firstLightFocusable(this) ?? this.root.querySelector(".dialog");
9105
+ first?.focus();
9106
+ });
9107
+ }
8539
9108
  this.dispatchEvent(new CustomEvent("open", { bubbles: true, composed: true }));
8540
9109
  }
8541
- /** Mantém o foco dentro do modal (WCAG 2.4.3 / 2.1.2). */
9110
+ /** Desfaz listeners, foco e scroll lock ao fechar/desconectar. */
9111
+ _release() {
9112
+ this._cleanup?.();
9113
+ this._cleanup = void 0;
9114
+ registerModalClose(this);
9115
+ if (this._locked) {
9116
+ unlockBodyScroll();
9117
+ this._locked = false;
9118
+ }
9119
+ this._restoreFocus();
9120
+ }
9121
+ /** Mantém o foco dentro do modal (WCAG 2.4.3 / 2.1.2), incluindo o slot. */
8542
9122
  _trapFocus(e) {
8543
9123
  const dialog = this.root.querySelector(".dialog");
8544
9124
  if (!dialog) return;
8545
- const focusables = dialog.querySelectorAll(
8546
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
8547
- );
9125
+ const focusables = modalFocusables(dialog, this);
8548
9126
  if (!focusables.length) return;
8549
9127
  const first = focusables[0];
8550
9128
  const last = focusables[focusables.length - 1];
8551
- const active = this.root.activeElement;
8552
- if (e.shiftKey && (active === first || active === dialog)) {
9129
+ const active = this.root.activeElement ?? document.activeElement;
9130
+ if (e.shiftKey && (active === first || active === dialog || active === this)) {
8553
9131
  e.preventDefault();
8554
- last.focus();
9132
+ last?.focus();
8555
9133
  } else if (!e.shiftKey && active === last) {
8556
9134
  e.preventDefault();
8557
- first.focus();
9135
+ first?.focus();
8558
9136
  }
8559
9137
  }
8560
9138
  _restoreFocus() {
@@ -8563,9 +9141,7 @@ const _FxDialog = class _FxDialog extends FxElement {
8563
9141
  }
8564
9142
  disconnectedCallback() {
8565
9143
  super.disconnectedCallback();
8566
- this._cleanup?.();
8567
- this._cleanup = void 0;
8568
- this._restoreFocus();
9144
+ this._release();
8569
9145
  }
8570
9146
  };
8571
9147
  _FxDialog.styles = css`
@@ -8971,7 +9547,35 @@ const _FxFileUpload = class _FxFileUpload extends FxFormElement {
8971
9547
  return this.getAttr("value");
8972
9548
  }
8973
9549
  set value(v) {
8974
- this.setAttribute("value", v);
9550
+ this.setAttribute("value", v == null ? "" : String(v));
9551
+ }
9552
+ get disabled() {
9553
+ return this.hasAttr("disabled");
9554
+ }
9555
+ set disabled(v) {
9556
+ this.toggleAttr("disabled", Boolean(v));
9557
+ }
9558
+ get multiple() {
9559
+ return this.hasAttr("multiple");
9560
+ }
9561
+ set multiple(v) {
9562
+ this.toggleAttr("multiple", Boolean(v));
9563
+ }
9564
+ get full() {
9565
+ return this.hasAttr("full");
9566
+ }
9567
+ set full(v) {
9568
+ this.toggleAttr("full", Boolean(v));
9569
+ }
9570
+ /**
9571
+ * Nome literal: o Vue NÃO cameliza atributos em elementos customizados,
9572
+ * então `:show-progress` procura a propriedade `'show-progress'`.
9573
+ */
9574
+ get "show-progress"() {
9575
+ return this.hasAttr("show-progress");
9576
+ }
9577
+ set "show-progress"(v) {
9578
+ this.toggleAttr("show-progress", Boolean(v));
8975
9579
  }
8976
9580
  get progress() {
8977
9581
  return Math.min(100, Math.max(0, Number(this.getAttr("progress", "0")) || 0));
@@ -9652,6 +10256,9 @@ const _FxAvatar = class _FxAvatar extends FxElement {
9652
10256
  get variant() {
9653
10257
  return this.getAttr("variant", "text");
9654
10258
  }
10259
+ set variant(v) {
10260
+ this.setAttribute("variant", v);
10261
+ }
9655
10262
  get src() {
9656
10263
  return this.getAttr("src");
9657
10264
  }
@@ -9755,6 +10362,18 @@ const _FxIcon = class _FxIcon extends FxElement {
9755
10362
  if (value) this.setAttribute("label", value);
9756
10363
  else this.removeAttribute("label");
9757
10364
  }
10365
+ get fill() {
10366
+ return this.hasAttr("fill");
10367
+ }
10368
+ set fill(value) {
10369
+ this.toggleAttr("fill", Boolean(value));
10370
+ }
10371
+ get bold() {
10372
+ return this.hasAttr("bold");
10373
+ }
10374
+ set bold(value) {
10375
+ this.toggleAttr("bold", Boolean(value));
10376
+ }
9758
10377
  connectedCallback() {
9759
10378
  loadFenixIconsFont();
9760
10379
  super.connectedCallback();
@@ -9801,6 +10420,12 @@ const _FxCard = class _FxCard extends FxElement {
9801
10420
  static get observedAttributes() {
9802
10421
  return ["variant", "radius", "size", "padded", "heading"];
9803
10422
  }
10423
+ get padded() {
10424
+ return this.hasAttr("padded");
10425
+ }
10426
+ set padded(value) {
10427
+ this.toggleAttr("padded", Boolean(value));
10428
+ }
9804
10429
  render() {
9805
10430
  const heading = this.getAttr("heading");
9806
10431
  this.setTemplate(`
@@ -12321,6 +12946,40 @@ const _FxTabs = class _FxTabs extends FxElement {
12321
12946
  set value(value) {
12322
12947
  this.setAttribute("value", value);
12323
12948
  }
12949
+ connectedCallback() {
12950
+ super.connectedCallback();
12951
+ this._observeLightDom();
12952
+ }
12953
+ disconnectedCallback() {
12954
+ super.disconnectedCallback();
12955
+ this._lightObserver?.disconnect();
12956
+ this._siblingObserver?.disconnect();
12957
+ this._lightObserver = void 0;
12958
+ this._siblingObserver = void 0;
12959
+ }
12960
+ _observeLightDom() {
12961
+ this._lightObserver?.disconnect();
12962
+ this._lightObserver = new MutationObserver(() => this.render());
12963
+ this._lightObserver.observe(this, {
12964
+ childList: true,
12965
+ subtree: true,
12966
+ attributes: true,
12967
+ attributeFilter: ["tab", "disabled"]
12968
+ });
12969
+ this._siblingObserver?.disconnect();
12970
+ const parent = this.parentNode;
12971
+ if (parent) {
12972
+ this._siblingObserver = new MutationObserver((mutations) => {
12973
+ const affectsPanels = mutations.some(
12974
+ (m) => [...m.addedNodes, ...m.removedNodes].some(
12975
+ (node) => node.nodeType === Node.ELEMENT_NODE && (node.matches("fx-tab-panel") || Boolean(node.querySelector("fx-tab-panel")))
12976
+ )
12977
+ );
12978
+ if (affectsPanels) this.render();
12979
+ });
12980
+ this._siblingObserver.observe(parent, { childList: true });
12981
+ }
12982
+ }
12324
12983
  render() {
12325
12984
  const tabs = Array.from(this.querySelectorAll("fx-tab"));
12326
12985
  const active = this.value || tabs[0]?.getAttribute("tab") || "";
@@ -12849,10 +13508,11 @@ const _FxDrawer = class _FxDrawer extends FxElement {
12849
13508
  this._onKeydown = () => {
12850
13509
  };
12851
13510
  this._previouslyFocused = null;
13511
+ this._locked = false;
13512
+ this._wasOpen = false;
12852
13513
  this._close = () => {
12853
13514
  if (!this.open) return;
12854
13515
  this.open = false;
12855
- this._restoreFocus();
12856
13516
  this.dispatchEvent(
12857
13517
  new CustomEvent("close", { bubbles: true, composed: true })
12858
13518
  );
@@ -12882,6 +13542,8 @@ const _FxDrawer = class _FxDrawer extends FxElement {
12882
13542
  }
12883
13543
  render() {
12884
13544
  const title = this.getAttr("title");
13545
+ const wasOpen = this._wasOpen;
13546
+ this._wasOpen = this.open;
12885
13547
  this.setTemplate(`
12886
13548
  <div class="overlay" part="overlay"></div>
12887
13549
  <div class="panel" part="panel" role="dialog" aria-modal="true" aria-label="${esc(title || "Drawer")}" tabindex="-1">
@@ -12892,20 +13554,40 @@ const _FxDrawer = class _FxDrawer extends FxElement {
12892
13554
  this.root.querySelector(".overlay")?.addEventListener("click", this._close);
12893
13555
  this.root.querySelector(".close")?.addEventListener("click", this._close);
12894
13556
  if (this.open) {
12895
- requestAnimationFrame(() => {
12896
- if (this._previouslyFocused === null) {
13557
+ if (!this._locked) {
13558
+ lockBodyScroll();
13559
+ registerModalOpen(this);
13560
+ this._locked = true;
13561
+ }
13562
+ if (!wasOpen) {
13563
+ requestAnimationFrame(() => {
13564
+ if (!this.open) return;
12897
13565
  this._previouslyFocused = document.activeElement;
12898
- }
12899
- this.root.querySelector(".panel")?.focus();
12900
- });
13566
+ const first = firstLightFocusable(this) ?? this.root.querySelector(".panel");
13567
+ first?.focus();
13568
+ });
13569
+ }
13570
+ } else {
13571
+ this._release();
13572
+ }
13573
+ }
13574
+ /** Desfaz scroll lock e restaura o foco ao fechar/desconectar. */
13575
+ _release() {
13576
+ registerModalClose(this);
13577
+ if (this._locked) {
13578
+ unlockBodyScroll();
13579
+ this._locked = false;
12901
13580
  }
13581
+ this._restoreFocus();
12902
13582
  }
12903
13583
  connectedCallback() {
12904
13584
  super.connectedCallback?.();
12905
13585
  this._onKeydown = (e) => {
12906
- if (e.key === "Escape" && this.open) {
13586
+ if (!this.open) return;
13587
+ if (e.key === "Escape") {
13588
+ if (!isTopmostModal(this)) return;
12907
13589
  this._close();
12908
- } else if (e.key === "Tab" && this.open) {
13590
+ } else if (e.key === "Tab") {
12909
13591
  this._trapFocus(e);
12910
13592
  }
12911
13593
  };
@@ -12913,26 +13595,24 @@ const _FxDrawer = class _FxDrawer extends FxElement {
12913
13595
  }
12914
13596
  disconnectedCallback() {
12915
13597
  document.removeEventListener("keydown", this._onKeydown);
12916
- this._restoreFocus();
13598
+ this._release();
12917
13599
  super.disconnectedCallback?.();
12918
13600
  }
12919
- /** Mantém o foco dentro do drawer (WCAG 2.4.3 / 2.1.2). */
13601
+ /** Mantém o foco dentro do drawer (WCAG 2.4.3 / 2.1.2), incluindo o slot. */
12920
13602
  _trapFocus(e) {
12921
13603
  const panel = this.root.querySelector(".panel");
12922
13604
  if (!panel) return;
12923
- const focusables = panel.querySelectorAll(
12924
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
12925
- );
13605
+ const focusables = modalFocusables(panel, this);
12926
13606
  if (!focusables.length) return;
12927
13607
  const first = focusables[0];
12928
13608
  const last = focusables[focusables.length - 1];
12929
- const active = this.root.activeElement;
12930
- if (e.shiftKey && (active === first || active === panel)) {
13609
+ const active = this.root.activeElement ?? document.activeElement;
13610
+ if (e.shiftKey && (active === first || active === panel || active === this)) {
12931
13611
  e.preventDefault();
12932
- last.focus();
13612
+ last?.focus();
12933
13613
  } else if (!e.shiftKey && active === last) {
12934
13614
  e.preventDefault();
12935
- first.focus();
13615
+ first?.focus();
12936
13616
  }
12937
13617
  }
12938
13618
  _restoreFocus() {
@@ -13101,17 +13781,16 @@ const _FxPagination = class _FxPagination extends FxElement {
13101
13781
  const pages = this.pages;
13102
13782
  const current = Math.min(Math.max(1, this.page), pages);
13103
13783
  const opts = (this.getAttr("rows-options", "5,10,20,50") || "").split(",").map((s) => s.trim()).filter(Boolean);
13104
- const window_ = [];
13105
- for (let p = Math.max(1, current - 2); p <= Math.min(pages, current + 2); p++) window_.push(p);
13106
- const from = total0(current, this.rows);
13784
+ const pageButtons = pageRange(current, pages).map(
13785
+ (item) => item === "ellipsis" ? `<span class="ellipsis" part="ellipsis" aria-hidden="true">…</span>` : `<button type="button" class="nav ${item === current ? "active" : ""}" part="page" data-go="${item}"${item === current ? ' aria-current="page"' : ""}>${item}</button>`
13786
+ ).join("");
13787
+ const from = (current - 1) * this.rows + 1;
13107
13788
  const to = Math.min(current * this.rows, this.total);
13108
13789
  this.setTemplate(`
13109
13790
  <span class="info">${this.total ? `${from}-${to} de ${this.total}` : "0 itens"}</span>
13110
13791
  <button type="button" class="nav" part="first" aria-label="Primeira página" data-go="1" ${current <= 1 ? "disabled" : ""}>${fenixIconHtml("first_page")}</button>
13111
13792
  <button type="button" class="nav" part="prev" aria-label="Página anterior" data-go="${current - 1}" ${current <= 1 ? "disabled" : ""}>${fenixIconHtml("navigate_before")}</button>
13112
- ${window_[0] !== 1 && pages > 5 ? `<button type="button" class="nav" data-go="1">1</button>` : ""}
13113
- ${window_.map((p) => `<button type="button" class="nav ${p === current ? "active" : ""}" data-go="${p}">${p}</button>`).join("")}
13114
- ${window_[window_.length - 1] !== pages && pages > 5 ? `<button type="button" class="nav" data-go="${pages}">${pages}</button>` : ""}
13793
+ ${pageButtons}
13115
13794
  <button type="button" class="nav" part="next" aria-label="Próxima página" data-go="${current + 1}" ${current >= pages ? "disabled" : ""}>${fenixIconHtml("navigate_next")}</button>
13116
13795
  <button type="button" class="nav" part="last" aria-label="Última página" data-go="${pages}" ${current >= pages ? "disabled" : ""}>${fenixIconHtml("last_page")}</button>
13117
13796
  ${opts.length ? `
@@ -13119,9 +13798,6 @@ const _FxPagination = class _FxPagination extends FxElement {
13119
13798
  ${opts.map((o) => `<option value="${esc(o)}">${esc(o)}</option>`).join("")}
13120
13799
  </fx-select>` : ""}
13121
13800
  `);
13122
- function total0(page, rows) {
13123
- return (page - 1) * rows + 1;
13124
- }
13125
13801
  this.root.querySelectorAll(".nav[data-go]").forEach((btn) => {
13126
13802
  btn.addEventListener("click", () => {
13127
13803
  const target = Number(btn.dataset.go);
@@ -13153,6 +13829,7 @@ _FxPagination.styles = css`
13153
13829
  :host {
13154
13830
  display: flex;
13155
13831
  align-items: center;
13832
+ flex-wrap: wrap;
13156
13833
  gap: var(--fx-space-sm);
13157
13834
  font-family: var(--fx-font-family);
13158
13835
  font-size: var(--fx-font-size);
@@ -13190,6 +13867,18 @@ _FxPagination.styles = css`
13190
13867
  color: #fff;
13191
13868
  font-weight: 600;
13192
13869
  }
13870
+ /* Elipse entre a janela de páginas e as bordas: ocupa o mesmo espaço de
13871
+ um botão (para o rodapé não "pular" ao navegar) mas não é clicável. */
13872
+ .ellipsis {
13873
+ display: inline-flex;
13874
+ align-items: center;
13875
+ justify-content: center;
13876
+ min-width: var(--fx-size-sm);
13877
+ height: var(--fx-size-sm);
13878
+ color: var(--fx-text-muted);
13879
+ font-size: calc(var(--fx-font-size) - 2px);
13880
+ user-select: none;
13881
+ }
13193
13882
  .info { color: var(--fx-text-muted); font-size: calc(var(--fx-font-size) - 2px); }
13194
13883
  fx-select { vertical-align: middle; }
13195
13884
  fx-select::part(trigger) {
@@ -13216,11 +13905,17 @@ const _FxAutocomplete = class _FxAutocomplete extends FxFormElement {
13216
13905
  set size(v) {
13217
13906
  this.setAttribute("size", v);
13218
13907
  }
13908
+ get disabled() {
13909
+ return this.hasAttr("disabled");
13910
+ }
13911
+ set disabled(v) {
13912
+ this.toggleAttr("disabled", Boolean(v));
13913
+ }
13219
13914
  get value() {
13220
13915
  return this.getAttr("value");
13221
13916
  }
13222
13917
  set value(v) {
13223
- this.setAttribute("value", v);
13918
+ this.setAttribute("value", v == null ? "" : String(v));
13224
13919
  this.syncInternalControl();
13225
13920
  this.updateFormValue();
13226
13921
  }
@@ -13525,8 +14220,8 @@ const _FxKnob = class _FxKnob extends FxFormElement {
13525
14220
  const min = this.min;
13526
14221
  const max = this.max;
13527
14222
  const value = this.clampValue(this.value);
13528
- const range = max - min;
13529
- const pct = range > 0 ? (value - min) / range : 0;
14223
+ const range2 = max - min;
14224
+ const pct = range2 > 0 ? (value - min) / range2 : 0;
13530
14225
  const sizeNum = this.getSizeNum();
13531
14226
  const strokeW = this.strokeWidth;
13532
14227
  const radius = (sizeNum - strokeW) / 2;
@@ -13754,6 +14449,7 @@ const _FxOrderList = class _FxOrderList extends FxElement {
13754
14449
  static get observedAttributes() {
13755
14450
  return [
13756
14451
  "data",
14452
+ "items",
13757
14453
  "data-key",
13758
14454
  "filter",
13759
14455
  "filter-by",
@@ -13767,13 +14463,35 @@ const _FxOrderList = class _FxOrderList extends FxElement {
13767
14463
  ];
13768
14464
  }
13769
14465
  set data(value) {
13770
- this._data = value;
14466
+ this._data = Array.isArray(value) ? value : [];
13771
14467
  this._filteredData = null;
13772
14468
  this.render();
13773
14469
  }
13774
14470
  get data() {
13775
14471
  return this._data;
13776
14472
  }
14473
+ /**
14474
+ * Alias de `data` para o binding do Vue/React: `data` colide com a
14475
+ * tipagem nativa de atributos HTML (`dataset`/`data-*`) e não reflete em
14476
+ * atributo. `items` funciona como propriedade e como atributo JSON.
14477
+ */
14478
+ get items() {
14479
+ return this._data;
14480
+ }
14481
+ set items(value) {
14482
+ const list = Array.isArray(value) ? value : [];
14483
+ this._data = list;
14484
+ this._filteredData = null;
14485
+ const json = (() => {
14486
+ try {
14487
+ return JSON.stringify(list);
14488
+ } catch {
14489
+ return null;
14490
+ }
14491
+ })();
14492
+ if (json !== null && this.getAttr("items") !== json) this.setAttribute("items", json);
14493
+ else if (this.isConnected) this.render();
14494
+ }
13777
14495
  set selection(value) {
13778
14496
  this._selection = value;
13779
14497
  this.render();
@@ -13853,7 +14571,16 @@ const _FxOrderList = class _FxOrderList extends FxElement {
13853
14571
  super.connectedCallback();
13854
14572
  }
13855
14573
  attributeChangedCallback(name, oldValue, newValue) {
13856
- if (this.getAttr("data", "") !== "") {
14574
+ if (name === "items") {
14575
+ try {
14576
+ const parsed = JSON.parse(newValue ?? "[]");
14577
+ this._data = Array.isArray(parsed) ? parsed : [];
14578
+ this._filteredData = null;
14579
+ } catch {
14580
+ this._data = [];
14581
+ this._filteredData = null;
14582
+ }
14583
+ } else if (this.getAttr("data", "") !== "") {
13857
14584
  this._parseDataAttribute();
13858
14585
  }
13859
14586
  super.attributeChangedCallback(name, oldValue, newValue);