@wrrdev/fenix-ui 1.1.16 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +12 -3
  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 +20 -0
  43. package/dist/components/floatlabel/floatlabel.d.ts.map +1 -1
  44. package/dist/components/floatlabel/floatlabel.js +99 -26
  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/radio/radio.d.ts +6 -0
  63. package/dist/components/radio/radio.d.ts.map +1 -1
  64. package/dist/components/radio/radio.js +12 -6
  65. package/dist/components/radio/radio.js.map +1 -1
  66. package/dist/components/select/select.d.ts +19 -1
  67. package/dist/components/select/select.d.ts.map +1 -1
  68. package/dist/components/select/select.js +116 -10
  69. package/dist/components/select/select.js.map +1 -1
  70. package/dist/components/switch/switch.d.ts +4 -1
  71. package/dist/components/switch/switch.d.ts.map +1 -1
  72. package/dist/components/switch/switch.js +2 -1
  73. package/dist/components/switch/switch.js.map +1 -1
  74. package/dist/components/table/table.d.ts +27 -0
  75. package/dist/components/table/table.d.ts.map +1 -1
  76. package/dist/components/table/table.js +119 -21
  77. package/dist/components/table/table.js.map +1 -1
  78. package/dist/components/tabs/tabs.d.ts +6 -0
  79. package/dist/components/tabs/tabs.d.ts.map +1 -1
  80. package/dist/components/tabs/tabs.js +34 -0
  81. package/dist/components/tabs/tabs.js.map +1 -1
  82. package/dist/components/textarea/textarea.d.ts +5 -1
  83. package/dist/components/textarea/textarea.d.ts.map +1 -1
  84. package/dist/components/textarea/textarea.js +25 -6
  85. package/dist/components/textarea/textarea.js.map +1 -1
  86. package/dist/core/jsx.d.ts +11 -5
  87. package/dist/core/jsx.d.ts.map +1 -1
  88. package/dist/core/jsx.js.map +1 -1
  89. package/dist/core/modal.d.ts +31 -0
  90. package/dist/core/modal.d.ts.map +1 -0
  91. package/dist/core/modal.js +61 -0
  92. package/dist/core/modal.js.map +1 -0
  93. package/dist/core/tokens.d.ts +2 -0
  94. package/dist/core/tokens.d.ts.map +1 -1
  95. package/dist/core/tokens.js +9 -0
  96. package/dist/core/tokens.js.map +1 -1
  97. package/dist/docs/app.d.ts.map +1 -1
  98. package/dist/docs/componentes/floatlabel.doc.d.ts.map +1 -1
  99. package/dist/docs/componentes/input.doc.d.ts.map +1 -1
  100. package/dist/docs/componentes/orderlist.doc.d.ts.map +1 -1
  101. package/dist/docs/componentes/table.doc.d.ts.map +1 -1
  102. package/dist/docs/shell.d.ts +10 -1
  103. package/dist/docs/shell.d.ts.map +1 -1
  104. package/dist/docs/theme-reference.d.ts +53 -0
  105. package/dist/docs/theme-reference.d.ts.map +1 -0
  106. package/dist/docs/types.d.ts +2 -0
  107. package/dist/docs/types.d.ts.map +1 -1
  108. package/dist/fenix-ui.umd.min.js +270 -233
  109. package/dist/fenix-ui4.esm.js +890 -176
  110. package/dist/fenix-ui4.esm.js.map +1 -1
  111. package/package.json +1 -1
@@ -73,6 +73,14 @@ const lightTokens = {
73
73
  /** Brilho dos estados `success`/`valid` — acompanha `focus-ring` igualmente. */
74
74
  "success-ring": "0 0 0 3px color-mix(in srgb, var(--fx-color-success, #16a34a) 18%, transparent)"
75
75
  },
76
+ table: {
77
+ /** Cor do corpo do fx-table; `var()` mantém claro/escuro automáticos. */
78
+ text: "var(--fx-text-default)",
79
+ /** Cabeçalho acompanha o corpo por padrão. */
80
+ "header-text": "var(--fx-table-text)",
81
+ /** O UA aplica bold no <th>; aqui o valor fica explícito e tokenizado. */
82
+ "header-weight": "bold"
83
+ },
76
84
  z: { base: "auto", dropdown: "1000", modal: "1100", toast: "1200" }
77
85
  };
78
86
  const darkTokens = {
@@ -99,6 +107,7 @@ const darkTokens = {
99
107
  },
100
108
  motion: { ...lightTokens.motion },
101
109
  effect: { ...lightTokens.effect },
110
+ table: { ...lightTokens.table },
102
111
  z: { ...lightTokens.z }
103
112
  };
104
113
  const defaultTokens = lightTokens;
