@wrrdev/fenix-ui 1.1.12 → 1.1.14

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 (115) hide show
  1. package/README.md +3 -3
  2. package/dist/components/autocomplete/autocomplete.d.ts +11 -5
  3. package/dist/components/autocomplete/autocomplete.d.ts.map +1 -1
  4. package/dist/components/autocomplete/autocomplete.js +18 -2
  5. package/dist/components/autocomplete/autocomplete.js.map +1 -1
  6. package/dist/components/button/button.d.ts +15 -0
  7. package/dist/components/button/button.d.ts.map +1 -1
  8. package/dist/components/button/button.js +20 -1
  9. package/dist/components/button/button.js.map +1 -1
  10. package/dist/components/card/card.d.ts +10 -0
  11. package/dist/components/card/card.d.ts.map +1 -1
  12. package/dist/components/card/card.js +18 -0
  13. package/dist/components/card/card.js.map +1 -1
  14. package/dist/components/checkbox/checkbox.d.ts +20 -4
  15. package/dist/components/checkbox/checkbox.d.ts.map +1 -1
  16. package/dist/components/checkbox/checkbox.js +39 -2
  17. package/dist/components/checkbox/checkbox.js.map +1 -1
  18. package/dist/components/datepicker/datepicker.d.ts +16 -2
  19. package/dist/components/datepicker/datepicker.d.ts.map +1 -1
  20. package/dist/components/datepicker/datepicker.js +46 -2
  21. package/dist/components/datepicker/datepicker.js.map +1 -1
  22. package/dist/components/fileupload/fileupload.d.ts +10 -2
  23. package/dist/components/fileupload/fileupload.d.ts.map +1 -1
  24. package/dist/components/fileupload/fileupload.js +17 -2
  25. package/dist/components/fileupload/fileupload.js.map +1 -1
  26. package/dist/components/floatlabel/floatlabel.d.ts +9 -3
  27. package/dist/components/floatlabel/floatlabel.d.ts.map +1 -1
  28. package/dist/components/floatlabel/floatlabel.js +22 -7
  29. package/dist/components/floatlabel/floatlabel.js.map +1 -1
  30. package/dist/components/input/input.d.ts +14 -7
  31. package/dist/components/input/input.d.ts.map +1 -1
  32. package/dist/components/input/input.js +18 -2
  33. package/dist/components/input/input.js.map +1 -1
  34. package/dist/components/knob/knob.d.ts +7 -3
  35. package/dist/components/knob/knob.d.ts.map +1 -1
  36. package/dist/components/knob/knob.js +4 -2
  37. package/dist/components/knob/knob.js.map +1 -1
  38. package/dist/components/multiselect/multiselect.d.ts +13 -3
  39. package/dist/components/multiselect/multiselect.d.ts.map +1 -1
  40. package/dist/components/multiselect/multiselect.js +27 -2
  41. package/dist/components/multiselect/multiselect.js.map +1 -1
  42. package/dist/components/radio/radio.d.ts +27 -4
  43. package/dist/components/radio/radio.d.ts.map +1 -1
  44. package/dist/components/radio/radio.js +67 -8
  45. package/dist/components/radio/radio.js.map +1 -1
  46. package/dist/components/rating/rating.d.ts +8 -4
  47. package/dist/components/rating/rating.d.ts.map +1 -1
  48. package/dist/components/rating/rating.js +5 -2
  49. package/dist/components/rating/rating.js.map +1 -1
  50. package/dist/components/select/select.d.ts +38 -4
  51. package/dist/components/select/select.d.ts.map +1 -1
  52. package/dist/components/select/select.js +109 -12
  53. package/dist/components/select/select.js.map +1 -1
  54. package/dist/components/slider/slider.d.ts +12 -6
  55. package/dist/components/slider/slider.d.ts.map +1 -1
  56. package/dist/components/slider/slider.js +10 -2
  57. package/dist/components/slider/slider.js.map +1 -1
  58. package/dist/components/switch/switch.d.ts +20 -4
  59. package/dist/components/switch/switch.d.ts.map +1 -1
  60. package/dist/components/switch/switch.js +40 -2
  61. package/dist/components/switch/switch.js.map +1 -1
  62. package/dist/components/textarea/textarea.d.ts +12 -5
  63. package/dist/components/textarea/textarea.d.ts.map +1 -1
  64. package/dist/components/textarea/textarea.js +12 -2
  65. package/dist/components/textarea/textarea.js.map +1 -1
  66. package/dist/components/toast/toast.d.ts +17 -5
  67. package/dist/components/toast/toast.d.ts.map +1 -1
  68. package/dist/components/toast/toast.js +161 -21
  69. package/dist/components/toast/toast.js.map +1 -1
  70. package/dist/components/toggle-button-group/toggle-button-group.d.ts +13 -5
  71. package/dist/components/toggle-button-group/toggle-button-group.d.ts.map +1 -1
  72. package/dist/components/toggle-button-group/toggle-button-group.js +19 -2
  73. package/dist/components/toggle-button-group/toggle-button-group.js.map +1 -1
  74. package/dist/core/base.d.ts +7 -0
  75. package/dist/core/base.d.ts.map +1 -1
  76. package/dist/core/base.js +4 -1
  77. package/dist/core/base.js.map +1 -1
  78. package/dist/core/form-element.d.ts +81 -0
  79. package/dist/core/form-element.d.ts.map +1 -0
  80. package/dist/core/form-element.js +129 -0
  81. package/dist/core/form-element.js.map +1 -0
  82. package/dist/core/presets.d.ts +8 -3
  83. package/dist/core/presets.d.ts.map +1 -1
  84. package/dist/core/presets.js +6 -17
  85. package/dist/core/presets.js.map +1 -1
  86. package/dist/core/types.d.ts +4 -0
  87. package/dist/core/types.d.ts.map +1 -1
  88. package/dist/docs/app.d.ts.map +1 -1
  89. package/dist/docs/componentes/autocomplete.doc.d.ts.map +1 -1
  90. package/dist/docs/componentes/card.doc.d.ts.map +1 -1
  91. package/dist/docs/componentes/checkbox.doc.d.ts.map +1 -1
  92. package/dist/docs/componentes/datepicker.doc.d.ts.map +1 -1
  93. package/dist/docs/componentes/fileupload.doc.d.ts.map +1 -1
  94. package/dist/docs/componentes/input.doc.d.ts.map +1 -1
  95. package/dist/docs/componentes/knob.doc.d.ts.map +1 -1
  96. package/dist/docs/componentes/multiselect.doc.d.ts.map +1 -1
  97. package/dist/docs/componentes/rating.doc.d.ts.map +1 -1
  98. package/dist/docs/componentes/select.doc.d.ts.map +1 -1
  99. package/dist/docs/componentes/slider.doc.d.ts.map +1 -1
  100. package/dist/docs/componentes/switch.doc.d.ts.map +1 -1
  101. package/dist/docs/componentes/textarea.doc.d.ts.map +1 -1
  102. package/dist/docs/componentes/toast.doc.d.ts.map +1 -1
  103. package/dist/docs/componentes/toggle-button-group.doc.d.ts.map +1 -1
  104. package/dist/docs/frameworks.d.ts +75 -0
  105. package/dist/docs/frameworks.d.ts.map +1 -0
  106. package/dist/docs/ui/code.d.ts +4 -0
  107. package/dist/docs/ui/code.d.ts.map +1 -1
  108. package/dist/fenix-ui.umd.min.js +233 -179
  109. package/dist/fenix-ui4.esm.js +773 -77
  110. package/dist/fenix-ui4.esm.js.map +1 -1
  111. package/dist/index.d.ts +1 -0
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +4 -1
  114. package/dist/index.js.map +1 -1
  115. package/package.json +1 -1