@@ -506,6 +515,12 @@ const _FxButton = class _FxButton extends FxElement {
506
515
  set loading(value) {
507
516
  this.toggleAttr("loading", Boolean(value));
508
517
  }
518
+ get full() {
519
+ return this.hasAttr("full");
520
+ }
521
+ set full(value) {
522
+ this.toggleAttr("full", Boolean(value));
523
+ }
509
524
  render() {
510
525
  const loading = this.hasAttr("loading");
511
526
  const disabled = this.hasAttr("disabled") || loading;
@@ -685,6 +700,18 @@ const _FxBadge = class _FxBadge extends FxElement {
685
700
  static get observedAttributes() {
686
701
  return ["variant", "round"];
687
702
  }
703
+ get variant() {
704
+ return this.getAttr("variant", "default");
705
+ }
706
+ set variant(v) {
707
+ this.setAttribute("variant", v);
708
+ }
709
+ get round() {
710
+ return this.hasAttr("round");
711
+ }
712
+ set round(v) {
713
+ this.toggleAttr("round", Boolean(v));
714
+ }
688
715
  render() {
689
716
  this.setTemplate(`<span class="badge" part="badge"><slot></slot></span>`);
690
717
  }
@@ -764,10 +791,21 @@ function defineFxSpinner() {
764
791
  }
765
792
  defineFxSpinner();
766
793
  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.
794
+ constructor() {
795
+ super(...arguments);
796
+ this._suppressRender = false;
797
+ this._fixedMode = false;
798
+ this._fixedResolved = false;
799
+ }
800
+ // `value` é observado para que `setAttribute('value', ...)` (HTML puro)
801
+ // também atualize o rótulo. Os caminhos internos de change usam
802
+ // `_suppressRender` para renderizar UMA vez, depois de fechar o painel.
769
803
  static get observedAttributes() {
770
- return ["size", "disabled", "placeholder", "searchable", "clearable", "error", "invalid", "success", "valid", "placement"];
804
+ return ["value", "size", "disabled", "placeholder", "searchable", "clearable", "full", "error", "invalid", "success", "valid", "placement"];
805
+ }
806
+ attributeChangedCallback(name, oldValue, newValue) {
807
+ if (this._suppressRender) return;
808
+ super.attributeChangedCallback(name, oldValue, newValue);
771
809
  }
772
810
  /** Tamanho do campo. Padrão: `'md'`. */
773
811
  get size() {
@@ -781,7 +819,9 @@ const _FxSelect = class _FxSelect extends FxFormElement {
781
819
  return this.getAttr("value");
782
820
  }
783
821
  set value(value) {
784
- this.setAttribute("value", value);
822
+ const next = value == null ? "" : String(value);
823
+ const prev = this.getAttr("value");
824
+ if (prev !== next) this.setAttribute("value", next);
785
825
  this.updateFormValue();
786
826
  }
787
827
  get disabled() {
@@ -790,6 +830,24 @@ const _FxSelect = class _FxSelect extends FxFormElement {
790
830
  set disabled(value) {
791
831
  this.toggleAttr("disabled", Boolean(value));
792
832
  }
833
+ get searchable() {
834
+ return this.hasAttr("searchable");
835
+ }
836
+ set searchable(value) {
837
+ this.toggleAttr("searchable", Boolean(value));
838
+ }
839
+ get clearable() {
840
+ return this.hasAttr("clearable");
841
+ }
842
+ set clearable(value) {
843
+ this.toggleAttr("clearable", Boolean(value));
844
+ }
845
+ get full() {
846
+ return this.hasAttr("full");
847
+ }
848
+ set full(value) {
849
+ this.toggleAttr("full", Boolean(value));
850
+ }
793
851
  get options() {
794
852
  return [...this.querySelectorAll("option")].map((o) => ({
795
853
  value: o.getAttribute("value") ?? o.textContent?.trim() ?? "",
@@ -798,8 +856,17 @@ const _FxSelect = class _FxSelect extends FxFormElement {
798
856
  }
799
857
  connectedCallback() {
800
858
  super.connectedCallback();
801
- this.observer = new MutationObserver(() => this.render());
802
- this.observer.observe(this, { childList: true, subtree: true, characterData: true });
859
+ this.observer = new MutationObserver((mutations) => {
860
+ if (mutations.every((m) => m.type === "attributes" && m.target === this)) return;
861
+ this.render();
862
+ });
863
+ this.observer.observe(this, {
864
+ childList: true,
865
+ subtree: true,
866
+ characterData: true,
867
+ attributes: true,
868
+ attributeFilter: ["value", "selected"]
869
+ });
803
870
  this.docListener = (e) => {
804
871
  if (!this.hasAttr("open")) return;
805
872
  if (e.composedPath().includes(this)) return;
@@ -816,6 +883,8 @@ const _FxSelect = class _FxSelect extends FxFormElement {
816
883
  }
817
884
  /** Abre o dropdown, posiciona o painel e passa a reposicioná-lo no reflow. */
818
885
  _abrir() {
886
+ this._fixedMode = _FxSelect._precisaFixed(this);
887
+ this._fixedResolved = true;
819
888
  this.setAttribute("open", "");
820
889
  this._bindReflow();
821
890
  }
@@ -823,6 +892,8 @@ const _FxSelect = class _FxSelect extends FxFormElement {
823
892
  _fechar() {
824
893
  this.removeAttribute("open");
825
894
  this.removeAttribute("data-placement");
895
+ this._fixedMode = false;
896
+ this._fixedResolved = false;
826
897
  this._unbindReflow();
827
898
  }
828
899
  /** Liga o reposicionamento (resize + scroll interno, em capture). */
@@ -865,7 +936,30 @@ const _FxSelect = class _FxSelect extends FxFormElement {
865
936
  _FxSelect.MIN_ALTURA,
866
937
  (paraCima ? acima : abaixo) - _FxSelect.GAP
867
938
  );
868
- painel.style.maxHeight = `${Math.min(desejado || disponivel, disponivel, maxAltura)}px`;
939
+ const altura = Math.min(desejado || disponivel, disponivel, maxAltura);
940
+ painel.style.maxHeight = `${altura}px`;
941
+ if (!this._fixedResolved) {
942
+ this._fixedMode = _FxSelect._precisaFixed(this);
943
+ this._fixedResolved = true;
944
+ }
945
+ if (this._fixedMode) {
946
+ this.setAttribute("data-fixed", "");
947
+ const largura2 = Math.max(r.width, 220);
948
+ const esquerda = Math.max(
949
+ _FxSelect.MARGEM,
950
+ Math.min(r.left, window.innerWidth - largura2 - _FxSelect.MARGEM)
951
+ );
952
+ const topo = paraCima ? Math.max(_FxSelect.MARGEM, r.top - _FxSelect.GAP - altura) : r.bottom + _FxSelect.GAP;
953
+ painel.style.bottom = "auto";
954
+ painel.style.width = `${largura2}px`;
955
+ painel.style.left = `${esquerda}px`;
956
+ painel.style.top = `${topo}px`;
957
+ return;
958
+ }
959
+ this.removeAttribute("data-fixed");
960
+ painel.style.top = "";
961
+ painel.style.bottom = "";
962
+ painel.style.width = "";
869
963
  painel.style.left = "";
870
964
  const largura = painel.offsetWidth;
871
965
  const excesso = r.left + largura - (window.innerWidth - _FxSelect.MARGEM);
@@ -874,6 +968,21 @@ const _FxSelect = class _FxSelect extends FxFormElement {
874
968
  painel.style.right = "auto";
875
969
  }
876
970
  }
971
+ /**
972
+ * Existe ancestral com overflow que cortaria o painel absoluto?
973
+ * `getComputedStyle` é defensivo: em jsdom retorna vazio e o modo
974
+ * histórico (absoluto) é mantido.
975
+ */
976
+ static _precisaFixed(host) {
977
+ let el = host.parentElement;
978
+ while (el && el !== document.body && el !== document.documentElement) {
979
+ const cs = getComputedStyle(el);
980
+ const overflow = `${cs.overflow}${cs.overflowX}${cs.overflowY}`;
981
+ if (/(hidden|clip|auto|scroll|overlay)/.test(overflow)) return true;
982
+ el = el.parentElement;
983
+ }
984
+ return false;
985
+ }
877
986
  /** Altura máxima do painel a partir de `--fx-select-panel-max-height`. */
878
987
  static _maxAltura(host) {
879
988
  const raw = getComputedStyle(host).getPropertyValue("--fx-select-panel-max-height");
@@ -881,7 +990,12 @@ const _FxSelect = class _FxSelect extends FxFormElement {
881
990
  return Number.isFinite(n) && n > 0 ? n : 260;
882
991
  }
883
992
  select(value) {
884
- this.value = value;
993
+ this._suppressRender = true;
994
+ try {
995
+ this.value = value;
996
+ } finally {
997
+ this._suppressRender = false;
998
+ }
885
999
  this._fechar();
886
1000
  this.render();
887
1001
  this.emitValueEvents();
@@ -908,6 +1022,10 @@ const _FxSelect = class _FxSelect extends FxFormElement {
908
1022
  }
909
1023
  render() {
910
1024
  const prevOpen = this.hasAttr("open");
1025
+ if (!prevOpen) {
1026
+ this._fixedMode = false;
1027
+ this._fixedResolved = false;
1028
+ }
911
1029
  const prevSearch = this.root.querySelector(".search");
912
1030
  const search = prevSearch?.value ?? "";
913
1031
  const searchFocused = prevSearch != null && this.root.activeElement === prevSearch;
@@ -984,7 +1102,12 @@ const _FxSelect = class _FxSelect extends FxFormElement {
984
1102
  });
985
1103
  this.root.querySelector(".clear")?.addEventListener("click", (e) => {
986
1104
  e.stopPropagation();
987
- this.value = "";
1105
+ this._suppressRender = true;
1106
+ try {
1107
+ this.value = "";
1108
+ } finally {
1109
+ this._suppressRender = false;
1110
+ }
988
1111
  this._fechar();
989
1112
  this.render();
990
1113
  this.emitValueEvents();
@@ -999,7 +1122,12 @@ const _FxSelect = class _FxSelect extends FxFormElement {
999
1122
  }
1000
1123
  /** form.reset(): restaura o valor inicial e re-renderiza o rótulo. */
1001
1124
  formResetCallback() {
1002
- super.formResetCallback();
1125
+ this._suppressRender = true;
1126
+ try {
1127
+ super.formResetCallback();
1128
+ } finally {
1129
+ this._suppressRender = false;
1130
+ }
1003
1131
  this.render();
1004
1132
  }
1005
1133
  };
@@ -1122,6 +1250,11 @@ _FxSelect.styles = css`
1122
1250
  border-radius: var(--fx-radius-md);
1123
1251
  box-shadow: var(--fx-shadow-lg);
1124
1252
  }
1253
+ /* Ancestral com overflow (ex.: fx-card) cortaria o painel absoluto:
1254
+ nesse caso o JS posiciona com position:fixed a partir do trigger. */
1255
+ :host([data-fixed]) .panel {
1256
+ position: fixed;
1257
+ }
1125
1258
  /* Padrão: abre para baixo. */
1126
1259
  :host([open]:not([data-placement='top'])) .panel {
1127
1260
  top: calc(100% + var(--fx-select-panel-offset, 4px));
@@ -1187,7 +1320,21 @@ function defineFxSelect() {
1187
1320
  return defineElement("fx-select", FxSelect);
1188
1321
  }
1189
1322
  defineFxSelect();
1190
- const INPUT_TYPES = /* @__PURE__ */ new Set(["text", "number", "password"]);
1323
+ const INPUT_TYPES = /* @__PURE__ */ new Set([
1324
+ "text",
1325
+ "number",
1326
+ "password",
1327
+ "email",
1328
+ "tel",
1329
+ "url",
1330
+ "search",
1331
+ "date",
1332
+ "time",
1333
+ "datetime-local",
1334
+ "month",
1335
+ "week",
1336
+ "color"
1337
+ ]);
1191
1338
  function normalizeInputType(value) {
1192
1339
  const type = (value || "text").toLowerCase();
1193
1340
  return INPUT_TYPES.has(type) ? type : "text";
@@ -1207,6 +1354,8 @@ const _FxInput = class _FxInput extends FxFormElement {
1207
1354
  "min",
1208
1355
  "max",
1209
1356
  "step",
1357
+ "clearable",
1358
+ "full",
1210
1359
  "icon",
1211
1360
  "icon-pos",
1212
1361
  "error",
@@ -1234,7 +1383,7 @@ const _FxInput = class _FxInput extends FxFormElement {
1234
1383
  return this.getAttr("value");
1235
1384
  }
1236
1385
  set value(value) {
1237
- this.setAttribute("value", value);
1386
+ this.setAttribute("value", value == null ? "" : String(value));
1238
1387
  this.syncInternalControl();
1239
1388
  this.updateFormValue();
1240
1389
  }
@@ -1244,6 +1393,24 @@ const _FxInput = class _FxInput extends FxFormElement {
1244
1393
  set disabled(value) {
1245
1394
  this.toggleAttr("disabled", Boolean(value));
1246
1395
  }
1396
+ get readonly() {
1397
+ return this.hasAttr("readonly");
1398
+ }
1399
+ set readonly(value) {
1400
+ this.toggleAttr("readonly", Boolean(value));
1401
+ }
1402
+ get clearable() {
1403
+ return this.hasAttr("clearable");
1404
+ }
1405
+ set clearable(value) {
1406
+ this.toggleAttr("clearable", Boolean(value));
1407
+ }
1408
+ get full() {
1409
+ return this.hasAttr("full");
1410
+ }
1411
+ set full(value) {
1412
+ this.toggleAttr("full", Boolean(value));
1413
+ }
1247
1414
  render() {
1248
1415
  const type = this.type;
1249
1416
  const placeholder = this.getAttr("placeholder");
@@ -1274,20 +1441,27 @@ const _FxInput = class _FxInput extends FxFormElement {
1274
1441
  field.value = this.getAttr("value");
1275
1442
  if (disabled) field.setAttribute("disabled", "");
1276
1443
  if (readonly) field.setAttribute("readonly", "");
1277
- const emit = (event) => {
1444
+ const notifyInput = () => {
1278
1445
  this.value = field.value;
1279
1446
  this.emitValueEvents();
1447
+ };
1448
+ const notifyChange = () => {
1449
+ this.value = field.value;
1280
1450
  this.dispatchEvent(
1281
- new CustomEvent(event, { bubbles: true, composed: true, detail: { value: field.value } })
1451
+ new CustomEvent("change", {
1452
+ bubbles: true,
1453
+ composed: true,
1454
+ detail: { value: field.value }
1455
+ })
1282
1456
  );
1283
1457
  };
1284
1458
  field.addEventListener("input", (e) => {
1285
1459
  e.stopPropagation();
1286
- emit("input");
1460
+ notifyInput();
1287
1461
  });
1288
1462
  field.addEventListener("change", (e) => {
1289
1463
  e.stopPropagation();
1290
- emit("change");
1464
+ notifyChange();
1291
1465
  });
1292
1466
  field.addEventListener("keydown", (e) => {
1293
1467
  if (e.key !== "Enter" || e.isComposing) return;
@@ -1303,8 +1477,15 @@ const _FxInput = class _FxInput extends FxFormElement {
1303
1477
  clearBtn?.addEventListener("click", () => {
1304
1478
  field.value = "";
1305
1479
  syncClear();
1306
- emit("input");
1307
- emit("change");
1480
+ this.value = "";
1481
+ this.emitValueEvents();
1482
+ this.dispatchEvent(
1483
+ new CustomEvent("change", {
1484
+ bubbles: true,
1485
+ composed: true,
1486
+ detail: { value: "" }
1487
+ })
1488
+ );
1308
1489
  field.focus();
1309
1490
  });
1310
1491
  field.addEventListener("input", syncClear);
@@ -1494,7 +1675,8 @@ const _FxSwitch = class _FxSwitch extends FxFormElement {
1494
1675
  return this.getAttr("value");
1495
1676
  }
1496
1677
  set value(v) {
1497
- this.setAttribute("value", v);
1678
+ this.setAttribute("value", v == null ? "" : String(v));
1679
+ this.updateFormValue();
1498
1680
  }
1499
1681
  /** No FormData: `value` (ou 'on') quando ligado; null (não envia) quando não. */
1500
1682
  getFormValue() {
@@ -1612,7 +1794,23 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1612
1794
  this.selected = /* @__PURE__ */ new Set();
1613
1795
  }
1614
1796
  static get observedAttributes() {
1615
- return ["disabled", "placeholder", "error", "invalid", "success", "valid"];
1797
+ return [
1798
+ "values",
1799
+ "size",
1800
+ "disabled",
1801
+ "placeholder",
1802
+ "searchable",
1803
+ "clearable",
1804
+ "full",
1805
+ "error",
1806
+ "invalid",
1807
+ "success",
1808
+ "valid"
1809
+ ];
1810
+ }
1811
+ attributeChangedCallback(name, oldValue, newValue) {
1812
+ if (name === "values") this.selected = new Set(this.values);
1813
+ super.attributeChangedCallback(name, oldValue, newValue);
1616
1814
  }
1617
1815
  /** Valores selecionados (CSV no atributo / array na propriedade). */
1618
1816
  get values() {
@@ -1620,7 +1818,15 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1620
1818
  return csv ? csv.split(",").map((v) => v.trim()).filter(Boolean) : [];
1621
1819
  }
1622
1820
  set values(list) {
1623
- this.setAttribute("values", list.join(","));
1821
+ const values = Array.isArray(list) ? list.map(String) : [];
1822
+ this.selected = new Set(values);
1823
+ const csv = values.join(",");
1824
+ if (this.getAttr("values") !== csv) {
1825
+ this.setAttribute("values", csv);
1826
+ } else if (this.isConnected) {
1827
+ this.render();
1828
+ }
1829
+ this.updateFormValue();
1624
1830
  }
1625
1831
  /** No FormData do form: uma entrada `name` por valor selecionado. */
1626
1832
  getFormValue() {
@@ -1636,6 +1842,24 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1636
1842
  set disabled(value) {
1637
1843
  this.toggleAttr("disabled", Boolean(value));
1638
1844
  }
1845
+ get searchable() {
1846
+ return this.hasAttr("searchable");
1847
+ }
1848
+ set searchable(value) {
1849
+ this.toggleAttr("searchable", Boolean(value));
1850
+ }
1851
+ get clearable() {
1852
+ return this.hasAttr("clearable");
1853
+ }
1854
+ set clearable(value) {
1855
+ this.toggleAttr("clearable", Boolean(value));
1856
+ }
1857
+ get full() {
1858
+ return this.hasAttr("full");
1859
+ }
1860
+ set full(value) {
1861
+ this.toggleAttr("full", Boolean(value));
1862
+ }
1639
1863
  get options() {
1640
1864
  return [...this.querySelectorAll("option")].map((o) => ({
1641
1865
  value: o.getAttribute("value") ?? o.textContent?.trim() ?? "",
@@ -1647,7 +1871,15 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1647
1871
  if (!this._initialValues) this._initialValues = [...this.values];
1648
1872
  super.connectedCallback();
1649
1873
  this.observer = new MutationObserver(() => this.render());
1650
- this.observer.observe(this, { childList: true, subtree: true, characterData: true });
1874
+ this.observer.observe(this, {
1875
+ childList: true,
1876
+ subtree: true,
1877
+ characterData: true,
1878
+ // Vue atualiza `:value`/`:selected` das <option> no light DOM; sem
1879
+ // observar atributos o label/valor antigo permanecia em tela.
1880
+ attributes: true,
1881
+ attributeFilter: ["value", "selected"]
1882
+ });
1651
1883
  this.docListener = (e) => {
1652
1884
  if (!this.hasAttr("open")) return;
1653
1885
  if (e.composedPath().includes(this)) return;
@@ -1663,8 +1895,10 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1663
1895
  }
1664
1896
  /** Aplica a seleção, reflete e emite change. */
1665
1897
  commit() {
1666
- this.values = [...this.selected];
1667
- this.render();
1898
+ const next = [...this.selected];
1899
+ const same = next.join(",") === this.values.join(",");
1900
+ this.values = next;
1901
+ if (same && this.isConnected) this.render();
1668
1902
  this.updateFormValue();
1669
1903
  this.dispatchEvent(
1670
1904
  new CustomEvent("change", {
@@ -1685,8 +1919,6 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1685
1919
  formResetCallback() {
1686
1920
  const initial = this._initialValues ?? [];
1687
1921
  this.values = initial;
1688
- this.selected = new Set(initial);
1689
- this.render();
1690
1922
  this.updateFormValue();
1691
1923
  }
1692
1924
  toggleValue(value) {
@@ -1715,8 +1947,10 @@ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1715
1947
  const wasOpen = this.hasAttr("open");
1716
1948
  const search = this.root.querySelector(".search")?.value ?? "";
1717
1949
  const opts = this.options;
1718
- for (const v of [...this.selected]) {
1719
- if (!opts.some((o) => o.value === v)) this.selected.delete(v);
1950
+ if (opts.length > 0) {
1951
+ for (const v of [...this.selected]) {
1952
+ if (!opts.some((o) => o.value === v)) this.selected.delete(v);
1953
+ }
1720
1954
  }
1721
1955
  const chips = opts.filter((o) => this.selected.has(o.value));
1722
1956
  const placeholder = this.getAttr("placeholder", "Selecione…");
@@ -2496,13 +2730,16 @@ defineFxCalendar();
2496
2730
  const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2497
2731
  constructor() {
2498
2732
  super(...arguments);
2733
+ this._suppressRender = false;
2499
2734
  this.time = { h: 0, m: 0, s: 0 };
2500
2735
  }
2501
2736
  static get observedAttributes() {
2502
- return ["value", "start", "end", "values", "mode", "min", "max", "placeholder", "disabled", "format", "show-time", "free-text", "error", "invalid", "success", "valid"];
2737
+ return ["value", "start", "end", "values", "mode", "min", "max", "placeholder", "disabled", "format", "show-time", "free-text", "clearable", "full", "error", "invalid", "success", "valid"];
2503
2738
  }
2504
2739
  attributeChangedCallback(name, oldValue, newValue) {
2505
- super.attributeChangedCallback(name, oldValue, newValue);
2740
+ if (!this._suppressRender) {
2741
+ super.attributeChangedCallback(name, oldValue, newValue);
2742
+ }
2506
2743
  if (name === "value" || name === "start" || name === "end" || name === "values") {
2507
2744
  this.updateFormValue();
2508
2745
  }
@@ -2538,7 +2775,19 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2538
2775
  return this.getAttr("value");
2539
2776
  }
2540
2777
  set value(v) {
2541
- this.setAttribute("value", v);
2778
+ this.setAttribute("value", v == null ? "" : String(v));
2779
+ }
2780
+ get start() {
2781
+ return this.getAttr("start");
2782
+ }
2783
+ set start(v) {
2784
+ this.setAttribute("start", v == null ? "" : String(v));
2785
+ }
2786
+ get end() {
2787
+ return this.getAttr("end");
2788
+ }
2789
+ set end(v) {
2790
+ this.setAttribute("end", v == null ? "" : String(v));
2542
2791
  }
2543
2792
  /** Datas do modo múltiplo (CSV no atributo). */
2544
2793
  get values() {
@@ -2546,7 +2795,8 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2546
2795
  return csv ? csv.split(",").map((v) => v.trim()).filter(Boolean) : [];
2547
2796
  }
2548
2797
  set values(list) {
2549
- this.setAttribute("values", list.join(","));
2798
+ const values = Array.isArray(list) ? list.map(String) : [];
2799
+ this.setAttribute("values", values.join(","));
2550
2800
  }
2551
2801
  get disabled() {
2552
2802
  return this.hasAttr("disabled");
@@ -2554,6 +2804,31 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2554
2804
  set disabled(value) {
2555
2805
  this.toggleAttr("disabled", Boolean(value));
2556
2806
  }
2807
+ /** `:show-time` procura a propriedade LITERAL no Vue (sem camelização). */
2808
+ get "show-time"() {
2809
+ return this.hasAttr("show-time");
2810
+ }
2811
+ set "show-time"(value) {
2812
+ this.toggleAttr("show-time", Boolean(value));
2813
+ }
2814
+ get "free-text"() {
2815
+ return this.hasAttr("free-text");
2816
+ }
2817
+ set "free-text"(value) {
2818
+ this.toggleAttr("free-text", Boolean(value));
2819
+ }
2820
+ get clearable() {
2821
+ return this.hasAttr("clearable");
2822
+ }
2823
+ set clearable(value) {
2824
+ this.toggleAttr("clearable", Boolean(value));
2825
+ }
2826
+ get full() {
2827
+ return this.hasAttr("full");
2828
+ }
2829
+ set full(value) {
2830
+ this.toggleAttr("full", Boolean(value));
2831
+ }
2557
2832
  connectedCallback() {
2558
2833
  if (!this._initialState) {
2559
2834
  this._initialState = {
@@ -2578,11 +2853,17 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2578
2853
  /** form.reset(): restaura value/start/end/values iniciais (re-render via atributos observados). */
2579
2854
  formResetCallback() {
2580
2855
  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);
2856
+ this._suppressRender = true;
2857
+ try {
2858
+ this.setAttribute("value", s.value);
2859
+ this.setAttribute("start", s.start);
2860
+ this.setAttribute("end", s.end);
2861
+ this.setAttribute("values", s.values);
2862
+ } finally {
2863
+ this._suppressRender = false;
2864
+ }
2585
2865
  this.updateFormValue();
2866
+ if (this.isConnected) this.render();
2586
2867
  }
2587
2868
  /** Formata uma data ISO ("YYYY-MM-DD" ou com "THH:mm:ss") segundo o atributo format. */
2588
2869
  formatDate(iso) {
@@ -2690,10 +2971,15 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2690
2971
  });
2691
2972
  this.root.querySelector(".clear")?.addEventListener("click", (e) => {
2692
2973
  e.stopPropagation();
2693
- this.removeAttribute("value");
2694
- this.removeAttribute("start");
2695
- this.removeAttribute("end");
2696
- this.removeAttribute("values");
2974
+ this._suppressRender = true;
2975
+ try {
2976
+ this.removeAttribute("value");
2977
+ this.removeAttribute("start");
2978
+ this.removeAttribute("end");
2979
+ this.removeAttribute("values");
2980
+ } finally {
2981
+ this._suppressRender = false;
2982
+ }
2697
2983
  this.removeAttribute("open");
2698
2984
  this.render();
2699
2985
  this.emitChange({});
@@ -2701,19 +2987,24 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2701
2987
  const cal = this.root.querySelector(".pop fx-calendar");
2702
2988
  cal?.addEventListener("change", (e) => {
2703
2989
  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");
2990
+ this._suppressRender = true;
2991
+ try {
2992
+ if (d.start !== void 0 || d.end !== void 0) {
2993
+ if (d.start) this.setAttribute("start", this.withTime(d.start));
2994
+ else this.removeAttribute("start");
2995
+ if (d.end) this.setAttribute("end", this.withTime(d.end));
2996
+ else this.removeAttribute("end");
2997
+ }
2998
+ if (d.values !== void 0) {
2999
+ this.values = d.values.map((v) => this.withTime(v));
3000
+ }
3001
+ if (d.value !== void 0) {
3002
+ if (d.value) this.setAttribute("value", this.withTime(d.value));
3003
+ else this.removeAttribute("value");
3004
+ if (this.mode === "single") this.removeAttribute("open");
3005
+ }
3006
+ } finally {
3007
+ this._suppressRender = false;
2717
3008
  }
2718
3009
  this.render();
2719
3010
  this.emitChange(d);
@@ -2740,11 +3031,16 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2740
3031
  s: clamp(s?.value, 59)
2741
3032
  };
2742
3033
  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));
3034
+ this._suppressRender = true;
3035
+ try {
3036
+ if (this.value) this.setAttribute("value", reattach(this.value));
3037
+ if (this.getAttr("start")) this.setAttribute("start", reattach(this.getAttr("start")));
3038
+ if (this.getAttr("end")) this.setAttribute("end", reattach(this.getAttr("end")));
3039
+ if (this.values.length) {
3040
+ this.values = this.values.map((v) => reattach(v));
3041
+ }
3042
+ } finally {
3043
+ this._suppressRender = false;
2748
3044
  }
2749
3045
  const display2 = this.root.querySelector(".display");
2750
3046
  if (display2) display2.value = this.displayText();
@@ -2800,23 +3096,32 @@ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2800
3096
  setTimeout(() => input.classList.remove("invalid"), 1500);
2801
3097
  return;
2802
3098
  }
3099
+ this._suppressRender = true;
3100
+ try {
3101
+ if (this.mode === "single") {
3102
+ this.setAttribute("value", this.withTime(parsed));
3103
+ } else if (this.mode === "range") {
3104
+ if (!this.getAttr("start") || this.getAttr("start") && this.getAttr("end")) {
3105
+ this.setAttribute("start", this.withTime(parsed));
3106
+ this.removeAttribute("end");
3107
+ } else {
3108
+ const [a, b] = [this.getAttr("start").split("T")[0], parsed].sort();
3109
+ this.setAttribute("start", this.withTime(a));
3110
+ this.setAttribute("end", this.withTime(b));
3111
+ }
3112
+ } else {
3113
+ const list = this.values.filter((v) => v.split("T")[0] !== parsed);
3114
+ list.push(this.withTime(parsed));
3115
+ this.values = list;
3116
+ }
3117
+ } finally {
3118
+ this._suppressRender = false;
3119
+ }
2803
3120
  if (this.mode === "single") {
2804
- this.setAttribute("value", this.withTime(parsed));
2805
3121
  this.emitChange({ value: this.value });
2806
3122
  } 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
3123
  this.emitChange({ start: this.getAttr("start"), end: this.getAttr("end") });
2816
3124
  } else {
2817
- const list = this.values.filter((v) => v.split("T")[0] !== parsed);
2818
- list.push(this.withTime(parsed));
2819
- this.values = list;
2820
3125
  this.emitChange({ values: this.values });
2821
3126
  }
2822
3127
  this.render();
@@ -2987,7 +3292,7 @@ const _FxCheckbox = class _FxCheckbox extends FxFormElement {
2987
3292
  this._initialCheckedCaptured = false;
2988
3293
  }
2989
3294
  static get observedAttributes() {
2990
- return ["checked", "disabled", "indeterminate"];
3295
+ return ["checked", "disabled", "indeterminate", "size"];
2991
3296
  }
2992
3297
  get checked() {
2993
3298
  return this.hasAttr("checked");
@@ -3016,7 +3321,8 @@ const _FxCheckbox = class _FxCheckbox extends FxFormElement {
3016
3321
  return this.getAttr("value");
3017
3322
  }
3018
3323
  set value(v) {
3019
- this.setAttribute("value", v);
3324
+ this.setAttribute("value", v == null ? "" : String(v));
3325
+ this.updateFormValue();
3020
3326
  }
3021
3327
  /** Tamanho do controle. Padrão: `'md'`. */
3022
3328
  get size() {
@@ -3028,9 +3334,7 @@ const _FxCheckbox = class _FxCheckbox extends FxFormElement {
3028
3334
  }
3029
3335
  render() {
3030
3336
  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
- }
3337
+ this.style.setProperty("--_fx-size-checkbox", px);
3034
3338
  this.setTemplate(`
3035
3339
  <span class="box" part="box" role="checkbox" tabindex="0"
3036
3340
  aria-checked="${this.indeterminate ? "mixed" : this.checked}"
@@ -3102,8 +3406,8 @@ _FxCheckbox.styles = css`
3102
3406
  -webkit-tap-highlight-color: transparent;
3103
3407
  }
3104
3408
  .control {
3105
- width: var(--fx-size-checkbox, 18px);
3106
- height: var(--fx-size-checkbox, 18px);
3409
+ width: var(--fx-size-checkbox, var(--_fx-size-checkbox, 18px));
3410
+ height: var(--fx-size-checkbox, var(--_fx-size-checkbox, 18px));
3107
3411
  flex-shrink: 0;
3108
3412
  display: inline-flex;
3109
3413
  align-items: center;
@@ -3112,7 +3416,7 @@ _FxCheckbox.styles = css`
3112
3416
  border-radius: calc(var(--fx-radius-sm) + 2px);
3113
3417
  background: var(--fx-surface-background);
3114
3418
  color: #fff;
3115
- font-size: calc(var(--fx-size-checkbox, 18px) - 6px);
3419
+ font-size: calc(var(--fx-size-checkbox, var(--_fx-size-checkbox, 18px)) - 6px);
3116
3420
  line-height: 1;
3117
3421
  transition:
3118
3422
  background-color var(--fx-motion-duration-fast) var(--fx-motion-easing),
@@ -3159,7 +3463,7 @@ const _FxRadio = class _FxRadio extends FxFormElement {
3159
3463
  this._initialCheckedCaptured = false;
3160
3464
  }
3161
3465
  static get observedAttributes() {
3162
- return ["checked", "disabled"];
3466
+ return ["checked", "disabled", "size"];
3163
3467
  }
3164
3468
  get checked() {
3165
3469
  return this.hasAttr("checked");
@@ -3192,6 +3496,14 @@ const _FxRadio = class _FxRadio extends FxFormElement {
3192
3496
  getFormValue() {
3193
3497
  return this.checked ? this.getAttr("value") || "on" : null;
3194
3498
  }
3499
+ /** getter/setter `value` — sem o setter o Vue não consegue escrever a propriedade. */
3500
+ get value() {
3501
+ return this.getAttr("value");
3502
+ }
3503
+ set value(v) {
3504
+ this.setAttribute("value", v == null ? "" : String(v));
3505
+ this.updateFormValue();
3506
+ }
3195
3507
  /** Tamanho do controle. Padrão: `'md'`. */
3196
3508
  get size() {
3197
3509
  const s = this.getAttr("size", "md");
@@ -3202,9 +3514,7 @@ const _FxRadio = class _FxRadio extends FxFormElement {
3202
3514
  }
3203
3515
  render() {
3204
3516
  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
- }
3517
+ this.style.setProperty("--_fx-size-radio", px);
3208
3518
  this.setTemplate(`
3209
3519
  <span class="box" part="box" role="radio" tabindex="0"
3210
3520
  aria-checked="${this.checked}" aria-disabled="${this.disabled}">
@@ -3292,8 +3602,8 @@ _FxRadio.styles = css`
3292
3602
  -webkit-tap-highlight-color: transparent;
3293
3603
  }
3294
3604
  .control {
3295
- width: var(--fx-size-radio, 18px);
3296
- height: var(--fx-size-radio, 18px);
3605
+ width: var(--fx-size-radio, var(--_fx-size-radio, 18px));
3606
+ height: var(--fx-size-radio, var(--_fx-size-radio, 18px));
3297
3607
  flex-shrink: 0;
3298
3608
  display: inline-flex;
3299
3609
  align-items: center;
@@ -3805,6 +4115,7 @@ const _FxTable = class _FxTable extends FxElement {
3805
4115
  this.sortDir = 1;
3806
4116
  this.filters = {};
3807
4117
  this.globalSearch = "";
4118
+ this._boundToolbarInputs = /* @__PURE__ */ new WeakSet();
3808
4119
  this._shellKey = "";
3809
4120
  }
3810
4121
  static get observedAttributes() {
@@ -3852,6 +4163,44 @@ const _FxTable = class _FxTable extends FxElement {
3852
4163
  get lazy() {
3853
4164
  return this.hasAttr("lazy");
3854
4165
  }
4166
+ set lazy(value) {
4167
+ this.toggleAttr("lazy", Boolean(value));
4168
+ }
4169
+ get pagination() {
4170
+ return this.hasAttr("pagination");
4171
+ }
4172
+ set pagination(value) {
4173
+ this.toggleAttr("pagination", Boolean(value));
4174
+ }
4175
+ get striped() {
4176
+ return this.hasAttr("striped");
4177
+ }
4178
+ set striped(value) {
4179
+ this.toggleAttr("striped", Boolean(value));
4180
+ }
4181
+ get hover() {
4182
+ return this.hasAttr("hover");
4183
+ }
4184
+ set hover(value) {
4185
+ this.toggleAttr("hover", Boolean(value));
4186
+ }
4187
+ get loading() {
4188
+ return this.hasAttr("loading");
4189
+ }
4190
+ set loading(value) {
4191
+ this.toggleAttr("loading", Boolean(value));
4192
+ }
4193
+ /**
4194
+ * Vue não cameliza atributos de elementos customizados:
4195
+ * `:no-borders-x="false"` procura a propriedade LITERAL
4196
+ * `'no-borders-x'` — o accessor literal evita o contorno `.attr`.
4197
+ */
4198
+ get "no-borders-x"() {
4199
+ return this.hasAttr("no-borders-x");
4200
+ }
4201
+ set "no-borders-x"(value) {
4202
+ this.toggleAttr("no-borders-x", Boolean(value));
4203
+ }
3855
4204
  get columns() {
3856
4205
  return [...this.querySelectorAll("fx-column")].map((c) => {
3857
4206
  const tpl = this._resolveCellTemplate(c);
@@ -3935,7 +4284,14 @@ const _FxTable = class _FxTable extends FxElement {
3935
4284
  loadFenixIconsFont();
3936
4285
  super.connectedCallback();
3937
4286
  this._columnObserver = new MutationObserver(() => this.render());
3938
- this._columnObserver.observe(this, { childList: true, subtree: true });
4287
+ this._columnObserver.observe(this, {
4288
+ childList: true,
4289
+ subtree: true,
4290
+ // Colunas com header/field/align/sortable/filterable alterados
4291
+ // em runtime também precisam re-renderizar.
4292
+ attributes: true,
4293
+ attributeFilter: ["header", "field", "align", "sortable", "filterable", "template"]
4294
+ });
3939
4295
  }
3940
4296
  disconnectedCallback() {
3941
4297
  this._columnObserver?.disconnect();
@@ -4017,7 +4373,14 @@ const _FxTable = class _FxTable extends FxElement {
4017
4373
  const pages = Math.max(1, Math.ceil(total / this.rowsPerPage));
4018
4374
  if (this.page >= pages) this.page = pages - 1;
4019
4375
  const toolbar = this.toolbarTemplate;
4020
- const toolbarHtml = toolbar ? `<div class="toolbar" part="toolbar">${toolbar.innerHTML}</div>` : "";
4376
+ const toolbarSlotted = toolbar?.localName === "fx-toolbar";
4377
+ const toolbarHasContent = Boolean(
4378
+ toolbar && (toolbarSlotted ? toolbar.children.length > 0 || (toolbar.textContent ?? "").trim() !== "" : toolbar.innerHTML.trim() !== "")
4379
+ );
4380
+ if (toolbarSlotted && toolbarHasContent) {
4381
+ toolbar.setAttribute("slot", "fx-toolbar");
4382
+ }
4383
+ const toolbarHtml = toolbarHasContent ? `<div class="toolbar" part="toolbar">${toolbarSlotted ? '<slot name="fx-toolbar"></slot>' : toolbar.innerHTML}</div>` : "";
4021
4384
  const headHtml = cols.map((c) => {
4022
4385
  const isSorted = this.sortField === c.field;
4023
4386
  const ind = isSorted ? this.sortDir === 1 ? "▲" : "▼" : c.sortable ? "⇅" : "";
@@ -4040,7 +4403,7 @@ const _FxTable = class _FxTable extends FxElement {
4040
4403
  <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>
4041
4404
  <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>
4042
4405
  <label class="info">${esc("Por página:")}
4043
- <fx-select class="rows-sel" size="sm" value="${this.rowsPerPage}" aria-label="Itens por página">${this.getAttr(
4406
+ <fx-select class="rows-sel" size="sm" placement="top" value="${this.rowsPerPage}" aria-label="Itens por página">${this.getAttr(
4044
4407
  "rows-options",
4045
4408
  "5,10,20,50"
4046
4409
  ).split(",").map((n) => n.trim()).map((n) => `<option value="${esc(n)}">${esc(n)}</option>`).join("")}</fx-select>
@@ -4061,13 +4424,15 @@ const _FxTable = class _FxTable extends FxElement {
4061
4424
  if (tbody) {
4062
4425
  tbody.innerHTML = bodyHtml;
4063
4426
  this._bindRowClicks(rows);
4427
+ this._bindSlottedToolbarSearch(toolbar);
4428
+ this._restoreToolbarSearch();
4064
4429
  return;
4065
4430
  }
4066
4431
  }
4067
4432
  this._shellKey = shellKey;
4068
4433
  this.setTemplate(`
4069
4434
  ${toolbarHtml}
4070
- <div class="frame${toolbar ? " has-toolbar" : ""}" part="frame">
4435
+ <div class="frame${toolbarHasContent ? " has-toolbar" : ""}" part="frame">
4071
4436
  <div class="table-wrap">
4072
4437
  <div class="scroll">
4073
4438
  <table part="table">
@@ -4128,21 +4493,10 @@ const _FxTable = class _FxTable extends FxElement {
4128
4493
  });
4129
4494
  inp.addEventListener("click", (e) => e.stopPropagation());
4130
4495
  });
4131
- this.root.querySelectorAll("[data-search-fields]").forEach((inp) => {
4132
- inp.addEventListener("input", () => {
4133
- this.globalSearch = inp.value;
4134
- this.page = 0;
4135
- const pos = inp.selectionStart;
4136
- this.render();
4137
- const again = this.root.querySelector(
4138
- "[data-search-fields]"
4139
- );
4140
- if (again) {
4141
- again.focus();
4142
- again.setSelectionRange(pos, pos);
4143
- }
4144
- });
4145
- });
4496
+ this.root.querySelectorAll("[data-search-fields]").forEach((inp) => this._bindSearchInput(inp, false));
4497
+ if (toolbarSlotted && toolbar) {
4498
+ this._bindSlottedToolbarSearch(toolbar);
4499
+ }
4146
4500
  this.root.querySelectorAll(".pg-btn").forEach((btn) => {
4147
4501
  btn.addEventListener("click", () => {
4148
4502
  const pg = btn.dataset.pg;
@@ -4195,11 +4549,47 @@ const _FxTable = class _FxTable extends FxElement {
4195
4549
  });
4196
4550
  });
4197
4551
  }
4552
+ /** Liga (uma vez) os inputs de busca de um <fx-toolbar> slotted. */
4553
+ _bindSlottedToolbarSearch(toolbar) {
4554
+ if (!toolbar || toolbar.localName !== "fx-toolbar") return;
4555
+ toolbar.querySelectorAll("[data-search-fields]").forEach((inp) => {
4556
+ if (this._boundToolbarInputs.has(inp)) return;
4557
+ this._boundToolbarInputs.add(inp);
4558
+ this._bindSearchInput(inp, true);
4559
+ });
4560
+ }
4561
+ /**
4562
+ * Liga um input da toolbar à busca global.
4563
+ * `persistent` = input slotted (não é recriado pelo render): o foco volta
4564
+ * no próprio nó. Inputs clonados são substituídos, então o foco é
4565
+ * restaurado no clone novo do shadow.
4566
+ */
4567
+ _bindSearchInput(inp, persistent) {
4568
+ inp.addEventListener("input", () => {
4569
+ this.globalSearch = inp.value;
4570
+ this.page = 0;
4571
+ const pos = inp.selectionStart;
4572
+ this.render();
4573
+ const again = persistent && inp.isConnected ? inp : this.root.querySelector("[data-search-fields]");
4574
+ if (again) {
4575
+ again.focus();
4576
+ again.setSelectionRange(pos, pos);
4577
+ }
4578
+ });
4579
+ }
4198
4580
  _restoreToolbarSearch() {
4199
- const input = this.root.querySelector(
4581
+ const shadow = this.root.querySelector(
4200
4582
  "[data-search-fields]"
4201
4583
  );
4202
- if (input && this.globalSearch) input.value = this.globalSearch;
4584
+ if (shadow && this.globalSearch) shadow.value = this.globalSearch;
4585
+ const toolbar = this.toolbarTemplate;
4586
+ if (toolbar?.localName === "fx-toolbar") {
4587
+ toolbar.querySelectorAll("[data-search-fields]").forEach((input) => {
4588
+ if (this.globalSearch && input.value !== this.globalSearch) {
4589
+ input.value = this.globalSearch;
4590
+ }
4591
+ });
4592
+ }
4203
4593
  }
4204
4594
  };
4205
4595
  _FxTable.styles = css`
@@ -4277,6 +4667,13 @@ _FxTable.styles = css`
4277
4667
  border-bottom: 2px solid var(--fx-border-default);
4278
4668
  white-space: nowrap;
4279
4669
  user-select: none;
4670
+ /* Cor/peso do cabeçalho por token de TABELA. Sem isso o th herda
4671
+ --fx-text-default do host e o peso vem do UA (bold). */
4672
+ color: var(
4673
+ --fx-table-header-text,
4674
+ var(--fx-table-text, var(--fx-text-default))
4675
+ );
4676
+ font-weight: var(--fx-table-header-weight, bold);
4280
4677
  }
4281
4678
  th.sortable {
4282
4679
  cursor: pointer;
@@ -4321,6 +4718,12 @@ _FxTable.styles = css`
4321
4718
  td {
4322
4719
  padding: var(--fx-space-sm) var(--fx-space-md);
4323
4720
  border-bottom: 1px solid var(--fx-border-default);
4721
+ /* Texto do corpo: token próprio para não depender de
4722
+ --fx-text-default (que afeta TODOS os textos do app) e para
4723
+ não herdar o font-weight do body da página — era isso que
4724
+ fazia a célula parecer mais clara que o cabeçalho. */
4725
+ color: var(--fx-table-text, var(--fx-text-default));
4726
+ font-weight: var(--fx-table-text-weight, inherit);
4324
4727
  }
4325
4728
  tbody tr {
4326
4729
  cursor: pointer;
@@ -4455,6 +4858,11 @@ _FxTable.styles = css`
4455
4858
  .toolbar:empty {
4456
4859
  display: none;
4457
4860
  }
4861
+ /* <fx-toolbar> é apenas um invólucro no light DOM: o slot projeta
4862
+ seus filhos (com listeners do Vue preservados) direto no flex. */
4863
+ ::slotted(fx-toolbar) {
4864
+ display: contents;
4865
+ }
4458
4866
  .loading-overlay {
4459
4867
  position: absolute;
4460
4868
  inset: 0;
@@ -4505,7 +4913,51 @@ const _FxFloatlabel = class _FxFloatlabel extends FxElement {
4505
4913
  this._labelClickBound = false;
4506
4914
  }
4507
4915
  static get observedAttributes() {
4508
- return ["variant", "error", "invalid", "success", "valid", "full", "icon", "icon-pos"];
4916
+ return ["variant", "error", "invalid", "success", "valid", "full", "icon", "icon-pos", "error-text"];
4917
+ }
4918
+ get error() {
4919
+ return this.hasAttr("error");
4920
+ }
4921
+ set error(value) {
4922
+ this.toggleAttr("error", Boolean(value));
4923
+ }
4924
+ get invalid() {
4925
+ return this.hasAttr("invalid");
4926
+ }
4927
+ set invalid(value) {
4928
+ this.toggleAttr("invalid", Boolean(value));
4929
+ }
4930
+ get success() {
4931
+ return this.hasAttr("success");
4932
+ }
4933
+ set success(value) {
4934
+ this.toggleAttr("success", Boolean(value));
4935
+ }
4936
+ get valid() {
4937
+ return this.hasAttr("valid");
4938
+ }
4939
+ set valid(value) {
4940
+ this.toggleAttr("valid", Boolean(value));
4941
+ }
4942
+ get full() {
4943
+ return this.hasAttr("full");
4944
+ }
4945
+ set full(value) {
4946
+ this.toggleAttr("full", Boolean(value));
4947
+ }
4948
+ get errorText() {
4949
+ return this.getAttr("error-text");
4950
+ }
4951
+ set errorText(value) {
4952
+ this.setAttribute("error-text", value == null ? "" : String(value));
4953
+ }
4954
+ connectedCallback() {
4955
+ super.connectedCallback();
4956
+ this._labelObserver = new MutationObserver(() => {
4957
+ const label = this.findLabel();
4958
+ if (label && label !== this._label) this.render();
4959
+ });
4960
+ this._labelObserver.observe(this, { childList: true });
4509
4961
  }
4510
4962
  render() {
4511
4963
  const control = this.findControl();
@@ -4548,7 +5000,10 @@ const _FxFloatlabel = class _FxFloatlabel extends FxElement {
4548
5000
  }
4549
5001
  disconnectedCallback() {
4550
5002
  super.disconnectedCallback();
5003
+ this.cancelScheduledSync();
4551
5004
  this.observer?.disconnect();
5005
+ this._labelObserver?.disconnect();
5006
+ this._labelObserver = void 0;
4552
5007
  }
4553
5008
  /** Campo de controle = qualquer filho direto que não seja label. */
4554
5009
  findControl() {
@@ -4575,14 +5030,28 @@ const _FxFloatlabel = class _FxFloatlabel extends FxElement {
4575
5030
  }
4576
5031
  if (this.listenersReady) return;
4577
5032
  this.listenersReady = true;
4578
- this.addEventListener("focusin", () => this.syncState());
4579
- this.addEventListener(
4580
- "focusout",
4581
- () => setTimeout(() => this.syncState(), 60)
4582
- );
5033
+ this.addEventListener("focusin", () => {
5034
+ this.cancelScheduledSync();
5035
+ this.syncState();
5036
+ });
5037
+ this.addEventListener("focusout", () => this.scheduleSync());
4583
5038
  this.addEventListener("input", () => this.syncState());
4584
5039
  this.addEventListener("change", () => this.syncState());
4585
5040
  }
5041
+ /** Reavalia o estado no próximo tick (o foco já saiu do campo). */
5042
+ scheduleSync() {
5043
+ this.cancelScheduledSync();
5044
+ this._syncTimer = setTimeout(() => {
5045
+ this._syncTimer = void 0;
5046
+ this.syncState();
5047
+ }, 0);
5048
+ }
5049
+ cancelScheduledSync() {
5050
+ if (this._syncTimer !== void 0) {
5051
+ clearTimeout(this._syncTimer);
5052
+ this._syncTimer = void 0;
5053
+ }
5054
+ }
4586
5055
  /** Observa mudanças de atributos do controle (value, open, values...). */
4587
5056
  syncControlReference(control = this.targetControl) {
4588
5057
  if (control !== this.targetControl) {
@@ -4615,18 +5084,16 @@ const _FxFloatlabel = class _FxFloatlabel extends FxElement {
4615
5084
  const c = this.targetControl;
4616
5085
  const attrValue = c.getAttribute ? (c.getAttribute("value") ?? "").trim() : "";
4617
5086
  const attrValues = c.getAttribute ? (c.getAttribute("values") ?? "").trim() : "";
5087
+ const isOpen = Boolean(c.hasAttribute?.("open"));
4618
5088
  const hasValue = Boolean(
4619
- typeof c.value === "string" && c.value.trim() !== "" || Array.isArray(c.values) && c.values.length > 0 || attrValue !== "" || attrValues !== "" || c.hasAttribute && (c.hasAttribute("open") || c.hasAttribute("start"))
5089
+ typeof c.value === "string" && c.value.trim() !== "" || Array.isArray(c.values) && c.values.length > 0 || attrValue !== "" || attrValues !== "" || c.hasAttribute && c.hasAttribute("start")
4620
5090
  );
4621
5091
  const focused = this.isFocusedInside();
4622
- if (hasValue || focused) {
4623
- this.setAttribute("active", "");
4624
- if (focused) this.setAttribute("focus", "");
4625
- else this.removeAttribute("focus");
4626
- } else {
4627
- this.removeAttribute("active");
4628
- this.removeAttribute("focus");
4629
- }
5092
+ const highlighted = focused || isOpen;
5093
+ if (hasValue || highlighted) this.setAttribute("active", "");
5094
+ else this.removeAttribute("active");
5095
+ if (highlighted) this.setAttribute("focus", "");
5096
+ else this.removeAttribute("focus");
4630
5097
  }
4631
5098
  /**
4632
5099
  * variant="in" + ícone à esquerda dentro do campo (fx-input icon):
@@ -4704,6 +5171,10 @@ _FxFloatlabel.styles = css`
4704
5171
  font-family: var(--fx-font-family);
4705
5172
  font-size: var(--fx-font-size);
4706
5173
  vertical-align: middle;
5174
+ /* Cor da label em UM único lugar. active (valor OU foco OU
5175
+ dropdown) só posiciona; focus (foco OU dropdown) só destaca —
5176
+ assim a label acompanha a borda, que só fica primária em foco. */
5177
+ --_flabel-color: var(--fx-text-muted, #94a3b8);
4707
5178
  }
4708
5179
  /* Full width: o host estica até o pai; o controle interno com
4709
5180
  atributo full (ou largura herdada) acompanha. */
@@ -4735,7 +5206,7 @@ _FxFloatlabel.styles = css`
4735
5206
  transform: translateY(-50%);
4736
5207
  font-size: calc(var(--fx-font-size, 14px) - 3px);
4737
5208
  font-weight: 600;
4738
- color: var(--fx-text-muted, #94a3b8);
5209
+ color: var(--_flabel-color);
4739
5210
  background-color: var(--fx-surface-background, #fff);
4740
5211
  padding: 0 4px;
4741
5212
  line-height: 1;
@@ -4753,8 +5224,11 @@ _FxFloatlabel.styles = css`
4753
5224
  color var(--fx-motion-duration-normal, 180ms)
4754
5225
  var(--fx-motion-easing, ease-in-out);
4755
5226
  }
4756
- :host([active]) .flabel {
4757
- color: var(--fx-color-primary, #4f46e5);
5227
+ /* Destacada = foco (ou dropdown aberto). NÃO usar [active]: ele
5228
+ também é verdadeiro com valor e sem foco — era o que deixava a
5229
+ label primária com a borda neutra. */
5230
+ :host([focus]) {
5231
+ --_flabel-color: var(--fx-color-primary, #4f46e5);
4758
5232
  }
4759
5233
 
4760
5234
  /* ---- variant="in": label como placeholder dentro, sobe ao focar/valor ---- */
@@ -4773,7 +5247,8 @@ _FxFloatlabel.styles = css`
4773
5247
  font-weight: 600;
4774
5248
  background-color: var(--fx-surface-background, #fff);
4775
5249
  padding: 0 5px;
4776
- color: var(--fx-color-primary, #4f46e5);
5250
+ /* Sem color aqui: a especificidade desta regra
5251
+ (variant + active) venceria :host([error])/:host([focus]). */
4777
5252
  }
4778
5253
 
4779
5254
  /* ---- variant="in" com ícone dentro do campo (fx-input icon):
@@ -4804,16 +5279,22 @@ _FxFloatlabel.styles = css`
4804
5279
  font-size: calc(var(--fx-font-size, 14px) - 3px);
4805
5280
  font-weight: 600;
4806
5281
  white-space: normal;
5282
+ /* Fora do campo, a label pode receber clique: o listener do
5283
+ render() foca o controle (nas variantes on/in o
5284
+ pointer-events: none é intencional, para não bloquear a borda). */
5285
+ pointer-events: auto;
4807
5286
  }
4808
5287
 
4809
- /* ---- estados de erro / sucesso ---- */
4810
- :host([error]) .flabel,
4811
- :host([invalid]) .flabel {
4812
- color: var(--fx-color-danger, #dc2626);
5288
+ /* ---- estados de erro / sucesso ----
5289
+ Declarados DEPOIS de :host([focus]) e com a mesma especificidade:
5290
+ a validação vence o foco pela ordem, sem depender de acidente. */
5291
+ :host([error]),
5292
+ :host([invalid]) {
5293
+ --_flabel-color: var(--fx-color-danger, #dc2626);
4813
5294
  }
4814
- :host([success]) .flabel,
4815
- :host([valid]) .flabel {
4816
- color: var(--fx-color-success, #10b981);
5295
+ :host([success]),
5296
+ :host([valid]) {
5297
+ --_flabel-color: var(--fx-color-success, #10b981);
4817
5298
  }
4818
5299
 
4819
5300
  /* ---- mensagem de erro sob o campo (fonte menor) ---- */
@@ -4838,7 +5319,7 @@ function defineFxFloatlabel() {
4838
5319
  defineFxFloatlabel();
4839
5320
  const _FxTextarea = class _FxTextarea extends FxFormElement {
4840
5321
  static get observedAttributes() {
4841
- return ["size", "placeholder", "disabled", "readonly", "rows", "maxlength", "error", "invalid", "success", "valid"];
5322
+ return ["size", "placeholder", "disabled", "readonly", "full", "rows", "maxlength", "error", "invalid", "success", "valid"];
4842
5323
  }
4843
5324
  get size() {
4844
5325
  const s = this.getAttr("size", "md");
@@ -4851,10 +5332,22 @@ const _FxTextarea = class _FxTextarea extends FxFormElement {
4851
5332
  return this.getAttr("value");
4852
5333
  }
4853
5334
  set value(value) {
4854
- this.setAttribute("value", value);
5335
+ this.setAttribute("value", value == null ? "" : String(value));
4855
5336
  this.syncInternalControl();
4856
5337
  this.updateFormValue();
4857
5338
  }
5339
+ get readonly() {
5340
+ return this.hasAttr("readonly");
5341
+ }
5342
+ set readonly(value) {
5343
+ this.toggleAttr("readonly", Boolean(value));
5344
+ }
5345
+ get full() {
5346
+ return this.hasAttr("full");
5347
+ }
5348
+ set full(value) {
5349
+ this.toggleAttr("full", Boolean(value));
5350
+ }
4858
5351
  render() {
4859
5352
  const placeholder = this.getAttr("placeholder");
4860
5353
  const readonly = this.hasAttr("readonly");
@@ -4871,20 +5364,27 @@ const _FxTextarea = class _FxTextarea extends FxFormElement {
4871
5364
  field.value = this.getAttr("value");
4872
5365
  if (disabled) field.setAttribute("disabled", "");
4873
5366
  if (readonly) field.setAttribute("readonly", "");
4874
- const emit = (event) => {
5367
+ const notifyInput = () => {
4875
5368
  this.value = field.value;
4876
5369
  this.emitValueEvents();
5370
+ };
5371
+ const notifyChange = () => {
5372
+ this.value = field.value;
4877
5373
  this.dispatchEvent(
4878
- new CustomEvent(event, { bubbles: true, composed: true, detail: { value: field.value } })
5374
+ new CustomEvent("change", {
5375
+ bubbles: true,
5376
+ composed: true,
5377
+ detail: { value: field.value }
5378
+ })
4879
5379
  );
4880
5380
  };
4881
5381
  field.addEventListener("input", (e) => {
4882
5382
  e.stopPropagation();
4883
- emit("input");
5383
+ notifyInput();
4884
5384
  });
4885
5385
  field.addEventListener("change", (e) => {
4886
5386
  e.stopPropagation();
4887
- emit("change");
5387
+ notifyChange();
4888
5388
  });
4889
5389
  this.updateFormValue();
4890
5390
  }
@@ -8530,10 +9030,61 @@ function defineFxEditor() {
8530
9030
  return defineElement("fx-editor", FxEditor);
8531
9031
  }
8532
9032
  defineFxEditor();
9033
+ const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
9034
+ function usable(nodes) {
9035
+ return [...nodes].filter(
9036
+ (el) => !el.hasAttribute("disabled") && el.getAttribute("aria-hidden") !== "true" && el.getAttribute("tabindex") !== "-1"
9037
+ );
9038
+ }
9039
+ function modalFocusables(panel, host) {
9040
+ const shadow = panel.querySelectorAll(FOCUSABLE_SELECTOR);
9041
+ const light = host.querySelectorAll(FOCUSABLE_SELECTOR);
9042
+ return usable([...shadow, ...light]);
9043
+ }
9044
+ function firstLightFocusable(host) {
9045
+ return usable(host.querySelectorAll(FOCUSABLE_SELECTOR))[0] ?? null;
9046
+ }
9047
+ const modalStack = [];
9048
+ function registerModalOpen(host) {
9049
+ if (!modalStack.includes(host)) modalStack.push(host);
9050
+ }
9051
+ function registerModalClose(host) {
9052
+ const index = modalStack.indexOf(host);
9053
+ if (index !== -1) modalStack.splice(index, 1);
9054
+ }
9055
+ function isTopmostModal(host) {
9056
+ if (modalStack.length === 0) return true;
9057
+ return modalStack[modalStack.length - 1] === host;
9058
+ }
9059
+ let scrollLocks = 0;
9060
+ let previousOverflow = "";
9061
+ let previousPaddingRight = "";
9062
+ function lockBodyScroll() {
9063
+ if (typeof document === "undefined" || !document.body) return;
9064
+ if (scrollLocks === 0) {
9065
+ const body = document.body;
9066
+ previousOverflow = body.style.overflow;
9067
+ previousPaddingRight = body.style.paddingRight;
9068
+ const scrollbar = window.innerWidth - document.documentElement.clientWidth;
9069
+ body.style.overflow = "hidden";
9070
+ if (scrollbar > 0) body.style.paddingRight = `${scrollbar}px`;
9071
+ }
9072
+ scrollLocks += 1;
9073
+ }
9074
+ function unlockBodyScroll() {
9075
+ if (typeof document === "undefined" || !document.body) return;
9076
+ if (scrollLocks === 0) return;
9077
+ scrollLocks -= 1;
9078
+ if (scrollLocks === 0) {
9079
+ document.body.style.overflow = previousOverflow;
9080
+ document.body.style.paddingRight = previousPaddingRight;
9081
+ }
9082
+ }
8533
9083
  const _FxDialog = class _FxDialog extends FxElement {
8534
9084
  constructor() {
8535
9085
  super(...arguments);
8536
9086
  this._previouslyFocused = null;
9087
+ this._locked = false;
8537
9088
  }
8538
9089
  static get observedAttributes() {
8539
9090
  return ["open", "size", "heading"];
@@ -8547,6 +9098,11 @@ const _FxDialog = class _FxDialog extends FxElement {
8547
9098
  render() {
8548
9099
  const isOpen = this.open;
8549
9100
  const heading = this.getAttr("heading");
9101
+ if (!isOpen) {
9102
+ this._release();
9103
+ this.setTemplate("");
9104
+ return;
9105
+ }
8550
9106
  this.setTemplate(`
8551
9107
  <div class="overlay" part="overlay" ${isOpen ? "" : "hidden"}>
8552
9108
  <div class="dialog" role="dialog" aria-modal="true" aria-label="${esc(heading)}" tabindex="-1">
@@ -8565,10 +9121,8 @@ const _FxDialog = class _FxDialog extends FxElement {
8565
9121
  const overlay = this.root.querySelector(".overlay");
8566
9122
  const closeBtn = this.root.querySelector(".close");
8567
9123
  const close = () => {
9124
+ if (!this.open) return;
8568
9125
  this.open = false;
8569
- this._cleanup?.();
8570
- this._cleanup = void 0;
8571
- this._restoreFocus();
8572
9126
  this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
8573
9127
  };
8574
9128
  overlay?.addEventListener("click", (e) => {
@@ -8577,6 +9131,7 @@ const _FxDialog = class _FxDialog extends FxElement {
8577
9131
  closeBtn?.addEventListener("click", close);
8578
9132
  const onKey = (e) => {
8579
9133
  if (e.key === "Escape") {
9134
+ if (!isTopmostModal(this)) return;
8580
9135
  e.stopPropagation();
8581
9136
  close();
8582
9137
  return;
@@ -8585,29 +9140,48 @@ const _FxDialog = class _FxDialog extends FxElement {
8585
9140
  };
8586
9141
  document.addEventListener("keydown", onKey);
8587
9142
  this._cleanup = () => document.removeEventListener("keydown", onKey);
8588
- requestAnimationFrame(() => {
8589
- this._previouslyFocused = document.activeElement;
8590
- this.root.querySelector(".dialog")?.focus();
8591
- });
9143
+ const alreadyOpen = this._locked;
9144
+ if (!this._locked) {
9145
+ lockBodyScroll();
9146
+ registerModalOpen(this);
9147
+ this._locked = true;
9148
+ }
9149
+ if (!alreadyOpen) {
9150
+ requestAnimationFrame(() => {
9151
+ if (!this.open) return;
9152
+ this._previouslyFocused = document.activeElement;
9153
+ const first = firstLightFocusable(this) ?? this.root.querySelector(".dialog");
9154
+ first?.focus();
9155
+ });
9156
+ }
8592
9157
  this.dispatchEvent(new CustomEvent("open", { bubbles: true, composed: true }));
8593
9158
  }
8594
- /** Mantém o foco dentro do modal (WCAG 2.4.3 / 2.1.2). */
9159
+ /** Desfaz listeners, foco e scroll lock ao fechar/desconectar. */
9160
+ _release() {
9161
+ this._cleanup?.();
9162
+ this._cleanup = void 0;
9163
+ registerModalClose(this);
9164
+ if (this._locked) {
9165
+ unlockBodyScroll();
9166
+ this._locked = false;
9167
+ }
9168
+ this._restoreFocus();
9169
+ }
9170
+ /** Mantém o foco dentro do modal (WCAG 2.4.3 / 2.1.2), incluindo o slot. */
8595
9171
  _trapFocus(e) {
8596
9172
  const dialog = this.root.querySelector(".dialog");
8597
9173
  if (!dialog) return;
8598
- const focusables = dialog.querySelectorAll(
8599
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
8600
- );
9174
+ const focusables = modalFocusables(dialog, this);
8601
9175
  if (!focusables.length) return;
8602
9176
  const first = focusables[0];
8603
9177
  const last = focusables[focusables.length - 1];
8604
- const active = this.root.activeElement;
8605
- if (e.shiftKey && (active === first || active === dialog)) {
9178
+ const active = this.root.activeElement ?? document.activeElement;
9179
+ if (e.shiftKey && (active === first || active === dialog || active === this)) {
8606
9180
  e.preventDefault();
8607
- last.focus();
9181
+ last?.focus();
8608
9182
  } else if (!e.shiftKey && active === last) {
8609
9183
  e.preventDefault();
8610
- first.focus();
9184
+ first?.focus();
8611
9185
  }
8612
9186
  }
8613
9187
  _restoreFocus() {
@@ -8616,9 +9190,7 @@ const _FxDialog = class _FxDialog extends FxElement {
8616
9190
  }
8617
9191
  disconnectedCallback() {
8618
9192
  super.disconnectedCallback();
8619
- this._cleanup?.();
8620
- this._cleanup = void 0;
8621
- this._restoreFocus();
9193
+ this._release();
8622
9194
  }
8623
9195
  };
8624
9196
  _FxDialog.styles = css`
@@ -9024,7 +9596,35 @@ const _FxFileUpload = class _FxFileUpload extends FxFormElement {
9024
9596
  return this.getAttr("value");
9025
9597
  }
9026
9598
  set value(v) {
9027
- this.setAttribute("value", v);
9599
+ this.setAttribute("value", v == null ? "" : String(v));
9600
+ }
9601
+ get disabled() {
9602
+ return this.hasAttr("disabled");
9603
+ }
9604
+ set disabled(v) {
9605
+ this.toggleAttr("disabled", Boolean(v));
9606
+ }
9607
+ get multiple() {
9608
+ return this.hasAttr("multiple");
9609
+ }
9610
+ set multiple(v) {
9611
+ this.toggleAttr("multiple", Boolean(v));
9612
+ }
9613
+ get full() {
9614
+ return this.hasAttr("full");
9615
+ }
9616
+ set full(v) {
9617
+ this.toggleAttr("full", Boolean(v));
9618
+ }
9619
+ /**
9620
+ * Nome literal: o Vue NÃO cameliza atributos em elementos customizados,
9621
+ * então `:show-progress` procura a propriedade `'show-progress'`.
9622
+ */
9623
+ get "show-progress"() {
9624
+ return this.hasAttr("show-progress");
9625
+ }
9626
+ set "show-progress"(v) {
9627
+ this.toggleAttr("show-progress", Boolean(v));
9028
9628
  }
9029
9629
  get progress() {
9030
9630
  return Math.min(100, Math.max(0, Number(this.getAttr("progress", "0")) || 0));
@@ -9705,6 +10305,9 @@ const _FxAvatar = class _FxAvatar extends FxElement {
9705
10305
  get variant() {
9706
10306
  return this.getAttr("variant", "text");
9707
10307
  }
10308
+ set variant(v) {
10309
+ this.setAttribute("variant", v);
10310
+ }
9708
10311
  get src() {
9709
10312
  return this.getAttr("src");
9710
10313
  }
@@ -9808,6 +10411,18 @@ const _FxIcon = class _FxIcon extends FxElement {
9808
10411
  if (value) this.setAttribute("label", value);
9809
10412
  else this.removeAttribute("label");
9810
10413
  }
10414
+ get fill() {
10415
+ return this.hasAttr("fill");
10416
+ }
10417
+ set fill(value) {
10418
+ this.toggleAttr("fill", Boolean(value));
10419
+ }
10420
+ get bold() {
10421
+ return this.hasAttr("bold");
10422
+ }
10423
+ set bold(value) {
10424
+ this.toggleAttr("bold", Boolean(value));
10425
+ }
9811
10426
  connectedCallback() {
9812
10427
  loadFenixIconsFont();
9813
10428
  super.connectedCallback();
@@ -9854,6 +10469,12 @@ const _FxCard = class _FxCard extends FxElement {
9854
10469
  static get observedAttributes() {
9855
10470
  return ["variant", "radius", "size", "padded", "heading"];
9856
10471
  }
10472
+ get padded() {
10473
+ return this.hasAttr("padded");
10474
+ }
10475
+ set padded(value) {
10476
+ this.toggleAttr("padded", Boolean(value));
10477
+ }
9857
10478
  render() {
9858
10479
  const heading = this.getAttr("heading");
9859
10480
  this.setTemplate(`
@@ -12374,6 +12995,40 @@ const _FxTabs = class _FxTabs extends FxElement {
12374
12995
  set value(value) {
12375
12996
  this.setAttribute("value", value);
12376
12997
  }
12998
+ connectedCallback() {
12999
+ super.connectedCallback();
13000
+ this._observeLightDom();
13001
+ }
13002
+ disconnectedCallback() {
13003
+ super.disconnectedCallback();
13004
+ this._lightObserver?.disconnect();
13005
+ this._siblingObserver?.disconnect();
13006
+ this._lightObserver = void 0;
13007
+ this._siblingObserver = void 0;
13008
+ }
13009
+ _observeLightDom() {
13010
+ this._lightObserver?.disconnect();
13011
+ this._lightObserver = new MutationObserver(() => this.render());
13012
+ this._lightObserver.observe(this, {
13013
+ childList: true,
13014
+ subtree: true,
13015
+ attributes: true,
13016
+ attributeFilter: ["tab", "disabled"]
13017
+ });
13018
+ this._siblingObserver?.disconnect();
13019
+ const parent = this.parentNode;
13020
+ if (parent) {
13021
+ this._siblingObserver = new MutationObserver((mutations) => {
13022
+ const affectsPanels = mutations.some(
13023
+ (m) => [...m.addedNodes, ...m.removedNodes].some(
13024
+ (node) => node.nodeType === Node.ELEMENT_NODE && (node.matches("fx-tab-panel") || Boolean(node.querySelector("fx-tab-panel")))
13025
+ )
13026
+ );
13027
+ if (affectsPanels) this.render();
13028
+ });
13029
+ this._siblingObserver.observe(parent, { childList: true });
13030
+ }
13031
+ }
12377
13032
  render() {
12378
13033
  const tabs = Array.from(this.querySelectorAll("fx-tab"));
12379
13034
  const active = this.value || tabs[0]?.getAttribute("tab") || "";
@@ -12902,10 +13557,11 @@ const _FxDrawer = class _FxDrawer extends FxElement {
12902
13557
  this._onKeydown = () => {
12903
13558
  };
12904
13559
  this._previouslyFocused = null;
13560
+ this._locked = false;
13561
+ this._wasOpen = false;
12905
13562
  this._close = () => {
12906
13563
  if (!this.open) return;
12907
13564
  this.open = false;
12908
- this._restoreFocus();
12909
13565
  this.dispatchEvent(
12910
13566
  new CustomEvent("close", { bubbles: true, composed: true })
12911
13567
  );
@@ -12935,6 +13591,8 @@ const _FxDrawer = class _FxDrawer extends FxElement {
12935
13591
  }
12936
13592
  render() {
12937
13593
  const title = this.getAttr("title");
13594
+ const wasOpen = this._wasOpen;
13595
+ this._wasOpen = this.open;
12938
13596
  this.setTemplate(`
12939
13597
  <div class="overlay" part="overlay"></div>
12940
13598
  <div class="panel" part="panel" role="dialog" aria-modal="true" aria-label="${esc(title || "Drawer")}" tabindex="-1">
@@ -12945,20 +13603,40 @@ const _FxDrawer = class _FxDrawer extends FxElement {
12945
13603
  this.root.querySelector(".overlay")?.addEventListener("click", this._close);
12946
13604
  this.root.querySelector(".close")?.addEventListener("click", this._close);
12947
13605
  if (this.open) {
12948
- requestAnimationFrame(() => {
12949
- if (this._previouslyFocused === null) {
13606
+ if (!this._locked) {
13607
+ lockBodyScroll();
13608
+ registerModalOpen(this);
13609
+ this._locked = true;
13610
+ }
13611
+ if (!wasOpen) {
13612
+ requestAnimationFrame(() => {
13613
+ if (!this.open) return;
12950
13614
  this._previouslyFocused = document.activeElement;
12951
- }
12952
- this.root.querySelector(".panel")?.focus();
12953
- });
13615
+ const first = firstLightFocusable(this) ?? this.root.querySelector(".panel");
13616
+ first?.focus();
13617
+ });
13618
+ }
13619
+ } else {
13620
+ this._release();
12954
13621
  }
12955
13622
  }
13623
+ /** Desfaz scroll lock e restaura o foco ao fechar/desconectar. */
13624
+ _release() {
13625
+ registerModalClose(this);
13626
+ if (this._locked) {
13627
+ unlockBodyScroll();
13628
+ this._locked = false;
13629
+ }
13630
+ this._restoreFocus();
13631
+ }
12956
13632
  connectedCallback() {
12957
13633
  super.connectedCallback?.();
12958
13634
  this._onKeydown = (e) => {
12959
- if (e.key === "Escape" && this.open) {
13635
+ if (!this.open) return;
13636
+ if (e.key === "Escape") {
13637
+ if (!isTopmostModal(this)) return;
12960
13638
  this._close();
12961
- } else if (e.key === "Tab" && this.open) {
13639
+ } else if (e.key === "Tab") {
12962
13640
  this._trapFocus(e);
12963
13641
  }
12964
13642
  };
@@ -12966,26 +13644,24 @@ const _FxDrawer = class _FxDrawer extends FxElement {
12966
13644
  }
12967
13645
  disconnectedCallback() {
12968
13646
  document.removeEventListener("keydown", this._onKeydown);
12969
- this._restoreFocus();
13647
+ this._release();
12970
13648
  super.disconnectedCallback?.();
12971
13649
  }
12972
- /** Mantém o foco dentro do drawer (WCAG 2.4.3 / 2.1.2). */
13650
+ /** Mantém o foco dentro do drawer (WCAG 2.4.3 / 2.1.2), incluindo o slot. */
12973
13651
  _trapFocus(e) {
12974
13652
  const panel = this.root.querySelector(".panel");
12975
13653
  if (!panel) return;
12976
- const focusables = panel.querySelectorAll(
12977
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
12978
- );
13654
+ const focusables = modalFocusables(panel, this);
12979
13655
  if (!focusables.length) return;
12980
13656
  const first = focusables[0];
12981
13657
  const last = focusables[focusables.length - 1];
12982
- const active = this.root.activeElement;
12983
- if (e.shiftKey && (active === first || active === panel)) {
13658
+ const active = this.root.activeElement ?? document.activeElement;
13659
+ if (e.shiftKey && (active === first || active === panel || active === this)) {
12984
13660
  e.preventDefault();
12985
- last.focus();
13661
+ last?.focus();
12986
13662
  } else if (!e.shiftKey && active === last) {
12987
13663
  e.preventDefault();
12988
- first.focus();
13664
+ first?.focus();
12989
13665
  }
12990
13666
  }
12991
13667
  _restoreFocus() {
@@ -13278,11 +13954,17 @@ const _FxAutocomplete = class _FxAutocomplete extends FxFormElement {
13278
13954
  set size(v) {
13279
13955
  this.setAttribute("size", v);
13280
13956
  }
13957
+ get disabled() {
13958
+ return this.hasAttr("disabled");
13959
+ }
13960
+ set disabled(v) {
13961
+ this.toggleAttr("disabled", Boolean(v));
13962
+ }
13281
13963
  get value() {
13282
13964
  return this.getAttr("value");
13283
13965
  }
13284
13966
  set value(v) {
13285
- this.setAttribute("value", v);
13967
+ this.setAttribute("value", v == null ? "" : String(v));
13286
13968
  this.syncInternalControl();
13287
13969
  this.updateFormValue();
13288
13970
  }
@@ -13816,6 +14498,7 @@ const _FxOrderList = class _FxOrderList extends FxElement {
13816
14498
  static get observedAttributes() {
13817
14499
  return [
13818
14500
  "data",
14501
+ "items",
13819
14502
  "data-key",
13820
14503
  "filter",
13821
14504
  "filter-by",
@@ -13829,13 +14512,35 @@ const _FxOrderList = class _FxOrderList extends FxElement {
13829
14512
  ];
13830
14513
  }
13831
14514
  set data(value) {
13832
- this._data = value;
14515
+ this._data = Array.isArray(value) ? value : [];
13833
14516
  this._filteredData = null;
13834
14517
  this.render();
13835
14518
  }
13836
14519
  get data() {
13837
14520
  return this._data;
13838
14521
  }
14522
+ /**
14523
+ * Alias de `data` para o binding do Vue/React: `data` colide com a
14524
+ * tipagem nativa de atributos HTML (`dataset`/`data-*`) e não reflete em
14525
+ * atributo. `items` funciona como propriedade e como atributo JSON.
14526
+ */
14527
+ get items() {
14528
+ return this._data;
14529
+ }
14530
+ set items(value) {
14531
+ const list = Array.isArray(value) ? value : [];
14532
+ this._data = list;
14533
+ this._filteredData = null;
14534
+ const json = (() => {
14535
+ try {
14536
+ return JSON.stringify(list);
14537
+ } catch {
14538
+ return null;
14539
+ }
14540
+ })();
14541
+ if (json !== null && this.getAttr("items") !== json) this.setAttribute("items", json);
14542
+ else if (this.isConnected) this.render();
14543
+ }
13839
14544
  set selection(value) {
13840
14545
  this._selection = value;
13841
14546
  this.render();
@@ -13915,7 +14620,16 @@ const _FxOrderList = class _FxOrderList extends FxElement {
13915
14620
  super.connectedCallback();
13916
14621
  }
13917
14622
  attributeChangedCallback(name, oldValue, newValue) {
13918
- if (this.getAttr("data", "") !== "") {
14623
+ if (name === "items") {
14624
+ try {
14625
+ const parsed = JSON.parse(newValue ?? "[]");
14626
+ this._data = Array.isArray(parsed) ? parsed : [];
14627
+ this._filteredData = null;
14628
+ } catch {
14629
+ this._data = [];
14630
+ this._filteredData = null;
14631
+ }
14632
+ } else if (this.getAttr("data", "") !== "") {
13919
14633
  this._parseDataAttribute();
13920
14634
  }
13921
14635
  super.attributeChangedCallback(name, oldValue, newValue);