@@ -4,7 +4,7 @@ var __typeError = (msg) => {
4
4
  var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
5
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
6
6
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
7
- var _FxToast_instances, syncProgress_fn, render_fn;
7
+ var _FxToast_instances, wirePause_fn, startTimer_fn, pauseTimer_fn, resumeTimer_fn, syncProgress_fn, render_fn, syncMore_fn;
8
8
  import { a as FENIX_ICON_BASE_CSS, f as fenixIconHtml, g as esc, l as loadFenixIconsFont, i as isFenixIconName } from "./assets/font-DPeWTz-n.js";
9
9
  import { h, j } from "./assets/font-DPeWTz-n.js";
10
10
  const lightTokens = {
@@ -167,15 +167,16 @@ const FenixUI = {
167
167
  tokenCssVars,
168
168
  deepMerge
169
169
  };
170
+ const DEFAULT_PRESET = "ikki";
170
171
  const registry = /* @__PURE__ */ new Map();
171
172
  function register(preset) {
172
173
  registry.set(preset.name, preset);
173
174
  return preset;
174
175
  }
175
176
  const themePresets = {
176
- fenix: register({
177
- name: "fenix",
178
- label: "Fenix (padrão)",
177
+ ikki: register({
178
+ name: "ikki",
179
+ label: "Ikki (padrão)",
179
180
  tokens: { effect: { "focus-ring": "none" } }
180
181
  }),
181
182
  seiya: register({
@@ -237,19 +238,6 @@ const themePresets = {
237
238
  radius: { sm: "10px", md: "14px", lg: "20px" }
238
239
  }
239
240
  }),
240
- ikki: register({
241
- name: "ikki",
242
- label: "Ikki",
243
- tokens: {
244
- color: {
245
- primary: "#ea580c",
246
- secondary: "#fb923c",
247
- danger: "#dc2626",
248
- warning: "#f59e0b"
249
- },
250
- radius: { sm: "2px", md: "4px", lg: "6px" }
251
- }
252
- }),
253
241
  aiolia: register({
254
242
  name: "aiolia",
255
243
  label: "Aiolia",
@@ -274,7 +262,7 @@ function defineCustomPreset(name, label, tokens) {
274
262
  return register({ name, label, tokens });
275
263
  }
276
264
  function applyPreset(presetName, mode = "light") {
277
- const preset = registry.get(presetName) ?? registry.get("fenix");
265
+ const preset = registry.get(presetName) ?? registry.get(DEFAULT_PRESET);
278
266
  const state = configure({ theme: mode, tokens: preset.tokens });
279
267
  return { ...state, preset: preset.name };
280
268
  }
@@ -290,7 +278,9 @@ const _FxElement = class _FxElement extends HTMLElement {
290
278
  }
291
279
  constructor() {
292
280
  super();
293
- this.root = this.attachShadow({ mode: "open" });
281
+ this.root = this.attachShadow(
282
+ this.constructor.shadowOptions
283
+ );
294
284
  }
295
285
  /** Chamado quando um atributo observado muda. */
296
286
  attributeChangedCallback(_name, _oldValue, _newValue) {
@@ -320,7 +310,132 @@ const _FxElement = class _FxElement extends HTMLElement {
320
310
  }
321
311
  };
322
312
  _FxElement.styles = "";
313
+ _FxElement.shadowOptions = { mode: "open" };
323
314
  let FxElement = _FxElement;
315
+ const _FxFormElement = class _FxFormElement extends FxElement {
316
+ constructor() {
317
+ super();
318
+ this._initialValue = "";
319
+ this._initialCaptured = false;
320
+ this._autofocused = false;
321
+ this.internals = typeof this.attachInternals === "function" ? this.attachInternals() : null;
322
+ }
323
+ /** Nome do campo no FormData (atributo `name`, como no nativo). */
324
+ get name() {
325
+ return this.getAttribute("name") ?? "";
326
+ }
327
+ /** Form ao qual o componente está associado (null se fora de um form). */
328
+ get form() {
329
+ return this.internals?.form ?? this.closest("form");
330
+ }
331
+ get type() {
332
+ return this.localName;
333
+ }
334
+ get willValidate() {
335
+ return this.internals?.willValidate ?? false;
336
+ }
337
+ get validity() {
338
+ return this.internals?.validity ?? null;
339
+ }
340
+ get validationMessage() {
341
+ return this.internals?.validationMessage ?? "";
342
+ }
343
+ get labels() {
344
+ return this.internals?.labels ?? this.childNodes;
345
+ }
346
+ checkValidity() {
347
+ return this.internals?.checkValidity?.() ?? true;
348
+ }
349
+ reportValidity() {
350
+ return this.internals?.reportValidity?.() ?? true;
351
+ }
352
+ connectedCallback() {
353
+ super.connectedCallback();
354
+ if (!this._initialCaptured) {
355
+ this._initialCaptured = true;
356
+ this._initialValue = this.getAttr("value");
357
+ }
358
+ this.updateFormValue();
359
+ if (!this._autofocused && this.hasAttr("autofocus")) {
360
+ this._autofocused = true;
361
+ queueMicrotask(() => {
362
+ if (this.isConnected && this.hasAttr("autofocus")) this.focusInnerControl();
363
+ });
364
+ }
365
+ }
366
+ /**
367
+ * Foca o controle interno: primeiro o HOST (com `delegatesFocus` o browser
368
+ * repassa ao controle interno) e, se nada for focado (ex.: ambientes sem
369
+ * delegatesFocus, como jsdom nos testes), o primeiro focável do shadow.
370
+ */
371
+ focusInnerControl() {
372
+ this.focus();
373
+ if (!this.root.activeElement) {
374
+ const first = this.root.querySelector(
375
+ 'input, textarea, button, select, [tabindex]:not([tabindex="-1"])'
376
+ );
377
+ first?.focus();
378
+ }
379
+ }
380
+ /**
381
+ * Valor publicado no FormData. Sobrescreva quando o valor NÃO é o atributo
382
+ * `value` (checkbox/switch enviariam `value || 'on'` quando marcados,
383
+ * seleção múltipla enviaria várias entradas, fileupload enviaria arquivos).
384
+ */
385
+ getFormValue() {
386
+ return this.getAttr("value") || null;
387
+ }
388
+ /**
389
+ * Sincroniza o campo interno, publica name/value no form (ElementInternals)
390
+ * e emite `input` (v-model do Vue) + `valueChange` ([(value)] do Angular).
391
+ *
392
+ * Chame quando o valor mudar — junto do evento semântico (`change`,
393
+ * `select`, ...) que o componente já dispara. `detail` opcional complementa
394
+ * o payload (ex.: { start, end } do fx-datepicker).
395
+ */
396
+ emitValueEvents(detail) {
397
+ const value = this.getAttr("value");
398
+ this.syncInternalControl();
399
+ this.updateFormValue();
400
+ const payload = { value, ...detail };
401
+ for (const type of ["input", "valueChange"]) {
402
+ this.dispatchEvent(
403
+ new CustomEvent(type, { bubbles: true, composed: true, detail: payload })
404
+ );
405
+ }
406
+ }
407
+ /** Sincroniza name/value com o form (ElementInternals). */
408
+ updateFormValue() {
409
+ if (!this.internals || typeof this.internals.setFormValue !== "function") return;
410
+ this.internals.setFormValue(this.getFormValue());
411
+ }
412
+ /**
413
+ * <form disabled>/<fieldset disabled>: espelha no atributo `disabled`
414
+ * observado — o re-render desabilita o controle interno.
415
+ */
416
+ formDisabledCallback(disabled) {
417
+ if (disabled) this.setAttribute("disabled", "");
418
+ else this.removeAttribute("disabled");
419
+ }
420
+ /** form.reset(): restaura o valor inicial e sincroniza o campo interno. */
421
+ formResetCallback() {
422
+ this.setAttribute("value", this._initialValue);
423
+ this.syncInternalControl();
424
+ this.updateFormValue();
425
+ }
426
+ /**
427
+ * Hook: empurra o valor atual do atributo `value` para o controle interno
428
+ * SEM re-renderizar (componentes com campo nativo interno sobrescrevem).
429
+ */
430
+ syncInternalControl() {
431
+ }
432
+ };
433
+ _FxFormElement.formAssociated = true;
434
+ _FxFormElement.shadowOptions = {
435
+ mode: "open",
436
+ delegatesFocus: true
437
+ };
438
+ let FxFormElement = _FxFormElement;
324
439
  function defineElement(tag, ctor) {
325
440
  if (typeof customElements !== "undefined" && !customElements.get(tag)) {
326
441
  customElements.define(tag, ctor);
@@ -330,7 +445,7 @@ function defineElement(tag, ctor) {
330
445
  const FX_JSX_TYPES = true;
331
446
  const _FxButton = class _FxButton extends FxElement {
332
447
  constructor() {
333
- super(...arguments);
448
+ super();
334
449
  this.spawnRipple = (event) => {
335
450
  if (this.hasAttr("disabled") || this.hasAttr("loading")) return;
336
451
  const enabled = getComputedStyle(this).getPropertyValue("--fx-effect-ripple").trim();
@@ -347,10 +462,20 @@ const _FxButton = class _FxButton extends FxElement {
347
462
  btn.appendChild(ripple);
348
463
  ripple.addEventListener("animationend", () => ripple.remove());
349
464
  };
465
+ this.internals = typeof this.attachInternals === "function" ? this.attachInternals() : null;
350
466
  }
351
467
  static get observedAttributes() {
352
468
  return ["variant", "size", "disabled", "loading", "type", "full", "icon", "icon-pos"];
353
469
  }
470
+ /** Form ao qual o botão está associado (null se fora de um form). */
471
+ get form() {
472
+ return this.internals?.form ?? this.closest("form");
473
+ }
474
+ /** <form disabled>/<fieldset disabled>: espelha no atributo observado. */
475
+ formDisabledCallback(disabled) {
476
+ if (disabled) this.setAttribute("disabled", "");
477
+ else this.removeAttribute("disabled");
478
+ }
354
479
  /** Tamanho. Padrão: `'md'`. */
355
480
  get size() {
356
481
  const s = this.getAttr("size", "md");
@@ -406,6 +531,13 @@ const _FxButton = class _FxButton extends FxElement {
406
531
  }
407
532
  if (loading) btn.setAttribute("aria-busy", "true");
408
533
  else btn.removeAttribute("aria-busy");
534
+ if (btnType === "submit" || btnType === "reset") {
535
+ btn.addEventListener("click", () => {
536
+ if (this.hasAttr("disabled") || this.hasAttr("loading")) return;
537
+ if (btnType === "submit" && this.form) this.form.requestSubmit();
538
+ else if (btnType === "reset" && this.form) this.form.reset();
539
+ });
540
+ }
409
541
  }
410
542
  connectedCallback() {
411
543
  super.connectedCallback();
@@ -416,6 +548,8 @@ const _FxButton = class _FxButton extends FxElement {
416
548
  this.removeEventListener("pointerdown", this.spawnRipple);
417
549
  }
418
550
  };
551
+ _FxButton.formAssociated = true;
552
+ _FxButton.shadowOptions = { mode: "open", delegatesFocus: true };
419
553
  _FxButton.styles = css`
420
554
  ${FENIX_ICON_BASE_CSS}
421
555
  :host {
@@ -619,11 +753,11 @@ function defineFxSpinner() {
619
753
  return defineElement("fx-spinner", FxSpinner);
620
754
  }
621
755
  defineFxSpinner();
622
- const _FxSelect = class _FxSelect extends FxElement {
756
+ const _FxSelect = class _FxSelect extends FxFormElement {
623
757
  // `value` fica FORA da observação: refleti-lo no change não pode
624
758
  // re-renderizar o template e fechar o dropdown.
625
759
  static get observedAttributes() {
626
- return ["size", "disabled", "placeholder", "searchable", "clearable", "error", "invalid", "success", "valid"];
760
+ return ["size", "disabled", "placeholder", "searchable", "clearable", "error", "invalid", "success", "valid", "placement"];
627
761
  }
628
762
  /** Tamanho do campo. Padrão: `'md'`. */
629
763
  get size() {
@@ -638,6 +772,7 @@ const _FxSelect = class _FxSelect extends FxElement {
638
772
  }
639
773
  set value(value) {
640
774
  this.setAttribute("value", value);
775
+ this.updateFormValue();
641
776
  }
642
777
  get disabled() {
643
778
  return this.hasAttr("disabled");
@@ -658,7 +793,7 @@ const _FxSelect = class _FxSelect extends FxElement {
658
793
  this.docListener = (e) => {
659
794
  if (!this.hasAttr("open")) return;
660
795
  if (e.composedPath().includes(this)) return;
661
- this.removeAttribute("open");
796
+ this._fechar();
662
797
  this.render();
663
798
  };
664
799
  document.addEventListener("click", this.docListener);
@@ -667,11 +802,79 @@ const _FxSelect = class _FxSelect extends FxElement {
667
802
  super.disconnectedCallback();
668
803
  this.observer?.disconnect();
669
804
  if (this.docListener) document.removeEventListener("click", this.docListener);
805
+ this._unbindReflow();
806
+ }
807
+ /** Abre o dropdown, posiciona o painel e passa a reposicioná-lo no reflow. */
808
+ _abrir() {
809
+ this.setAttribute("open", "");
810
+ this._bindReflow();
811
+ }
812
+ /** Fecha o dropdown e libera os listeners de resize/scroll. */
813
+ _fechar() {
814
+ this.removeAttribute("open");
815
+ this.removeAttribute("data-placement");
816
+ this._unbindReflow();
817
+ }
818
+ /** Liga o reposicionamento (resize + scroll interno, em capture). */
819
+ _bindReflow() {
820
+ if (this.onReflow) return;
821
+ this.onReflow = () => {
822
+ if (this.hasAttr("open")) this.ajustarPosicao();
823
+ };
824
+ window.addEventListener("resize", this.onReflow);
825
+ window.addEventListener("scroll", this.onReflow, true);
826
+ }
827
+ _unbindReflow() {
828
+ if (!this.onReflow) return;
829
+ window.removeEventListener("resize", this.onReflow);
830
+ window.removeEventListener("scroll", this.onReflow, true);
831
+ this.onReflow = void 0;
832
+ }
833
+ /**
834
+ * Decide a posição vertical do painel medindo o espaço real da viewport.
835
+ *
836
+ * `placement="top"`/`"bottom"` são respeitados literalmente; o padrão
837
+ * (`auto`) só inverte quando o painel não cabe embaixo E couber melhor
838
+ * acima. Também limita a altura ao espaço disponível e evita overflow
839
+ * horizontal próximo à borda direita.
840
+ */
841
+ ajustarPosicao() {
842
+ const painel = this.root.querySelector(".panel");
843
+ const trigger = this.root.querySelector(".trigger");
844
+ if (!painel || !trigger) return;
845
+ const r = trigger.getBoundingClientRect();
846
+ const abaixo = window.innerHeight - r.bottom;
847
+ const acima = r.top;
848
+ const desejado = painel.scrollHeight;
849
+ const maxAltura = _FxSelect._maxAltura(this);
850
+ const pedido = this.getAttr("placement", "auto");
851
+ const paraCima = pedido === "top" || pedido !== "bottom" && abaixo < Math.min(desejado, maxAltura) && acima > abaixo;
852
+ if (paraCima) this.setAttribute("data-placement", "top");
853
+ else this.removeAttribute("data-placement");
854
+ const disponivel = Math.max(
855
+ _FxSelect.MIN_ALTURA,
856
+ (paraCima ? acima : abaixo) - _FxSelect.GAP
857
+ );
858
+ painel.style.maxHeight = `${Math.min(desejado || disponivel, disponivel, maxAltura)}px`;
859
+ painel.style.left = "";
860
+ const largura = painel.offsetWidth;
861
+ const excesso = r.left + largura - (window.innerWidth - _FxSelect.MARGEM);
862
+ if (excesso > 0) {
863
+ painel.style.left = `${-excesso}px`;
864
+ painel.style.right = "auto";
865
+ }
866
+ }
867
+ /** Altura máxima do painel a partir de `--fx-select-panel-max-height`. */
868
+ static _maxAltura(host) {
869
+ const raw = getComputedStyle(host).getPropertyValue("--fx-select-panel-max-height");
870
+ const n = parseInt(raw, 10);
871
+ return Number.isFinite(n) && n > 0 ? n : 260;
670
872
  }
671
873
  select(value) {
672
874
  this.value = value;
673
- this.removeAttribute("open");
875
+ this._fechar();
674
876
  this.render();
877
+ this.emitValueEvents();
675
878
  this.dispatchEvent(
676
879
  new CustomEvent("change", { bubbles: true, composed: true, detail: { value } })
677
880
  );
@@ -680,7 +883,7 @@ const _FxSelect = class _FxSelect extends FxElement {
680
883
  _navigateOptions(e) {
681
884
  e.preventDefault();
682
885
  if (!this.hasAttr("open")) {
683
- this.toggleAttribute("open");
886
+ this._abrir();
684
887
  this.render();
685
888
  }
686
889
  const opts = Array.from(this.root.querySelectorAll(".opt"));
@@ -725,6 +928,7 @@ const _FxSelect = class _FxSelect extends FxElement {
725
928
  </div>
726
929
  `);
727
930
  if (prevOpen) this.setAttribute("open", "");
931
+ if (this.hasAttr("open")) this.ajustarPosicao();
728
932
  const searchInput = this.root.querySelector(".search");
729
933
  if (searchInput) {
730
934
  searchInput.value = search;
@@ -744,7 +948,8 @@ const _FxSelect = class _FxSelect extends FxElement {
744
948
  trigger.addEventListener("click", (e) => {
745
949
  if (this.disabled) return;
746
950
  if (e.target.closest(".clear")) return;
747
- this.toggleAttribute("open");
951
+ if (this.hasAttr("open")) this._fechar();
952
+ else this._abrir();
748
953
  this.render();
749
954
  this.root.querySelector(".search")?.focus();
750
955
  });
@@ -757,7 +962,7 @@ const _FxSelect = class _FxSelect extends FxElement {
757
962
  }
758
963
  if (e.key === "Escape") {
759
964
  if (this.hasAttr("open")) {
760
- this.removeAttribute("open");
965
+ this._fechar();
761
966
  this.render();
762
967
  this.root.querySelector(".trigger")?.focus();
763
968
  }
@@ -770,8 +975,9 @@ const _FxSelect = class _FxSelect extends FxElement {
770
975
  this.root.querySelector(".clear")?.addEventListener("click", (e) => {
771
976
  e.stopPropagation();
772
977
  this.value = "";
773
- this.removeAttribute("open");
978
+ this._fechar();
774
979
  this.render();
980
+ this.emitValueEvents();
775
981
  this.dispatchEvent(
776
982
  new CustomEvent("change", { bubbles: true, composed: true, detail: { value: "" } })
777
983
  );
@@ -779,6 +985,12 @@ const _FxSelect = class _FxSelect extends FxElement {
779
985
  this.root.querySelectorAll(".opt").forEach((opt) => {
780
986
  opt.addEventListener("click", () => this.select(opt.dataset.value));
781
987
  });
988
+ this.updateFormValue();
989
+ }
990
+ /** form.reset(): restaura o valor inicial e re-renderiza o rótulo. */
991
+ formResetCallback() {
992
+ super.formResetCallback();
993
+ this.render();
782
994
  }
783
995
  };
784
996
  _FxSelect.styles = css`
@@ -792,6 +1004,11 @@ _FxSelect.styles = css`
792
1004
  e style inline no elemento definem a largura sem precisar do full. */
793
1005
  width: var(--fx-select-width, max-content);
794
1006
  min-width: var(--fx-select-min-width, 200px);
1007
+ /* Espaço entre o campo e o painel + altura máxima do painel. Expostos
1008
+ como custom property porque ::part() do lado do consumidor tem
1009
+ prioridade MENOR que a folha do shadow root e não vence o top. */
1010
+ --fx-select-panel-offset: 4px;
1011
+ --fx-select-panel-max-height: 260px;
795
1012
  }
796
1013
  .trigger {
797
1014
  display: inline-flex;
@@ -879,14 +1096,15 @@ _FxSelect.styles = css`
879
1096
  }
880
1097
  .caret { font-size: calc(var(--fx-font-size) - 3px); color: var(--fx-text-muted); pointer-events: none; }
881
1098
 
882
- /* Painel do dropdown */
1099
+ /* Painel do dropdown — a POSIÇÃO vertical é definida pelas regras
1100
+ [data-placement] abaixo (o JS de ajustarPosicao decide), por isso
1101
+ top/bottom não ficam aqui. */
883
1102
  .panel {
884
1103
  position: absolute;
885
- top: calc(100% + 4px);
886
1104
  left: 0;
887
1105
  z-index: var(--fx-z-dropdown, 1000);
888
1106
  width: max(100%, 220px);
889
- max-height: 260px;
1107
+ max-height: var(--fx-select-panel-max-height, 260px);
890
1108
  overflow-y: auto;
891
1109
  display: none;
892
1110
  background: var(--fx-surface-background);
@@ -894,6 +1112,16 @@ _FxSelect.styles = css`
894
1112
  border-radius: var(--fx-radius-md);
895
1113
  box-shadow: var(--fx-shadow-lg);
896
1114
  }
1115
+ /* Padrão: abre para baixo. */
1116
+ :host([open]:not([data-placement='top'])) .panel {
1117
+ top: calc(100% + var(--fx-select-panel-offset, 4px));
1118
+ }
1119
+ /* placement="top" explícito ou auto-flip: abre para cima. */
1120
+ :host([open][placement='top']) .panel,
1121
+ :host([open][data-placement='top']) .panel {
1122
+ top: auto;
1123
+ bottom: calc(100% + var(--fx-select-panel-offset, 4px));
1124
+ }
897
1125
  :host([open]) .panel { display: block; }
898
1126
  .search {
899
1127
  position: sticky;
@@ -941,12 +1169,15 @@ _FxSelect.styles = css`
941
1169
  .clear:hover { color: var(--fx-color-danger); }
942
1170
  .clear[hidden] { display: none; }
943
1171
  `;
1172
+ _FxSelect.GAP = 4;
1173
+ _FxSelect.MARGEM = 8;
1174
+ _FxSelect.MIN_ALTURA = 80;
944
1175
  let FxSelect = _FxSelect;
945
1176
  function defineFxSelect() {
946
1177
  return defineElement("fx-select", FxSelect);
947
1178
  }
948
1179
  defineFxSelect();
949
- const _FxInput = class _FxInput extends FxElement {
1180
+ const _FxInput = class _FxInput extends FxFormElement {
950
1181
  // `value` fica FORA da observação: refleti-lo a cada tecla não pode
951
1182
  // re-renderizar o template, senão o campo perde o foco ao digitar.
952
1183
  // `error`/`success` (+ aliases `invalid`/`valid`) PRECISAM ser observados:
@@ -982,6 +1213,8 @@ const _FxInput = class _FxInput extends FxElement {
982
1213
  }
983
1214
  set value(value) {
984
1215
  this.setAttribute("value", value);
1216
+ this.syncInternalControl();
1217
+ this.updateFormValue();
985
1218
  }
986
1219
  get disabled() {
987
1220
  return this.hasAttr("disabled");
@@ -1021,6 +1254,7 @@ const _FxInput = class _FxInput extends FxElement {
1021
1254
  if (readonly) field.setAttribute("readonly", "");
1022
1255
  const emit = (event) => {
1023
1256
  this.value = field.value;
1257
+ this.emitValueEvents();
1024
1258
  this.dispatchEvent(
1025
1259
  new CustomEvent(event, { bubbles: true, composed: true, detail: { value: field.value } })
1026
1260
  );
@@ -1033,6 +1267,12 @@ const _FxInput = class _FxInput extends FxElement {
1033
1267
  e.stopPropagation();
1034
1268
  emit("change");
1035
1269
  });
1270
+ field.addEventListener("keydown", (e) => {
1271
+ if (e.key !== "Enter" || e.isComposing) return;
1272
+ if (!this.form) return;
1273
+ e.preventDefault();
1274
+ this.form.requestSubmit();
1275
+ });
1036
1276
  const clearBtn = this.root.querySelector(".clear");
1037
1277
  const syncClear = () => {
1038
1278
  clearBtn?.toggleAttribute("hidden", field.value === "");
@@ -1046,6 +1286,13 @@ const _FxInput = class _FxInput extends FxElement {
1046
1286
  field.focus();
1047
1287
  });
1048
1288
  field.addEventListener("input", syncClear);
1289
+ this.updateFormValue();
1290
+ }
1291
+ /** Empurra o atributo `value` para o campo interno sem re-renderizar. */
1292
+ syncInternalControl() {
1293
+ const field = this.root.querySelector(".field");
1294
+ const value = this.getAttr("value");
1295
+ if (field && field.value !== value) field.value = value;
1049
1296
  }
1050
1297
  };
1051
1298
  _FxInput.styles = css`
@@ -1189,7 +1436,12 @@ function defineFxInput() {
1189
1436
  return defineElement("fx-input", FxInput);
1190
1437
  }
1191
1438
  defineFxInput();
1192
- const _FxSwitch = class _FxSwitch extends FxElement {
1439
+ const _FxSwitch = class _FxSwitch extends FxFormElement {
1440
+ constructor() {
1441
+ super(...arguments);
1442
+ this._initialChecked = false;
1443
+ this._initialCheckedCaptured = false;
1444
+ }
1193
1445
  static get observedAttributes() {
1194
1446
  return ["checked", "disabled", "size"];
1195
1447
  }
@@ -1213,6 +1465,17 @@ const _FxSwitch = class _FxSwitch extends FxElement {
1213
1465
  set disabled(value) {
1214
1466
  this.toggleAttr("disabled", Boolean(value));
1215
1467
  }
1468
+ /** getter/setter `value` → espelho do atributo `value` (compat com v-model / [(value)]). */
1469
+ get value() {
1470
+ return this.getAttr("value");
1471
+ }
1472
+ set value(v) {
1473
+ this.setAttribute("value", v);
1474
+ }
1475
+ /** No FormData: `value` (ou 'on') quando ligado; null (não envia) quando não. */
1476
+ getFormValue() {
1477
+ return this.checked ? this.getAttr("value") || "on" : null;
1478
+ }
1216
1479
  render() {
1217
1480
  this.setTemplate(`
1218
1481
  <button class="switch" part="switch" type="button" role="switch"
@@ -1234,7 +1497,29 @@ const _FxSwitch = class _FxSwitch extends FxElement {
1234
1497
  detail: { checked: this.checked }
1235
1498
  })
1236
1499
  );
1500
+ this.dispatchEvent(
1501
+ new CustomEvent("checkedChange", {
1502
+ bubbles: true,
1503
+ composed: true,
1504
+ detail: { checked: this.checked, value: this.getAttr("value") }
1505
+ })
1506
+ );
1237
1507
  });
1508
+ this.updateFormValue();
1509
+ }
1510
+ connectedCallback() {
1511
+ if (!this._initialCheckedCaptured) {
1512
+ this._initialCheckedCaptured = true;
1513
+ this._initialChecked = this.checked;
1514
+ }
1515
+ super.connectedCallback();
1516
+ }
1517
+ /** form.reset(): restaura o estado inicial (desligado por padrão). */
1518
+ formResetCallback() {
1519
+ this.checked = this._initialChecked;
1520
+ const btn = this.root.querySelector(".switch");
1521
+ if (btn) btn.setAttribute("aria-checked", String(this.checked));
1522
+ this.updateFormValue();
1238
1523
  }
1239
1524
  };
1240
1525
  _FxSwitch.styles = css`
@@ -1297,7 +1582,7 @@ function defineFxSwitch() {
1297
1582
  return defineElement("fx-switch", FxSwitch);
1298
1583
  }
1299
1584
  defineFxSwitch();
1300
- const _FxMultiselect = class _FxMultiselect extends FxElement {
1585
+ const _FxMultiselect = class _FxMultiselect extends FxFormElement {
1301
1586
  constructor() {
1302
1587
  super(...arguments);
1303
1588
  this.selected = /* @__PURE__ */ new Set();
@@ -1313,6 +1598,14 @@ const _FxMultiselect = class _FxMultiselect extends FxElement {
1313
1598
  set values(list) {
1314
1599
  this.setAttribute("values", list.join(","));
1315
1600
  }
1601
+ /** No FormData do form: uma entrada `name` por valor selecionado. */
1602
+ getFormValue() {
1603
+ const values = this.values;
1604
+ if (!this.name || values.length === 0) return null;
1605
+ const fd = new FormData();
1606
+ values.forEach((v) => fd.append(this.name, v));
1607
+ return fd;
1608
+ }
1316
1609
  get disabled() {
1317
1610
  return this.hasAttr("disabled");
1318
1611
  }
@@ -1327,6 +1620,7 @@ const _FxMultiselect = class _FxMultiselect extends FxElement {
1327
1620
  }
1328
1621
  connectedCallback() {
1329
1622
  this.selected = new Set(this.values);
1623
+ if (!this._initialValues) this._initialValues = [...this.values];
1330
1624
  super.connectedCallback();
1331
1625
  this.observer = new MutationObserver(() => this.render());
1332
1626
  this.observer.observe(this, { childList: true, subtree: true, characterData: true });
@@ -1347,6 +1641,7 @@ const _FxMultiselect = class _FxMultiselect extends FxElement {
1347
1641
  commit() {
1348
1642
  this.values = [...this.selected];
1349
1643
  this.render();
1644
+ this.updateFormValue();
1350
1645
  this.dispatchEvent(
1351
1646
  new CustomEvent("change", {
1352
1647
  bubbles: true,
@@ -1354,6 +1649,21 @@ const _FxMultiselect = class _FxMultiselect extends FxElement {
1354
1649
  detail: { values: [...this.selected] }
1355
1650
  })
1356
1651
  );
1652
+ this.dispatchEvent(
1653
+ new CustomEvent("valuesChange", {
1654
+ bubbles: true,
1655
+ composed: true,
1656
+ detail: { values: [...this.selected] }
1657
+ })
1658
+ );
1659
+ }
1660
+ /** form.reset(): restaura a seleção inicial. */
1661
+ formResetCallback() {
1662
+ const initial = this._initialValues ?? [];
1663
+ this.values = initial;
1664
+ this.selected = new Set(initial);
1665
+ this.render();
1666
+ this.updateFormValue();
1357
1667
  }
1358
1668
  toggleValue(value) {
1359
1669
  if (this.selected.has(value)) this.selected.delete(value);
@@ -2159,7 +2469,7 @@ function defineFxCalendar() {
2159
2469
  return defineElement("fx-calendar", FxCalendar);
2160
2470
  }
2161
2471
  defineFxCalendar();
2162
- const _FxDatepicker = class _FxDatepicker extends FxElement {
2472
+ const _FxDatepicker = class _FxDatepicker extends FxFormElement {
2163
2473
  constructor() {
2164
2474
  super(...arguments);
2165
2475
  this.time = { h: 0, m: 0, s: 0 };
@@ -2167,6 +2477,32 @@ const _FxDatepicker = class _FxDatepicker extends FxElement {
2167
2477
  static get observedAttributes() {
2168
2478
  return ["value", "start", "end", "values", "mode", "min", "max", "placeholder", "disabled", "format", "show-time", "free-text", "error", "invalid", "success", "valid"];
2169
2479
  }
2480
+ attributeChangedCallback(name, oldValue, newValue) {
2481
+ super.attributeChangedCallback(name, oldValue, newValue);
2482
+ if (name === "value" || name === "start" || name === "end" || name === "values") {
2483
+ this.updateFormValue();
2484
+ }
2485
+ }
2486
+ /** No FormData do form, conforme o modo (single/range/multiple). */
2487
+ getFormValue() {
2488
+ if (this.mode === "range") {
2489
+ const start = this.getAttr("start");
2490
+ const end = this.getAttr("end");
2491
+ if (!this.name || !start && !end) return null;
2492
+ const fd = new FormData();
2493
+ if (start) fd.append(`${this.name}-start`, start);
2494
+ if (end) fd.append(`${this.name}-end`, end);
2495
+ return fd;
2496
+ }
2497
+ if (this.mode === "multiple") {
2498
+ const values = this.values;
2499
+ if (!this.name || values.length === 0) return null;
2500
+ const fd = new FormData();
2501
+ values.forEach((v) => fd.append(this.name, v));
2502
+ return fd;
2503
+ }
2504
+ return this.getAttr("value") || null;
2505
+ }
2170
2506
  get mode() {
2171
2507
  const m = this.getAttr("mode", "single");
2172
2508
  return ["single", "range", "multiple"].includes(m) ? m : "single";
@@ -2195,6 +2531,14 @@ const _FxDatepicker = class _FxDatepicker extends FxElement {
2195
2531
  this.toggleAttr("disabled", Boolean(value));
2196
2532
  }
2197
2533
  connectedCallback() {
2534
+ if (!this._initialState) {
2535
+ this._initialState = {
2536
+ value: this.getAttr("value"),
2537
+ start: this.getAttr("start"),
2538
+ end: this.getAttr("end"),
2539
+ values: this.getAttr("values")
2540
+ };
2541
+ }
2198
2542
  super.connectedCallback();
2199
2543
  this.docListener = (e) => {
2200
2544
  if (!this.hasAttr("open")) return;
@@ -2207,6 +2551,15 @@ const _FxDatepicker = class _FxDatepicker extends FxElement {
2207
2551
  super.disconnectedCallback();
2208
2552
  if (this.docListener) document.removeEventListener("click", this.docListener);
2209
2553
  }
2554
+ /** form.reset(): restaura value/start/end/values iniciais (re-render via atributos observados). */
2555
+ formResetCallback() {
2556
+ const s = this._initialState ?? { value: "", start: "", end: "", values: "" };
2557
+ this.setAttribute("value", s.value);
2558
+ this.setAttribute("start", s.start);
2559
+ this.setAttribute("end", s.end);
2560
+ this.setAttribute("values", s.values);
2561
+ this.updateFormValue();
2562
+ }
2210
2563
  /** Formata uma data ISO ("YYYY-MM-DD" ou com "THH:mm:ss") segundo o atributo format. */
2211
2564
  formatDate(iso) {
2212
2565
  if (!iso) return "";
@@ -2379,6 +2732,7 @@ const _FxDatepicker = class _FxDatepicker extends FxElement {
2379
2732
  }
2380
2733
  }
2381
2734
  emitChange(detail) {
2735
+ this.emitValueEvents(detail);
2382
2736
  this.dispatchEvent(
2383
2737
  new CustomEvent("change", { bubbles: true, composed: true, detail })
2384
2738
  );
@@ -2602,7 +2956,12 @@ function defineFxDatepicker() {
2602
2956
  return defineElement("fx-datepicker", FxDatepicker);
2603
2957
  }
2604
2958
  defineFxDatepicker();
2605
- const _FxCheckbox = class _FxCheckbox extends FxElement {
2959
+ const _FxCheckbox = class _FxCheckbox extends FxFormElement {
2960
+ constructor() {
2961
+ super(...arguments);
2962
+ this._initialChecked = false;
2963
+ this._initialCheckedCaptured = false;
2964
+ }
2606
2965
  static get observedAttributes() {
2607
2966
  return ["checked", "disabled", "indeterminate"];
2608
2967
  }
@@ -2624,6 +2983,17 @@ const _FxCheckbox = class _FxCheckbox extends FxElement {
2624
2983
  set disabled(value) {
2625
2984
  this.toggleAttr("disabled", Boolean(value));
2626
2985
  }
2986
+ /** No FormData: `value` (ou 'on') quando marcado; null (não envia) quando não. */
2987
+ getFormValue() {
2988
+ return this.checked ? this.getAttr("value") || "on" : null;
2989
+ }
2990
+ /** getter/setter `value` → espelho do atributo `value` (compat com v-model / [(value)]). */
2991
+ get value() {
2992
+ return this.getAttr("value");
2993
+ }
2994
+ set value(v) {
2995
+ this.setAttribute("value", v);
2996
+ }
2627
2997
  /** Tamanho do controle. Padrão: `'md'`. */
2628
2998
  get size() {
2629
2999
  const s = this.getAttr("size", "md");
@@ -2656,6 +3026,20 @@ const _FxCheckbox = class _FxCheckbox extends FxElement {
2656
3026
  this.toggle();
2657
3027
  }
2658
3028
  });
3029
+ this.updateFormValue();
3030
+ }
3031
+ connectedCallback() {
3032
+ if (!this._initialCheckedCaptured) {
3033
+ this._initialCheckedCaptured = true;
3034
+ this._initialChecked = this.checked;
3035
+ }
3036
+ super.connectedCallback();
3037
+ }
3038
+ /** form.reset(): restaura o estado marcado inicial (checked/indeterminate não enviam nada). */
3039
+ formResetCallback() {
3040
+ this.checked = this._initialChecked;
3041
+ this.indeterminate = false;
3042
+ this.updateFormValue();
2659
3043
  }
2660
3044
  toggle() {
2661
3045
  if (this.disabled) return;
@@ -2668,6 +3052,13 @@ const _FxCheckbox = class _FxCheckbox extends FxElement {
2668
3052
  detail: { checked: this.checked, value: this.getAttr("value") }
2669
3053
  })
2670
3054
  );
3055
+ this.dispatchEvent(
3056
+ new CustomEvent("checkedChange", {
3057
+ bubbles: true,
3058
+ composed: true,
3059
+ detail: { checked: this.checked, value: this.getAttr("value") }
3060
+ })
3061
+ );
2671
3062
  }
2672
3063
  };
2673
3064
  _FxCheckbox.styles = css`
@@ -2737,7 +3128,12 @@ function defineFxCheckbox() {
2737
3128
  return defineElement("fx-checkbox", FxCheckbox);
2738
3129
  }
2739
3130
  defineFxCheckbox();
2740
- const _FxRadio = class _FxRadio extends FxElement {
3131
+ const _FxRadio = class _FxRadio extends FxFormElement {
3132
+ constructor() {
3133
+ super(...arguments);
3134
+ this._initialChecked = false;
3135
+ this._initialCheckedCaptured = false;
3136
+ }
2741
3137
  static get observedAttributes() {
2742
3138
  return ["checked", "disabled"];
2743
3139
  }
@@ -2753,6 +3149,25 @@ const _FxRadio = class _FxRadio extends FxElement {
2753
3149
  set disabled(value) {
2754
3150
  this.toggleAttr("disabled", Boolean(value));
2755
3151
  }
3152
+ /**
3153
+ * Grupo = radios com o mesmo `name` NA MESMA árvore. `getRootNode()` devolve
3154
+ * o document OU o ShadowRoot do app consumidor — grupos com o mesmo name
3155
+ * em shadow DOMs diferentes ficam isolados (o `document` global cruzaria
3156
+ * fronteiras indevidamente).
3157
+ */
3158
+ _group() {
3159
+ const name = this.getAttr("name");
3160
+ if (!name) return [this];
3161
+ const root = this.getRootNode();
3162
+ const safeName = name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3163
+ return Array.from(
3164
+ root.querySelectorAll(`fx-radio[name="${safeName}"]`)
3165
+ );
3166
+ }
3167
+ /** No FormData: só o radio MARCADO envia `value` (ou 'on'). */
3168
+ getFormValue() {
3169
+ return this.checked ? this.getAttr("value") || "on" : null;
3170
+ }
2756
3171
  /** Tamanho do controle. Padrão: `'md'`. */
2757
3172
  get size() {
2758
3173
  const s = this.getAttr("size", "md");
@@ -2777,19 +3192,35 @@ const _FxRadio = class _FxRadio extends FxElement {
2777
3192
  if (!box) return;
2778
3193
  box.addEventListener("click", () => this.select());
2779
3194
  box.addEventListener("keydown", (e) => {
2780
- if (e.key === " " || e.key === "Enter") {
3195
+ const key2 = e.key;
3196
+ if (key2 === " " || key2 === "Enter") {
2781
3197
  e.preventDefault();
2782
3198
  this.select();
3199
+ return;
3200
+ }
3201
+ if (key2 === "ArrowDown" || key2 === "ArrowRight" || key2 === "ArrowUp" || key2 === "ArrowLeft") {
3202
+ e.preventDefault();
3203
+ this._moveFocus(key2 === "ArrowDown" || key2 === "ArrowRight" ? 1 : -1);
2783
3204
  }
2784
3205
  });
3206
+ this.updateFormValue();
3207
+ }
3208
+ /** Move o foco/marcação para o próximo (ou anterior) radio do grupo. */
3209
+ _moveFocus(dir) {
3210
+ const group = this._group().filter((r) => !r.disabled);
3211
+ if (group.length < 2) return;
3212
+ const i = group.indexOf(this);
3213
+ const next = group[(i + dir + group.length) % group.length];
3214
+ next.focus();
3215
+ if (!next.root.activeElement) {
3216
+ next.root.querySelector(".box")?.focus();
3217
+ }
3218
+ if (!next.checked) next.select();
2785
3219
  }
2786
3220
  select() {
2787
3221
  if (this.disabled || this.checked) return;
2788
- const name = this.getAttr("name");
2789
- if (name) {
2790
- document.querySelectorAll(`fx-radio[name="${CSS.escape(name)}"]`).forEach((r) => {
2791
- r.checked = false;
2792
- });
3222
+ for (const r of this._group()) {
3223
+ if (r !== this) r.checked = false;
2793
3224
  }
2794
3225
  this.checked = true;
2795
3226
  this.dispatchEvent(
@@ -2799,6 +3230,25 @@ const _FxRadio = class _FxRadio extends FxElement {
2799
3230
  detail: { checked: true, value: this.getAttr("value") }
2800
3231
  })
2801
3232
  );
3233
+ this.dispatchEvent(
3234
+ new CustomEvent("checkedChange", {
3235
+ bubbles: true,
3236
+ composed: true,
3237
+ detail: { checked: true, value: this.getAttr("value") }
3238
+ })
3239
+ );
3240
+ }
3241
+ connectedCallback() {
3242
+ if (!this._initialCheckedCaptured) {
3243
+ this._initialCheckedCaptured = true;
3244
+ this._initialChecked = this.checked;
3245
+ }
3246
+ super.connectedCallback();
3247
+ }
3248
+ /** form.reset(): restaura o estado marcado inicial do grupo. */
3249
+ formResetCallback() {
3250
+ this.checked = this._initialChecked;
3251
+ this.updateFormValue();
2802
3252
  }
2803
3253
  };
2804
3254
  _FxRadio.styles = css`
@@ -3974,28 +4424,43 @@ const _FxFloatlabel = class _FxFloatlabel extends FxElement {
3974
4424
  this.targetControl = null;
3975
4425
  this.observer = null;
3976
4426
  this.listenersReady = false;
4427
+ this._label = null;
4428
+ this._labelClickBound = false;
3977
4429
  }
3978
4430
  static get observedAttributes() {
3979
4431
  return ["variant", "error", "invalid", "success", "valid", "full", "icon", "icon-pos"];
3980
4432
  }
3981
4433
  render() {
3982
4434
  const control = this.findControl();
3983
- const label = this.findLabel();
3984
- const originalLabel = label;
3985
- const labelText = originalLabel ? originalLabel.textContent?.replace(/\s+/g, " ").trim() : "";
3986
- const labelFor = originalLabel ? originalLabel.getAttribute("for") || "" : "";
4435
+ const lightLabel = this.findLabel();
4436
+ if (lightLabel) this._label = lightLabel;
4437
+ const label = this._label;
3987
4438
  const isOver = this.getAttribute("variant") === "over";
3988
4439
  this.setTemplate(`
3989
4440
  <div class="wrapper" part="wrapper">
3990
- ${isOver ? `<label class="flabel" part="label" for="${labelFor}">${labelText}</label>` : ""}
3991
4441
  <div class="field-box" part="field">
3992
4442
  <slot></slot>
3993
- ${isOver ? "" : `<label class="flabel" part="label" for="${labelFor}">${labelText}</label>`}
3994
4443
  </div>
3995
4444
  <div class="error-message" part="error"></div>
3996
4445
  </div>
3997
4446
  `);
3998
- if (originalLabel) originalLabel.setAttribute("hidden", "");
4447
+ if (label) {
4448
+ label.classList.add("flabel");
4449
+ label.setAttribute("part", "label");
4450
+ const wrapper = this.root.querySelector(".wrapper");
4451
+ const fieldBox = this.root.querySelector(".field-box");
4452
+ if (wrapper && fieldBox) {
4453
+ if (isOver) wrapper.insertBefore(label, fieldBox);
4454
+ else fieldBox.appendChild(label);
4455
+ }
4456
+ if (!this._labelClickBound) {
4457
+ this._labelClickBound = true;
4458
+ label.addEventListener("click", (e) => {
4459
+ e.preventDefault();
4460
+ this.targetControl?.focus?.();
4461
+ });
4462
+ }
4463
+ }
3999
4464
  this.attachEvents();
4000
4465
  this.syncControlReference(control);
4001
4466
  this.syncState();
@@ -4294,7 +4759,7 @@ function defineFxFloatlabel() {
4294
4759
  return defineElement("fx-floatlabel", FxFloatlabel);
4295
4760
  }
4296
4761
  defineFxFloatlabel();
4297
- const _FxTextarea = class _FxTextarea extends FxElement {
4762
+ const _FxTextarea = class _FxTextarea extends FxFormElement {
4298
4763
  static get observedAttributes() {
4299
4764
  return ["size", "placeholder", "disabled", "readonly", "rows", "maxlength", "error", "invalid", "success", "valid"];
4300
4765
  }
@@ -4310,6 +4775,8 @@ const _FxTextarea = class _FxTextarea extends FxElement {
4310
4775
  }
4311
4776
  set value(value) {
4312
4777
  this.setAttribute("value", value);
4778
+ this.syncInternalControl();
4779
+ this.updateFormValue();
4313
4780
  }
4314
4781
  render() {
4315
4782
  const placeholder = this.getAttr("placeholder");
@@ -4329,6 +4796,7 @@ const _FxTextarea = class _FxTextarea extends FxElement {
4329
4796
  if (readonly) field.setAttribute("readonly", "");
4330
4797
  const emit = (event) => {
4331
4798
  this.value = field.value;
4799
+ this.emitValueEvents();
4332
4800
  this.dispatchEvent(
4333
4801
  new CustomEvent(event, { bubbles: true, composed: true, detail: { value: field.value } })
4334
4802
  );
@@ -4341,6 +4809,13 @@ const _FxTextarea = class _FxTextarea extends FxElement {
4341
4809
  e.stopPropagation();
4342
4810
  emit("change");
4343
4811
  });
4812
+ this.updateFormValue();
4813
+ }
4814
+ /** Empurra o atributo `value` para o campo interno sem re-renderizar. */
4815
+ syncInternalControl() {
4816
+ const field = this.root.querySelector(".field");
4817
+ const value = this.getAttr("value");
4818
+ if (field && field.value !== value) field.value = value;
4344
4819
  }
4345
4820
  };
4346
4821
  _FxTextarea.styles = css`
@@ -4883,7 +5358,7 @@ function defineFxConfirmPopup() {
4883
5358
  return defineElement("fx-confirmpopup", FxConfirmPopup);
4884
5359
  }
4885
5360
  defineFxConfirmPopup();
4886
- const _FxFileUpload = class _FxFileUpload extends FxElement {
5361
+ const _FxFileUpload = class _FxFileUpload extends FxFormElement {
4887
5362
  constructor() {
4888
5363
  super(...arguments);
4889
5364
  this._files = [];
@@ -4921,6 +5396,14 @@ const _FxFileUpload = class _FxFileUpload extends FxElement {
4921
5396
  this.value = this._files.map((f) => f.name).join(", ");
4922
5397
  this._syncFilesArea();
4923
5398
  }
5399
+ /** No FormData do form: os arquivos selecionados, como o <input type="file">. */
5400
+ getFormValue() {
5401
+ const files = this._files;
5402
+ if (!this.name || files.length === 0) return null;
5403
+ const fd = new FormData();
5404
+ for (const f of files) fd.append(this.name, f, f.name);
5405
+ return fd;
5406
+ }
4924
5407
  render() {
4925
5408
  const mode = this.getAttr("mode", "basic");
4926
5409
  const advanced = mode === "advanced";
@@ -4953,6 +5436,7 @@ const _FxFileUpload = class _FxFileUpload extends FxElement {
4953
5436
  `);
4954
5437
  this._bindPicker();
4955
5438
  this._syncFilesArea();
5439
+ this.updateFormValue();
4956
5440
  if (!this._done && showProgress && pct >= 100 && this._files.length) {
4957
5441
  this._done = true;
4958
5442
  this.dispatchEvent(new CustomEvent("complete", { bubbles: true, composed: true }));
@@ -5001,6 +5485,12 @@ const _FxFileUpload = class _FxFileUpload extends FxElement {
5001
5485
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
5002
5486
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5003
5487
  }
5488
+ /** form.reset(): limpa os arquivos selecionados. */
5489
+ formResetCallback() {
5490
+ this._files = [];
5491
+ this.value = "";
5492
+ this.updateFormValue();
5493
+ }
5004
5494
  /** Renderiza a lista de arquivos sem re-renderizar o template (mantém estado). */
5005
5495
  _syncFilesArea() {
5006
5496
  const area = this.root?.querySelector?.(".files");
@@ -5172,7 +5662,7 @@ function defineFxFileUpload() {
5172
5662
  return defineElement("fx-fileupload", FxFileUpload);
5173
5663
  }
5174
5664
  defineFxFileUpload();
5175
- const _FxSlider = class _FxSlider extends FxElement {
5665
+ const _FxSlider = class _FxSlider extends FxFormElement {
5176
5666
  constructor() {
5177
5667
  super(...arguments);
5178
5668
  this._dragging = false;
@@ -5196,6 +5686,7 @@ const _FxSlider = class _FxSlider extends FxElement {
5196
5686
  const snapped = this._snap(v);
5197
5687
  this.setAttribute("value", String(snapped));
5198
5688
  if (this.isConnected) this._syncVisual();
5689
+ this.updateFormValue();
5199
5690
  }
5200
5691
  _num(attr, fallback) {
5201
5692
  const raw = this.getAttribute(attr);
@@ -5270,6 +5761,7 @@ const _FxSlider = class _FxSlider extends FxElement {
5270
5761
  window.removeEventListener("pointerup", up);
5271
5762
  this.value = valueFromEvent(e);
5272
5763
  this._syncVisual();
5764
+ this.emitValueEvents();
5273
5765
  this.dispatchEvent(
5274
5766
  new CustomEvent("change", { bubbles: true, composed: true, detail: { value: this.value } })
5275
5767
  );
@@ -5324,6 +5816,7 @@ const _FxSlider = class _FxSlider extends FxElement {
5324
5816
  ev.preventDefault();
5325
5817
  this.value = next;
5326
5818
  this._syncVisual();
5819
+ this.emitValueEvents();
5327
5820
  this.dispatchEvent(
5328
5821
  new CustomEvent("input", { bubbles: true, composed: true, detail: { value: this.value } })
5329
5822
  );
@@ -5350,6 +5843,11 @@ const _FxSlider = class _FxSlider extends FxElement {
5350
5843
  const labelPct = this.root.querySelector(".label span:last-child");
5351
5844
  if (labelPct) labelPct.textContent = String(this.value);
5352
5845
  }
5846
+ /** form.reset(): restaura o valor inicial e o visual sem re-render. */
5847
+ formResetCallback() {
5848
+ super.formResetCallback();
5849
+ this._syncVisual();
5850
+ }
5353
5851
  };
5354
5852
  _FxSlider.styles = css`
5355
5853
  :host {
@@ -5720,6 +6218,24 @@ const _FxCard = class _FxCard extends FxElement {
5720
6218
  <footer part="footer"><slot name="footer"></slot></footer>
5721
6219
  </div>
5722
6220
  `);
6221
+ this._syncSlots();
6222
+ for (const name of ["header", "footer"]) {
6223
+ this.root.querySelector(`slot[name="${name}"]`)?.addEventListener("slotchange", () => this._syncSlots());
6224
+ }
6225
+ }
6226
+ /**
6227
+ * Esconde `header`/`footer` quando o respectivo slot não recebeu nenhum nó.
6228
+ * O `<slot>` fica no shadow e os nós no light DOM, então a checagem é feita
6229
+ * via `assignedNodes()` — cobre conteúdo adicionado dinamicamente.
6230
+ */
6231
+ _syncSlots() {
6232
+ for (const name of ["header", "footer"]) {
6233
+ const slot = this.root.querySelector(`slot[name="${name}"]`);
6234
+ const section = this.root.querySelector(name);
6235
+ if (!slot || !section) continue;
6236
+ const temConteudo = slot.assignedNodes({ flatten: true }).some((n) => n.nodeType === 1 || (n.textContent ?? "").trim() !== "") || name === "header" && Boolean(this.getAttr("heading"));
6237
+ section.toggleAttribute("hidden", !temConteudo);
6238
+ }
5723
6239
  }
5724
6240
  };
5725
6241
  _FxCard.styles = css`
@@ -6274,7 +6790,7 @@ function defineFxStepper() {
6274
6790
  return defineElement("fx-stepper", FxStepper);
6275
6791
  }
6276
6792
  defineFxStepper();
6277
- const _FxRating = class _FxRating extends FxElement {
6793
+ const _FxRating = class _FxRating extends FxFormElement {
6278
6794
  static get observedAttributes() {
6279
6795
  return ["value", "max", "readonly", "disabled", "allow-half", "size"];
6280
6796
  }
@@ -6285,6 +6801,7 @@ const _FxRating = class _FxRating extends FxElement {
6285
6801
  set value(v) {
6286
6802
  this.setAttribute("value", String(Math.min(this.max, Math.max(0, v))));
6287
6803
  this._syncVisual();
6804
+ this.updateFormValue();
6288
6805
  }
6289
6806
  get max() {
6290
6807
  const n = Number(this.getAttr("max", "5"));
@@ -6304,6 +6821,7 @@ const _FxRating = class _FxRating extends FxElement {
6304
6821
  `);
6305
6822
  this._bindStars();
6306
6823
  this._syncVisual();
6824
+ this.updateFormValue();
6307
6825
  }
6308
6826
  _starNodes() {
6309
6827
  return Array.from(this.root.querySelectorAll(".star"));
@@ -6327,6 +6845,7 @@ const _FxRating = class _FxRating extends FxElement {
6327
6845
  let next = n;
6328
6846
  if (this.hasAttr("allow-half")) next += -0.5;
6329
6847
  this.value = next;
6848
+ this.emitValueEvents();
6330
6849
  this.dispatchEvent(
6331
6850
  new CustomEvent("change", { bubbles: true, composed: true, detail: { value: this.value } })
6332
6851
  );
@@ -6533,7 +7052,7 @@ function defineFxMenu() {
6533
7052
  return defineElement("fx-menu", FxMenu);
6534
7053
  }
6535
7054
  defineFxMenu();
6536
- const _FxToggleButtonGroup = class _FxToggleButtonGroup extends FxElement {
7055
+ const _FxToggleButtonGroup = class _FxToggleButtonGroup extends FxFormElement {
6537
7056
  static get observedAttributes() {
6538
7057
  return ["multiple", "size"];
6539
7058
  }
@@ -6543,10 +7062,19 @@ const _FxToggleButtonGroup = class _FxToggleButtonGroup extends FxElement {
6543
7062
  }
6544
7063
  set value(v) {
6545
7064
  this.setAttribute("value", v.join(","));
7065
+ this.updateFormValue();
6546
7066
  }
6547
7067
  get multiple() {
6548
7068
  return this.hasAttr("multiple");
6549
7069
  }
7070
+ /** No FormData do form: uma entrada `name` por valor selecionado. */
7071
+ getFormValue() {
7072
+ const values = this.value;
7073
+ if (!this.name || values.length === 0) return null;
7074
+ const fd = new FormData();
7075
+ values.forEach((v) => fd.append(this.name, v));
7076
+ return fd;
7077
+ }
6550
7078
  get options() {
6551
7079
  return Array.from(this.children).filter((el) => el.matches('[slot="option"]')).map((el) => el);
6552
7080
  }
@@ -6564,6 +7092,13 @@ const _FxToggleButtonGroup = class _FxToggleButtonGroup extends FxElement {
6564
7092
  </div>
6565
7093
  `);
6566
7094
  this._bindOptions();
7095
+ this.updateFormValue();
7096
+ }
7097
+ /** form.reset(): restaura a seleção inicial e re-renderiza os botões. */
7098
+ formResetCallback() {
7099
+ this.setAttribute("value", this._initialValue);
7100
+ this.render();
7101
+ this.updateFormValue();
6567
7102
  }
6568
7103
  _bindOptions() {
6569
7104
  Array.from(this.root.querySelectorAll(".option") || []).forEach((opt) => {
@@ -6593,6 +7128,7 @@ const _FxToggleButtonGroup = class _FxToggleButtonGroup extends FxElement {
6593
7128
  o.setAttribute("aria-pressed", String(on));
6594
7129
  });
6595
7130
  this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true, detail: { value: current } }));
7131
+ this.dispatchEvent(new CustomEvent("valuesChange", { bubbles: true, composed: true, detail: { values: [...current] } }));
6596
7132
  }
6597
7133
  };
6598
7134
  _FxToggleButtonGroup.styles = css`
@@ -7273,6 +7809,7 @@ const KIND_ICON = {
7273
7809
  warning: "!",
7274
7810
  info: "i"
7275
7811
  };
7812
+ const COPY_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
7276
7813
  const CARD_CSS = `
7277
7814
  :host { display: contents; pointer-events: auto; }
7278
7815
  /* Modo explícito: sobrepõe os tokens herdados do tema global SOMENTE neste card. */
@@ -7306,8 +7843,11 @@ const CARD_CSS = `
7306
7843
  user-select: none;
7307
7844
  }
7308
7845
  .card {
7309
- display: flex; align-items: flex-start; gap: 10px;
7846
+ display: flex; align-items: center; gap: 10px;
7310
7847
  min-width: 260px; max-width: 360px;
7848
+ /* Altura mínima idêntica à de um toast COM título: sem título o card não
7849
+ fica "achatado" e o conjunto mantém o mesmo ritmo visual. */
7850
+ min-height: 56px;
7311
7851
  background: var(--fx-surface-background, #fff);
7312
7852
  color: var(--fx-text-default, #1e293b);
7313
7853
  border: 1px solid var(--fx-border-default, #e2e8f0);
@@ -7326,16 +7866,45 @@ const CARD_CSS = `
7326
7866
  color: #fff;
7327
7867
  display: flex; align-items: center; justify-content: center;
7328
7868
  font-size: 12px; font-weight: 700;
7329
- flex: none; margin-top: 2px;
7869
+ flex: none; align-self: center;
7330
7870
  user-select: none;
7331
7871
  }
7332
- .body { flex: 1; min-width: 0; }
7872
+ .body { flex: 1; min-width: 0; align-self: center; }
7333
7873
  .title { font-weight: 600; font-size: 14px; }
7334
- .msg { font-size: calc(var(--fx-font-size, 14px) - 2px); color: var(--fx-text-muted, #64748b); margin-top: 2px; word-break: break-word; white-space: pre-line; }
7335
- .close {
7874
+ .msg {
7875
+ font-size: calc(var(--fx-font-size, 14px) - 2px);
7876
+ color: var(--fx-text-muted, #64748b);
7877
+ margin-top: 2px;
7878
+ word-break: break-word; white-space: pre-line;
7879
+ /* Mensagens longas mantêm o card no tamanho padrão e terminam em "…";
7880
+ "Ver mais" expande o toast (sem perder o restante do texto). */
7881
+ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
7882
+ line-clamp: 2; overflow: hidden;
7883
+ }
7884
+ .card.expanded .msg {
7885
+ display: block; overflow: visible;
7886
+ -webkit-line-clamp: unset; line-clamp: unset;
7887
+ }
7888
+ .more {
7889
+ all: unset; cursor: pointer; display: inline-block;
7890
+ margin-top: 4px; padding: 2px 0;
7891
+ font-size: 11px; font-weight: 600; color: var(--kind);
7892
+ }
7893
+ .more[hidden] { display: none; }
7894
+ .more:hover { text-decoration: underline; }
7895
+ .actions {
7896
+ display: flex; align-items: center; gap: 2px;
7897
+ flex: none; align-self: flex-start;
7898
+ }
7899
+ .copy, .close {
7336
7900
  all: unset; cursor: pointer; flex: none; line-height: 1;
7337
7901
  color: var(--fx-text-muted, #64748b); font-size: 15px; padding: 2px 4px; border-radius: 4px;
7338
7902
  }
7903
+ .copy[hidden] { display: none; }
7904
+ .copy { font-size: 14px; display: inline-flex; align-items: center; }
7905
+ .copy svg { display: block; }
7906
+ .copy.done { color: var(--fx-color-success, #10b981); }
7907
+ .copy:hover { color: var(--fx-color-primary, #6366f1); background: var(--fx-surface-surface-hover, rgba(0,0,0,.05)); }
7339
7908
  .close:hover { color: var(--fx-color-danger, #f43f5e); background: var(--fx-surface-surface-hover, rgba(0,0,0,.05)); }
7340
7909
 
7341
7910
  /* --- Barra de contagem regressiva (atributo progress) ---
@@ -7364,9 +7933,12 @@ const CARD_CSS = `
7364
7933
  will-change: transform;
7365
7934
  }
7366
7935
  .progress.run { animation: fx-toast-drain var(--toast-duration, 4000ms) linear forwards; }
7367
- /* No modo hover a animação fica parada até o cursor/foco entrar no card. */
7936
+ /* Passar o mouse (ou focar) no toast pausa a contagem: a barra e o timer
7937
+ congelam juntos até o cursor sair — tempo de ler a mensagem inteira.
7938
+ progress="hover" é mantido por compatibilidade e se comporta como
7939
+ "always" (o hover já pausa a contagem). */
7368
7940
  .card:hover .progress.run,
7369
- .card:focus-within .progress.run { animation-play-state: running; }
7941
+ .card:focus-within .progress.run { animation-play-state: paused; }
7370
7942
  @keyframes fx-toast-drain {
7371
7943
  from { transform: scaleX(1); }
7372
7944
  to { transform: scaleX(0); }
@@ -7385,6 +7957,12 @@ class FxToast extends HTMLElement {
7385
7957
  super();
7386
7958
  __privateAdd(this, _FxToast_instances);
7387
7959
  this.timer = null;
7960
+ this.remaining = 0;
7961
+ this.startedAt = 0;
7962
+ this.hovering = false;
7963
+ this.disposed = false;
7964
+ this.counting = false;
7965
+ this.wired = false;
7388
7966
  this.attachShadow({ mode: "open" });
7389
7967
  }
7390
7968
  static get observedAttributes() {
@@ -7393,19 +7971,19 @@ class FxToast extends HTMLElement {
7393
7971
  /**
7394
7972
  * Exibe a barra de contagem regressiva na base do card.
7395
7973
  *
7396
- * A barra esvazia da esquerda para a direita durante exatamente `duration`
7397
- * e usa a cor do `kind`. Com `duration="0"` (toast fixo) a barra é
7398
- * omitida — não há tempo restante a representar.
7974
+ * A barra esvazia durante exatamente `duration` e usa a cor do `kind`.
7975
+ * Com `duration="0"` (toast fixo) a barra é omitida — não há tempo restante.
7399
7976
  *
7400
7977
  * - `'always'` (ou atributo vazio): barra sempre correndo.
7401
- * - `'hover'`: a barra só corre enquanto o cursor estiver sobre o toast;
7402
- * ao sair, a animação pausa, então dá tempo de ler a mensagem.
7403
7978
  * - `'paused'`: barra visível, porém parada.
7979
+ * - `'hover'`: **compatibilidade** — equivale a `'always'`. O hover agora
7980
+ * pausa a contagem (timer + barra) em qualquer modo, então a barra "só
7981
+ * correr sob o cursor" deixou de fazer sentido.
7404
7982
  */
7405
7983
  get progress() {
7406
7984
  if (!this.hasAttribute("progress")) return void 0;
7407
7985
  const v = this.getAttribute("progress");
7408
- return v === "hover" || v === "paused" ? v : "always";
7986
+ return v === "paused" ? "paused" : "always";
7409
7987
  }
7410
7988
  set progress(value) {
7411
7989
  if (value) this.setAttribute("progress", value);
@@ -7445,11 +8023,13 @@ class FxToast extends HTMLElement {
7445
8023
  }
7446
8024
  connectedCallback() {
7447
8025
  if (!this.shadowRoot.firstChild) __privateMethod(this, _FxToast_instances, render_fn).call(this);
8026
+ this.disposed = false;
8027
+ __privateMethod(this, _FxToast_instances, wirePause_fn).call(this);
7448
8028
  const safe = this.safeDuration;
7449
- if (safe > 0 && !this.timer) {
7450
- this.timer = setTimeout(() => this.dismiss(), safe);
7451
- }
8029
+ this.counting = safe > 0;
8030
+ if (safe > 0 && !this.timer) __privateMethod(this, _FxToast_instances, startTimer_fn).call(this, safe);
7452
8031
  __privateMethod(this, _FxToast_instances, syncProgress_fn).call(this);
8032
+ __privateMethod(this, _FxToast_instances, syncMore_fn).call(this);
7453
8033
  }
7454
8034
  /**
7455
8035
  * Limpa recursos ao sair do DOM e avisa a API imperativa (`FenixToast`).
@@ -7460,11 +8040,21 @@ class FxToast extends HTMLElement {
7460
8040
  * sem limite em uso prolongado.
7461
8041
  */
7462
8042
  disconnectedCallback() {
8043
+ this.disposed = true;
7463
8044
  if (this.timer) clearTimeout(this.timer);
7464
8045
  this.timer = null;
7465
8046
  this.dispatchEvent(new CustomEvent(REMOVED_EVENT, { bubbles: false, composed: false }));
7466
8047
  }
7467
8048
  attributeChangedCallback(name) {
8049
+ if (name === "duration") {
8050
+ if (this.timer) {
8051
+ clearTimeout(this.timer);
8052
+ this.timer = null;
8053
+ }
8054
+ const safe = this.safeDuration;
8055
+ this.counting = safe > 0;
8056
+ if (safe > 0 && this.isConnected && !this.disposed) __privateMethod(this, _FxToast_instances, startTimer_fn).call(this, safe);
8057
+ }
7468
8058
  if (name === "duration" || name === "progress" || name === "progress-origin" || name === "kind") {
7469
8059
  __privateMethod(this, _FxToast_instances, syncProgress_fn).call(this);
7470
8060
  }
@@ -7482,6 +8072,49 @@ class FxToast extends HTMLElement {
7482
8072
  }
7483
8073
  }
7484
8074
  _FxToast_instances = new WeakSet();
8075
+ /**
8076
+ * Pausa a contagem enquanto o cursor (ou o foco) estiver no toast e retoma
8077
+ * de onde parou ao sair — assim uma mensagem longa pode ser lida inteira.
8078
+ * A barra de progresso pausa junto (regra CSS de :hover/:focus-within).
8079
+ */
8080
+ wirePause_fn = function() {
8081
+ if (this.wired) return;
8082
+ this.wired = true;
8083
+ this.addEventListener("mouseenter", () => {
8084
+ this.hovering = true;
8085
+ __privateMethod(this, _FxToast_instances, pauseTimer_fn).call(this);
8086
+ });
8087
+ this.addEventListener("mouseleave", () => {
8088
+ this.hovering = false;
8089
+ __privateMethod(this, _FxToast_instances, resumeTimer_fn).call(this);
8090
+ });
8091
+ this.addEventListener("focusin", () => __privateMethod(this, _FxToast_instances, pauseTimer_fn).call(this));
8092
+ this.addEventListener("focusout", () => {
8093
+ if (!this.hovering) __privateMethod(this, _FxToast_instances, resumeTimer_fn).call(this);
8094
+ });
8095
+ };
8096
+ startTimer_fn = function(ms) {
8097
+ this.remaining = ms;
8098
+ this.startedAt = Date.now();
8099
+ this.timer = setTimeout(() => {
8100
+ this.timer = null;
8101
+ this.dismiss();
8102
+ }, ms);
8103
+ };
8104
+ pauseTimer_fn = function() {
8105
+ if (!this.timer) return;
8106
+ clearTimeout(this.timer);
8107
+ this.timer = null;
8108
+ this.remaining = Math.max(0, this.remaining - (Date.now() - this.startedAt));
8109
+ };
8110
+ resumeTimer_fn = function() {
8111
+ if (!this.counting || this.timer || this.disposed) return;
8112
+ if (this.remaining <= 0) {
8113
+ this.dismiss();
8114
+ return;
8115
+ }
8116
+ __privateMethod(this, _FxToast_instances, startTimer_fn).call(this, this.remaining);
8117
+ };
7485
8118
  /**
7486
8119
  * Aplica (ou remove) a barra de contagem regressiva.
7487
8120
  *
@@ -7526,17 +8159,60 @@ render_fn = function() {
7526
8159
  <div class="card" style="--kind:${color}" role="status" part="card">
7527
8160
  <span class="icon${isFxIcon ? " fx-icon" : ""}">${iconContent}</span>
7528
8161
  <div class="body">
7529
- ${title ? '<div class="title"></div>' : ""}
7530
- ${msg ? '<div class="msg"></div>' : ""}
8162
+ ${title ? '<div class="title" part="title"></div>' : ""}
8163
+ ${msg ? '<div class="msg" part="message"></div>' : ""}
8164
+ ${msg ? '<button class="more" part="more" hidden>Ver mais</button>' : ""}
8165
+ </div>
8166
+ <div class="actions">
8167
+ ${msg ? `<button class="copy" part="copy" hidden aria-label="Copiar mensagem" title="Copiar mensagem">${COPY_SVG}</button>` : ""}
8168
+ <button class="close" aria-label="Fechar" title="Fechar">✕</button>
7531
8169
  </div>
7532
- <button class="close" aria-label="Fechar">✕</button>
7533
8170
  </div>`;
7534
8171
  const t = this.shadowRoot.querySelector(".title");
7535
8172
  const m = this.shadowRoot.querySelector(".msg");
7536
8173
  if (t) t.textContent = title;
7537
- if (m) m.textContent = msg;
8174
+ if (m) {
8175
+ m.textContent = msg;
8176
+ m.title = msg;
8177
+ }
8178
+ const card = this.shadowRoot.querySelector(".card");
8179
+ const more = this.shadowRoot.querySelector(".more");
8180
+ const copy = this.shadowRoot.querySelector(".copy");
8181
+ more?.addEventListener("click", () => {
8182
+ const expanded = card.classList.toggle("expanded");
8183
+ more.textContent = expanded ? "Ver menos" : "Ver mais";
8184
+ if (copy) copy.hidden = !expanded;
8185
+ if (expanded) __privateMethod(this, _FxToast_instances, pauseTimer_fn).call(this);
8186
+ });
8187
+ copy?.addEventListener("click", (e) => {
8188
+ const btn = e.currentTarget;
8189
+ const text = [title, msg].filter(Boolean).join("\n");
8190
+ navigator.clipboard?.writeText(text).catch(() => {
8191
+ });
8192
+ btn.classList.add("done");
8193
+ btn.title = "Copiado!";
8194
+ setTimeout(() => {
8195
+ btn.classList.remove("done");
8196
+ btn.title = "Copiar mensagem";
8197
+ }, 1200);
8198
+ });
7538
8199
  this.shadowRoot.querySelector(".close").addEventListener("click", () => this.dismiss());
7539
8200
  };
8201
+ /**
8202
+ * Mostra "Ver mais" somente quando a mensagem passou de 2 linhas.
8203
+ * Sem layout (jsdom) scrollHeight/clientHeight são 0 → fica oculto.
8204
+ */
8205
+ syncMore_fn = function() {
8206
+ const card = this.shadowRoot?.querySelector(".card");
8207
+ const more = this.shadowRoot?.querySelector(".more");
8208
+ const msg = this.shadowRoot?.querySelector(".msg");
8209
+ if (!card || !more || !msg) return;
8210
+ if (card.classList.contains("expanded")) {
8211
+ more.hidden = false;
8212
+ return;
8213
+ }
8214
+ more.hidden = !(msg.scrollHeight - msg.clientHeight > 1);
8215
+ };
7540
8216
  function regionFor(position) {
7541
8217
  let region = document.querySelector(`[data-fx-toast-region="${position}"]`);
7542
8218
  if (!region) {
@@ -8904,7 +9580,7 @@ function defineFxPagination() {
8904
9580
  return defineElement("fx-pagination", FxPagination);
8905
9581
  }
8906
9582
  defineFxPagination();
8907
- const _FxAutocomplete = class _FxAutocomplete extends FxElement {
9583
+ const _FxAutocomplete = class _FxAutocomplete extends FxFormElement {
8908
9584
  static get observedAttributes() {
8909
9585
  return ["size", "placeholder", "source", "disabled", "min-chars", "error", "invalid", "success", "valid"];
8910
9586
  }
@@ -8920,6 +9596,8 @@ const _FxAutocomplete = class _FxAutocomplete extends FxElement {
8920
9596
  }
8921
9597
  set value(v) {
8922
9598
  this.setAttribute("value", v);
9599
+ this.syncInternalControl();
9600
+ this.updateFormValue();
8923
9601
  }
8924
9602
  get source() {
8925
9603
  try {
@@ -8969,6 +9647,7 @@ const _FxAutocomplete = class _FxAutocomplete extends FxElement {
8969
9647
  field.value = btn.dataset.v ?? "";
8970
9648
  this.value = field.value;
8971
9649
  this.toggleAttr("open", false);
9650
+ this.emitValueEvents();
8972
9651
  this.dispatchEvent(new CustomEvent("select", {
8973
9652
  bubbles: true,
8974
9653
  composed: true,
@@ -8980,6 +9659,7 @@ const _FxAutocomplete = class _FxAutocomplete extends FxElement {
8980
9659
  };
8981
9660
  field.addEventListener("input", () => {
8982
9661
  this.value = field.value;
9662
+ this.emitValueEvents();
8983
9663
  openList();
8984
9664
  });
8985
9665
  field.addEventListener("focus", () => {
@@ -8990,6 +9670,11 @@ const _FxAutocomplete = class _FxAutocomplete extends FxElement {
8990
9670
  this.toggleAttr("open", false);
8991
9671
  return;
8992
9672
  }
9673
+ if (e.key === "Enter" && !e.isComposing && !this.hasAttr("open") && this.form) {
9674
+ e.preventDefault();
9675
+ this.form.requestSubmit();
9676
+ return;
9677
+ }
8993
9678
  if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
8994
9679
  e.preventDefault();
8995
9680
  if (!this.hasAttr("open")) openList();
@@ -8999,6 +9684,13 @@ const _FxAutocomplete = class _FxAutocomplete extends FxElement {
8999
9684
  const next = e.key === "ArrowDown" ? current === -1 ? 0 : Math.min(current + 1, opts.length - 1) : current === -1 ? opts.length - 1 : Math.max(current - 1, 0);
9000
9685
  opts[next]?.focus();
9001
9686
  });
9687
+ this.updateFormValue();
9688
+ }
9689
+ /** Empurra o atributo `value` para o campo interno sem re-renderizar. */
9690
+ syncInternalControl() {
9691
+ const field = this.root.querySelector(".field");
9692
+ const value = this.getAttr("value");
9693
+ if (field && field.value !== value) field.value = value;
9002
9694
  }
9003
9695
  };
9004
9696
  _FxAutocomplete.styles = css`
@@ -9092,7 +9784,7 @@ function defineFxAutocomplete() {
9092
9784
  return defineElement("fx-autocomplete", FxAutocomplete);
9093
9785
  }
9094
9786
  defineFxAutocomplete();
9095
- const _FxKnob = class _FxKnob extends FxElement {
9787
+ const _FxKnob = class _FxKnob extends FxFormElement {
9096
9788
  constructor() {
9097
9789
  super(...arguments);
9098
9790
  this._isDragging = false;
@@ -9237,6 +9929,7 @@ const _FxKnob = class _FxKnob extends FxElement {
9237
9929
  </div>
9238
9930
  `);
9239
9931
  this.attachListeners();
9932
+ this.updateFormValue();
9240
9933
  }
9241
9934
  getSizeNum() {
9242
9935
  const s = this.size;
@@ -9268,6 +9961,7 @@ const _FxKnob = class _FxKnob extends FxElement {
9268
9961
  const clamped = this.clampValue(newValue);
9269
9962
  if (clamped !== this.value) {
9270
9963
  this.value = clamped;
9964
+ this.emitValueEvents();
9271
9965
  this.dispatchEvent(
9272
9966
  new CustomEvent("change", {
9273
9967
  bubbles: true,
@@ -11097,6 +11791,7 @@ function defineFxAccordionPanel() {
11097
11791
  defineFxAccordion();
11098
11792
  defineFxAccordionPanel();
11099
11793
  export {
11794
+ DEFAULT_PRESET,
11100
11795
  FX_JSX_TYPES,
11101
11796
  FenixToast,
11102
11797
  FenixUI,
@@ -11123,6 +11818,7 @@ export {
11123
11818
  FxEmptyState,
11124
11819
  FxFileUpload,
11125
11820
  FxFloatlabel,
11821
+ FxFormElement,
11126
11822
  FxIcon,
11127
11823
  FxInput,
11128
11824
  FxKnob,