fokus-styles 2.0.0 → 2.4.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 (137) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -3
  3. package/dist/css/components.css +532 -2
  4. package/dist/css/components.css.map +1 -1
  5. package/dist/css/components.min.css +1 -1
  6. package/dist/css/components.min.css.map +1 -1
  7. package/dist/css/fokus-components.css +4072 -0
  8. package/dist/css/fokus-components.css.map +1 -0
  9. package/dist/css/fokus-components.min.css +2 -0
  10. package/dist/css/fokus-components.min.css.map +1 -0
  11. package/dist/css/fokus-core.css +1585 -0
  12. package/dist/css/fokus-core.css.map +1 -0
  13. package/dist/css/fokus-core.min.css +2 -0
  14. package/dist/css/fokus-core.min.css.map +1 -0
  15. package/dist/css/fokus-dark.css +314 -0
  16. package/dist/css/fokus-dark.css.map +1 -0
  17. package/dist/css/fokus-dark.min.css +2 -0
  18. package/dist/css/fokus-dark.min.css.map +1 -0
  19. package/dist/css/fokus-rtl.css +8552 -0
  20. package/dist/css/fokus-rtl.css.map +1 -0
  21. package/dist/css/fokus-rtl.min.css +2 -0
  22. package/dist/css/fokus-rtl.min.css.map +1 -0
  23. package/dist/css/fokus-utilities.css +4198 -0
  24. package/dist/css/fokus-utilities.css.map +1 -0
  25. package/dist/css/fokus-utilities.min.css +2 -0
  26. package/dist/css/fokus-utilities.min.css.map +1 -0
  27. package/dist/css/fokus.css +1361 -53
  28. package/dist/css/fokus.css.map +1 -1
  29. package/dist/css/fokus.min.css +1 -1
  30. package/dist/css/fokus.min.css.map +1 -1
  31. package/dist/css/forms.css +137 -2
  32. package/dist/css/forms.css.map +1 -1
  33. package/dist/css/forms.min.css +1 -1
  34. package/dist/css/forms.min.css.map +1 -1
  35. package/dist/css/helpers.css +398 -2
  36. package/dist/css/helpers.css.map +1 -1
  37. package/dist/css/helpers.min.css +1 -1
  38. package/dist/css/helpers.min.css.map +1 -1
  39. package/dist/css/layout.css +582 -2
  40. package/dist/css/layout.css.map +1 -1
  41. package/dist/css/layout.min.css +1 -1
  42. package/dist/css/layout.min.css.map +1 -1
  43. package/dist/js/fokus.js +347 -12
  44. package/dist/js/fokus.js.map +4 -4
  45. package/dist/js/fokus.min.js +17 -17
  46. package/dist/js/fokus.min.js.map +4 -4
  47. package/docs/components/accordion.md +3 -0
  48. package/docs/components/avatar.md +13 -0
  49. package/docs/components/button-group.md +13 -0
  50. package/docs/components/close-button.md +13 -0
  51. package/docs/components/code.md +13 -0
  52. package/docs/components/icon-link.md +13 -0
  53. package/docs/components/list-group.md +13 -0
  54. package/docs/components/modal.md +3 -0
  55. package/docs/components/navbar.md +3 -0
  56. package/docs/components/pagination.md +3 -0
  57. package/docs/components/placeholder.md +13 -0
  58. package/docs/components/ratio.md +13 -0
  59. package/docs/components/scrollspy.md +13 -0
  60. package/docs/components/skeleton.md +3 -0
  61. package/docs/components/table.md +3 -0
  62. package/docs/guides/javascript-api.md +9 -0
  63. package/docs/guides/rtl-and-system-preferences.md +28 -0
  64. package/docs/guides/utility-api.md +9 -0
  65. package/docs/reference/size-baseline.json +13 -13
  66. package/mockup/assets/example-theme.css +3 -1
  67. package/mockup/assets/example-theme.js +2 -0
  68. package/mockup/assets/showcase-contracts.js +2 -2
  69. package/mockup/assets/showcase.css +4 -2
  70. package/mockup/assets/showcase.js +2 -0
  71. package/mockup/examples/js-foundation.html +25 -0
  72. package/mockup/examples/tables-navbar.html +12 -8
  73. package/mockup/examples/theming.html +2 -1
  74. package/mockup/foundations.html +2 -2
  75. package/package.json +15 -1
  76. package/packages/fokus-cli/bin/fokus.mjs +18 -1
  77. package/packages/fokus-cli/lib/build.mjs +12 -2
  78. package/packages/fokus-components/scss/components/_accordion.scss +19 -0
  79. package/packages/fokus-components/scss/components/_avatar.scss +22 -0
  80. package/packages/fokus-components/scss/components/_button-group.scss +14 -0
  81. package/packages/fokus-components/scss/components/_close-button.scss +22 -0
  82. package/packages/fokus-components/scss/components/_code.scss +19 -0
  83. package/packages/fokus-components/scss/components/_icon-link.scss +8 -0
  84. package/packages/fokus-components/scss/components/_index.scss +10 -0
  85. package/packages/fokus-components/scss/components/_list-group.scss +26 -0
  86. package/packages/fokus-components/scss/components/_modal.scss +36 -0
  87. package/packages/fokus-components/scss/components/_navbar.scss +48 -0
  88. package/packages/fokus-components/scss/components/_offcanvas.scss +19 -0
  89. package/packages/fokus-components/scss/components/_pagination.scss +10 -0
  90. package/packages/fokus-components/scss/components/_placeholder.scss +13 -0
  91. package/packages/fokus-components/scss/components/_progress.scss +8 -0
  92. package/packages/fokus-components/scss/components/_ratio.scss +10 -0
  93. package/packages/fokus-components/scss/components/_scrollspy.scss +5 -0
  94. package/packages/fokus-components/scss/components/_skeleton.scss +19 -0
  95. package/packages/fokus-components/scss/components/_spinner.scss +9 -53
  96. package/packages/fokus-components/scss/components/_tables.scss +20 -0
  97. package/packages/fokus-components/scss/forms/_forms.scss +29 -0
  98. package/packages/fokus-core/scss/base/_accessibility.scss +59 -0
  99. package/packages/fokus-core/scss/base/_index.scss +1 -0
  100. package/packages/fokus-core/scss/layout/_grid.scss +18 -0
  101. package/packages/fokus-core/scss/settings/_config.scss +43 -0
  102. package/packages/fokus-core/scss/settings/_index.scss +1 -0
  103. package/packages/fokus-core/scss/themes/_dark.scss +38 -2
  104. package/packages/fokus-core/scss/tokens/_root.scss +25 -0
  105. package/packages/fokus-core/scss/tools/_mixins.scss +8 -0
  106. package/packages/fokus-js/js/accordion.js +7 -1
  107. package/packages/fokus-js/js/core/focus.js +20 -0
  108. package/packages/fokus-js/js/core/overlay.d.ts +2 -0
  109. package/packages/fokus-js/js/core/overlay.js +10 -0
  110. package/packages/fokus-js/js/core/register.js +4 -2
  111. package/packages/fokus-js/js/focus-trap.d.ts +3 -0
  112. package/packages/fokus-js/js/focus-trap.js +1 -0
  113. package/packages/fokus-js/js/fokus.d.ts +7 -0
  114. package/packages/fokus-js/js/fokus.js +7 -0
  115. package/packages/fokus-js/js/form-validation.d.ts +7 -0
  116. package/packages/fokus-js/js/form-validation.js +20 -0
  117. package/packages/fokus-js/js/modal.js +16 -2
  118. package/packages/fokus-js/js/navbar.d.ts +10 -0
  119. package/packages/fokus-js/js/navbar.js +27 -0
  120. package/packages/fokus-js/js/offcanvas.d.ts +4 -0
  121. package/packages/fokus-js/js/offcanvas.js +16 -2
  122. package/packages/fokus-js/js/pagination.d.ts +7 -0
  123. package/packages/fokus-js/js/pagination.js +38 -0
  124. package/packages/fokus-js/js/scrollspy.d.ts +7 -0
  125. package/packages/fokus-js/js/scrollspy.js +26 -0
  126. package/packages/fokus-js/js/spinner.d.ts +9 -0
  127. package/packages/fokus-js/js/spinner.js +18 -0
  128. package/packages/fokus-js/js/theme.d.ts +9 -0
  129. package/packages/fokus-js/js/theme.js +19 -0
  130. package/packages/fokus-js/js/toast.js +4 -0
  131. package/packages/fokus-utilities/scss/utilities/_api.scss +48 -0
  132. package/packages/fokus-utilities/scss/utilities/_index.scss +2 -0
  133. package/packages/fokus-utilities/scss/utilities/_states.scss +19 -0
  134. package/packages/fokus-utilities/scss/utilities/_visibility.scss +24 -0
  135. package/scss/entries/core-entry.scss +5 -0
  136. package/scss/entries/dark-entry.scss +3 -0
  137. package/scss/entries/rtl-entry.scss +11 -0
@@ -34,7 +34,8 @@
34
34
  $dark-alert-bg-weight: 15%;
35
35
  $dark-alert-text-weight: 55%;
36
36
 
37
- [data-theme="dark"] {
37
+ [data-theme="dark"],
38
+ [data-fs-theme="dark"] {
38
39
  --fs-color-text: #{rgb-fallback($dark-text)};
39
40
  --fs-color-muted: #{rgb-fallback($dark-muted)};
40
41
  --fs-color-border: #{rgb-fallback($dark-border)};
@@ -65,7 +66,8 @@
65
66
  }
66
67
 
67
68
  @supports (color: oklch(0% 0 0)) {
68
- [data-theme="dark"] {
69
+ [data-theme="dark"],
70
+ [data-fs-theme="dark"] {
69
71
  --fs-color-text: #{$dark-text};
70
72
  --fs-color-muted: #{$dark-muted};
71
73
  --fs-color-border: #{$dark-border};
@@ -92,4 +94,38 @@
92
94
  }
93
95
  }
94
96
  }
97
+
98
+ @media (prefers-color-scheme: dark) {
99
+ [data-fs-theme="auto"] {
100
+ --fs-color-text: #{rgb-fallback($dark-text)};
101
+ --fs-color-muted: #{rgb-fallback($dark-muted)};
102
+ --fs-color-border: #{rgb-fallback($dark-border)};
103
+ --fs-color-surface: #{rgb-fallback($dark-surface)};
104
+ --fs-color-subtle: #{rgb-fallback($dark-subtle)};
105
+ --fs-color-bg-surface: var(--fs-color-surface);
106
+ --fs-color-bg-subtle: var(--fs-color-subtle);
107
+ --fs-color-surface-raised: var(--fs-color-surface);
108
+ --fs-color-surface-interactive: var(--fs-color-subtle);
109
+ --fs-color-text-primary: var(--fs-color-text);
110
+ --fs-color-text-muted: var(--fs-color-muted);
111
+ --fs-color-border-default: var(--fs-color-border);
112
+ --fs-color-focus: var(--fs-color-primary);
113
+ --fs-tooltip-bg: #{rgb-fallback($color-gray-100)};
114
+ --fs-tooltip-text: #{rgb-fallback($color-gray-900)};
115
+ }
116
+ }
117
+
118
+ @media (forced-colors: active) {
119
+ :root,
120
+ [data-theme="dark"],
121
+ [data-fs-theme="light"],
122
+ [data-fs-theme="dark"],
123
+ [data-fs-theme="auto"] {
124
+ --fs-color-border: CanvasText;
125
+ --fs-color-text: CanvasText;
126
+ --fs-color-surface: Canvas;
127
+ --fs-color-subtle: Canvas;
128
+ --fs-focus-color: Highlight;
129
+ }
130
+ }
95
131
  }
@@ -55,12 +55,28 @@
55
55
  --fs-control-padding-x-lg: 16px;
56
56
  --fs-focus-width: 2px;
57
57
  --fs-focus-offset: 2px;
58
+ --fs-focus-color: var(--fs-color-primary);
59
+ --fs-focus-ring-size: 3px;
60
+ --fs-focus-ring-color: color-mix(in srgb, var(--fs-focus-color) 22%, transparent);
58
61
  --fs-focus-ring-alpha: 22%;
59
62
  --fs-transition-fast: 150ms;
60
63
  --fs-transition-normal: 220ms;
64
+ --fs-motion-duration-fast: var(--fs-transition-fast);
65
+ --fs-motion-duration-normal: var(--fs-transition-normal);
66
+ --fs-motion-ease-standard: var(--fs-ease-standard);
61
67
  --fs-ease-standard: cubic-bezier(0.2, 0, 0, 1);
62
68
  --fs-border-width: 1px;
63
69
  --fs-disabled-opacity: 0.5;
70
+ --fs-safe-area-top: env(safe-area-inset-top, 0px);
71
+ --fs-safe-area-right: env(safe-area-inset-right, 0px);
72
+ --fs-safe-area-bottom: env(safe-area-inset-bottom, 0px);
73
+ --fs-safe-area-left: env(safe-area-inset-left, 0px);
74
+ --fs-component-control-height: var(--fs-control-height);
75
+ --fs-component-overlay-backdrop: rgb(0 0 0 / 55%);
76
+ --fs-component-panel-radius: var(--fs-radius-md);
77
+ --fs-component-panel-shadow: var(--fs-shadow-lg);
78
+ --fs-contrast-border-width: var(--fs-border-width);
79
+ --fs-contrast-focus-width: var(--fs-focus-width);
64
80
 
65
81
  // Referência dos limiares usados pelos utilitários `@container` (ver
66
82
  // `packages/fokus-utilities/scss/utilities/_container-queries.scss`).
@@ -114,4 +130,13 @@
114
130
  }
115
131
  }
116
132
  }
133
+
134
+ @media (prefers-contrast: more) {
135
+ :root {
136
+ --fs-contrast-border-width: 2px;
137
+ --fs-contrast-focus-width: 3px;
138
+ --fs-focus-width: var(--fs-contrast-focus-width);
139
+ --fs-border-width: var(--fs-contrast-border-width);
140
+ }
141
+ }
117
142
  }
@@ -3,6 +3,14 @@
3
3
  @use "sass:color";
4
4
  @use "../settings" as *;
5
5
 
6
+ @mixin fs-fluid-type($min, $max, $min-vw: 320px, $max-vw: 1280px) {
7
+ font-size: clamp($min, calc(#{$min} + (#{ $max } - #{ $min }) * ((100vw - #{ $min-vw }) / (#{ $max-vw } - #{ $min-vw }))), $max);
8
+ }
9
+
10
+ @mixin fs-fluid-value($property, $min, $max, $min-vw: 320px, $max-vw: 1280px) {
11
+ #{$property}: clamp($min, calc(#{$min} + (#{ $max } - #{ $min }) * ((100vw - #{ $min-vw }) / (#{ $max-vw } - #{ $min-vw }))), $max);
12
+ }
13
+
6
14
  @mixin media-breakpoint-up($name) {
7
15
  $min: map.get($breakpoints, $name);
8
16
 
@@ -7,7 +7,8 @@ let idCounter = 0;
7
7
  export class Accordion {
8
8
  constructor(accordionEl) {
9
9
  this.accordionEl = accordionEl;
10
- this.exclusive = accordionEl.getAttribute("data-multiple") !== "true";
10
+ this.exclusive = accordionEl.getAttribute("data-multiple") !== "true" && accordionEl.getAttribute("data-fs-always-open") !== "true";
11
+ this.alwaysOpen = accordionEl.getAttribute("data-fs-always-open") === "true";
11
12
  this.items = [];
12
13
 
13
14
  const buttons = Array.from(accordionEl.querySelectorAll(".fs-accordion-button"));
@@ -46,6 +47,7 @@ export class Accordion {
46
47
  const isOpen = entry.button.getAttribute("aria-expanded") === "true";
47
48
 
48
49
  if (isOpen) {
50
+ if (this.alwaysOpen) return;
49
51
  this._close(entry);
50
52
  return;
51
53
  }
@@ -60,17 +62,21 @@ export class Accordion {
60
62
  }
61
63
 
62
64
  _open(entry) {
65
+ entry.button.dispatchEvent(new CustomEvent("fs:show", { bubbles: true }));
63
66
  entry.button.setAttribute("aria-expanded", "true");
64
67
  entry.panel.hidden = false;
65
68
  expand(entry.panel).then(() => {
69
+ entry.button.dispatchEvent(new CustomEvent("fs:shown", { bubbles: true }));
66
70
  entry.button.dispatchEvent(new CustomEvent("fs:accordion:shown", { bubbles: true }));
67
71
  });
68
72
  }
69
73
 
70
74
  _close(entry) {
75
+ entry.button.dispatchEvent(new CustomEvent("fs:hide", { bubbles: true }));
71
76
  entry.button.setAttribute("aria-expanded", "false");
72
77
  collapse(entry.panel).then(() => {
73
78
  entry.panel.hidden = true;
79
+ entry.button.dispatchEvent(new CustomEvent("fs:hidden", { bubbles: true }));
74
80
  entry.button.dispatchEvent(new CustomEvent("fs:accordion:hidden", { bubbles: true }));
75
81
  });
76
82
  }
@@ -6,6 +6,23 @@ export function getFocusableElements(container) {
6
6
  }
7
7
 
8
8
  export function createFocusTrap(container) {
9
+ const inertSiblings = [];
10
+ const setSiblingsInert = (value) => {
11
+ if (!container.parentElement) return;
12
+ [...container.parentElement.children].filter((el) => el !== container).forEach((el) => {
13
+ if (value) {
14
+ inertSiblings.push({ el, value: el.inert, ariaHidden: el.getAttribute("aria-hidden") });
15
+ el.inert = true;
16
+ el.setAttribute("aria-hidden", "true");
17
+ } else {
18
+ const previous = inertSiblings.find((item) => item.el === el);
19
+ if (previous) {
20
+ el.inert = previous.value;
21
+ previous.ariaHidden === null ? el.removeAttribute("aria-hidden") : el.setAttribute("aria-hidden", previous.ariaHidden);
22
+ }
23
+ }
24
+ });
25
+ };
9
26
  function handleKeydown(event) {
10
27
  if (event.key !== "Tab") return;
11
28
 
@@ -32,11 +49,14 @@ export function createFocusTrap(container) {
32
49
 
33
50
  return {
34
51
  activate() {
52
+ setSiblingsInert(true);
35
53
  const focusable = getFocusableElements(container);
36
54
  if (focusable.length > 0) focusable[0].focus();
37
55
  },
38
56
  deactivate() {
39
57
  container.removeEventListener("keydown", handleKeydown);
58
+ setSiblingsInert(false);
59
+ inertSiblings.length = 0;
40
60
  },
41
61
  };
42
62
  }
@@ -1,3 +1,5 @@
1
1
  export function lockScroll(): void;
2
2
  export function unlockScroll(): void;
3
3
  export function onClickOutside(el: Element, callback: (event: MouseEvent) => void): () => void;
4
+ export function registerOverlay(instance: { hide?: () => void }): void;
5
+ export function unregisterOverlay(instance: { hide?: () => void }): void;
@@ -2,6 +2,16 @@ let lockCount = 0;
2
2
  let paddingCompensated = false;
3
3
  let previousOverflow = "";
4
4
  let previousPaddingRight = "";
5
+ const activeOverlays = [];
6
+
7
+ export function registerOverlay(instance) {
8
+ if (!activeOverlays.includes(instance)) activeOverlays.push(instance);
9
+ }
10
+
11
+ export function unregisterOverlay(instance) {
12
+ const index = activeOverlays.lastIndexOf(instance);
13
+ if (index >= 0) activeOverlays.splice(index, 1);
14
+ }
5
15
 
6
16
  export function lockScroll() {
7
17
  lockCount += 1;
@@ -14,11 +14,13 @@ export function createInstanceRegistry() {
14
14
  };
15
15
  }
16
16
 
17
- export function autoInit(name, Ctor) {
17
+ export function autoInit(name, Ctor, options = {}) {
18
18
  if (typeof document === "undefined") return;
19
19
 
20
+ const selector = options.selector ?? `[data-fs="${name}"]`;
21
+
20
22
  const init = () => {
21
- document.querySelectorAll(`[data-fs="${name}"]`).forEach((el) => {
23
+ document.querySelectorAll(selector).forEach((el) => {
22
24
  if (!Ctor.getInstance(el)) new Ctor(el);
23
25
  });
24
26
  };
@@ -0,0 +1,3 @@
1
+ export declare function getFocusableElements(container: Element): Element[];
2
+ export declare function createFocusTrap(container: Element): { activate(): void; deactivate(): void };
3
+ export declare function onEscapeKey(callback: (event: KeyboardEvent) => void): () => void;
@@ -0,0 +1 @@
1
+ export { createFocusTrap, getFocusableElements, onEscapeKey } from "./core/focus.js";
@@ -24,3 +24,10 @@ export { CommandPalette } from "./command-palette.js";
24
24
  export { TreeView } from "./tree-view.js";
25
25
  export { RangeSlider } from "./range.js";
26
26
  export { FileUploadAdvanced } from "./file-upload-advanced.js";
27
+ export { Theme } from "./theme.js";
28
+ export { Navbar } from "./navbar.js";
29
+ export { Scrollspy } from "./scrollspy.js";
30
+ export { FormValidation } from "./form-validation.js";
31
+ export { createFocusTrap, getFocusableElements, onEscapeKey } from "./focus-trap.js";
32
+ export { Spinner } from "./spinner.js";
33
+ export { Pagination } from "./pagination.js";
@@ -24,3 +24,10 @@ export { CommandPalette } from "./command-palette.js";
24
24
  export { TreeView } from "./tree-view.js";
25
25
  export { RangeSlider } from "./range.js";
26
26
  export { FileUploadAdvanced } from "./file-upload-advanced.js";
27
+ export { Theme } from "./theme.js";
28
+ export { Navbar } from "./navbar.js";
29
+ export { Scrollspy } from "./scrollspy.js";
30
+ export { FormValidation } from "./form-validation.js";
31
+ export { Spinner } from "./spinner.js";
32
+ export { Pagination } from "./pagination.js";
33
+ export { createFocusTrap, getFocusableElements, onEscapeKey } from "./focus-trap.js";
@@ -0,0 +1,7 @@
1
+ export declare class FormValidation {
2
+ constructor(form: HTMLFormElement);
3
+ static getInstance(form: HTMLFormElement): FormValidation | undefined;
4
+ static getOrCreateInstance(form: HTMLFormElement): FormValidation;
5
+ validate(event: SubmitEvent): boolean;
6
+ dispose(): void;
7
+ }
@@ -0,0 +1,20 @@
1
+ import { autoInit, createInstanceRegistry } from "./core/register.js";
2
+
3
+ const instances = createInstanceRegistry();
4
+
5
+ export class FormValidation {
6
+ constructor(form) { this.form = form; this._submit = (event) => this.validate(event); form.addEventListener("submit", this._submit); instances.set(form, this); }
7
+ static getInstance(el) { return instances.get(el); }
8
+ static getOrCreateInstance(el) { return this.getInstance(el) ?? new FormValidation(el); }
9
+ validate(event) {
10
+ const valid = this.form.checkValidity();
11
+ this.form.classList.add("fs-was-validated");
12
+ this.form.querySelectorAll('[required], [aria-required="true"]').forEach((field) => field.setAttribute("aria-invalid", String(!field.checkValidity())));
13
+ if (!valid) { event.preventDefault(); event.stopPropagation(); }
14
+ this.form.dispatchEvent(new CustomEvent("fs:form:validated", { bubbles: true, detail: { valid } }));
15
+ return valid;
16
+ }
17
+ dispose() { this.form.removeEventListener("submit", this._submit); instances.delete(this.form); }
18
+ }
19
+
20
+ autoInit("form-validation", FormValidation);
@@ -1,4 +1,4 @@
1
- import { lockScroll, unlockScroll, onClickOutside } from "./core/overlay.js";
1
+ import { lockScroll, unlockScroll, onClickOutside, registerOverlay, unregisterOverlay } from "./core/overlay.js";
2
2
  import { createFocusTrap, onEscapeKey } from "./core/focus.js";
3
3
  import { autoInit, createInstanceRegistry } from "./core/register.js";
4
4
 
@@ -47,6 +47,14 @@ export class Modal {
47
47
  return instances.get(el);
48
48
  }
49
49
 
50
+ static getOrCreateInstance(el, options = {}) {
51
+ return this.getInstance(el) ?? new Modal(el, options);
52
+ }
53
+
54
+ static show(el, options = {}) { return this.getOrCreateInstance(el, options).show(); }
55
+ static hide(el) { return this.getInstance(el)?.hide(); }
56
+ static toggle(el, options = {}) { return this.getOrCreateInstance(el, options).toggle(); }
57
+
50
58
  _handleTriggerClick(event) {
51
59
  event.preventDefault();
52
60
  this.show();
@@ -60,7 +68,9 @@ export class Modal {
60
68
 
61
69
  show() {
62
70
  if (this.isOpen) return;
71
+ this.triggerEl.dispatchEvent(new CustomEvent("fs:show", { bubbles: true }));
63
72
  this.isOpen = true;
73
+ registerOverlay(this);
64
74
 
65
75
  this.modalEl.classList.add("is-open");
66
76
  this.modalEl.setAttribute("aria-hidden", "false");
@@ -73,12 +83,15 @@ export class Modal {
73
83
  this._outsideClickCleanup = onClickOutside(this.dialogEl, () => this.hide());
74
84
  }
75
85
 
86
+ this.triggerEl.dispatchEvent(new CustomEvent("fs:shown", { bubbles: true }));
76
87
  this.triggerEl.dispatchEvent(new CustomEvent("fs:modal:shown", { bubbles: true }));
77
88
  }
78
89
 
79
90
  hide() {
80
91
  if (!this.isOpen) return;
92
+ this.triggerEl.dispatchEvent(new CustomEvent("fs:hide", { bubbles: true }));
81
93
  this.isOpen = false;
94
+ unregisterOverlay(this);
82
95
 
83
96
  this.modalEl.classList.remove("is-open");
84
97
  this.modalEl.setAttribute("aria-hidden", "true");
@@ -92,6 +105,7 @@ export class Modal {
92
105
 
93
106
  this.triggerEl.focus();
94
107
 
108
+ this.triggerEl.dispatchEvent(new CustomEvent("fs:hidden", { bubbles: true }));
95
109
  this.triggerEl.dispatchEvent(new CustomEvent("fs:modal:hidden", { bubbles: true }));
96
110
  }
97
111
 
@@ -112,4 +126,4 @@ export class Modal {
112
126
  }
113
127
  }
114
128
 
115
- autoInit("modal", Modal);
129
+ autoInit("modal", Modal, { selector: '[data-fs="modal"], [data-fs-toggle="modal"]' });
@@ -0,0 +1,10 @@
1
+ export declare class Navbar {
2
+ constructor(element: Element);
3
+ static getInstance(element: Element): Navbar | undefined;
4
+ static getOrCreateInstance(element: Element): Navbar;
5
+ isOpen(): boolean;
6
+ show(): void;
7
+ hide(): void;
8
+ toggle(): void;
9
+ dispose(): void;
10
+ }
@@ -0,0 +1,27 @@
1
+ import { autoInit, createInstanceRegistry } from "./core/register.js";
2
+
3
+ const instances = createInstanceRegistry();
4
+
5
+ export class Navbar {
6
+ constructor(navbarEl) {
7
+ this.navbarEl = navbarEl;
8
+ this.toggleEl = navbarEl.querySelector('[data-fs-toggle="navbar"]');
9
+ const target = this.toggleEl?.getAttribute("data-fs-target");
10
+ this.menuEl = target ? document.querySelector(target) : navbarEl.querySelector(".fs-navbar-collapse");
11
+ if (!this.toggleEl || !this.menuEl) return;
12
+ this._toggle = () => this.toggle();
13
+ this._escape = (event) => { if (event.key === "Escape" && this.isOpen()) this.hide(); };
14
+ this.toggleEl.addEventListener("click", this._toggle);
15
+ this.menuEl.addEventListener("keydown", this._escape);
16
+ instances.set(navbarEl, this);
17
+ }
18
+ static getInstance(el) { return instances.get(el); }
19
+ static getOrCreateInstance(el) { return this.getInstance(el) ?? new Navbar(el); }
20
+ isOpen() { return this.menuEl?.classList.contains("is-open"); }
21
+ show() { this.menuEl.classList.add("is-open"); this.menuEl.hidden = false; this.toggleEl.setAttribute("aria-expanded", "true"); this.navbarEl.dispatchEvent(new CustomEvent("fs:shown", { bubbles: true })); }
22
+ hide() { this.menuEl.classList.remove("is-open"); this.menuEl.hidden = true; this.toggleEl.setAttribute("aria-expanded", "false"); this.toggleEl.focus(); this.navbarEl.dispatchEvent(new CustomEvent("fs:hidden", { bubbles: true })); }
23
+ toggle() { this.isOpen() ? this.hide() : this.show(); }
24
+ dispose() { this.toggleEl?.removeEventListener("click", this._toggle); this.menuEl?.removeEventListener("keydown", this._escape); instances.delete(this.navbarEl); }
25
+ }
26
+
27
+ autoInit("navbar", Navbar);
@@ -12,6 +12,10 @@ export class Offcanvas {
12
12
  constructor(triggerEl: HTMLElement, options?: OffcanvasOptions);
13
13
 
14
14
  static getInstance(el: Element): Offcanvas | undefined;
15
+ static getOrCreateInstance(el: HTMLElement, options?: OffcanvasOptions): Offcanvas;
16
+ static show(el: HTMLElement, options?: OffcanvasOptions): void;
17
+ static hide(el: HTMLElement): void;
18
+ static toggle(el: HTMLElement, options?: OffcanvasOptions): void;
15
19
 
16
20
  show(): void;
17
21
  hide(): void;
@@ -1,4 +1,4 @@
1
- import { lockScroll, unlockScroll, onClickOutside } from "./core/overlay.js";
1
+ import { lockScroll, unlockScroll, onClickOutside, registerOverlay, unregisterOverlay } from "./core/overlay.js";
2
2
  import { createFocusTrap, onEscapeKey } from "./core/focus.js";
3
3
  import { autoInit, createInstanceRegistry } from "./core/register.js";
4
4
 
@@ -48,6 +48,14 @@ export class Offcanvas {
48
48
  return instances.get(el);
49
49
  }
50
50
 
51
+ static getOrCreateInstance(el, options = {}) {
52
+ return this.getInstance(el) ?? new Offcanvas(el, options);
53
+ }
54
+
55
+ static show(el, options = {}) { return this.getOrCreateInstance(el, options).show(); }
56
+ static hide(el) { return this.getInstance(el)?.hide(); }
57
+ static toggle(el, options = {}) { return this.getOrCreateInstance(el, options).toggle(); }
58
+
51
59
  _handleTriggerClick(event) {
52
60
  event.preventDefault();
53
61
  this.show();
@@ -61,7 +69,9 @@ export class Offcanvas {
61
69
 
62
70
  show() {
63
71
  if (this.isOpen) return;
72
+ this.triggerEl.dispatchEvent(new CustomEvent("fs:show", { bubbles: true }));
64
73
  this.isOpen = true;
74
+ registerOverlay(this);
65
75
 
66
76
  if (this.hasBackdrop) {
67
77
  this._backdropEl = document.createElement("div");
@@ -96,12 +106,15 @@ export class Offcanvas {
96
106
  });
97
107
  }
98
108
 
109
+ this.triggerEl.dispatchEvent(new CustomEvent("fs:shown", { bubbles: true }));
99
110
  this.triggerEl.dispatchEvent(new CustomEvent("fs:offcanvas:shown", { bubbles: true }));
100
111
  }
101
112
 
102
113
  hide() {
103
114
  if (!this.isOpen) return;
115
+ this.triggerEl.dispatchEvent(new CustomEvent("fs:hide", { bubbles: true }));
104
116
  this.isOpen = false;
117
+ unregisterOverlay(this);
105
118
 
106
119
  this.offcanvasEl.classList.remove("is-open");
107
120
  this.offcanvasEl.setAttribute("aria-hidden", "true");
@@ -118,6 +131,7 @@ export class Offcanvas {
118
131
 
119
132
  this.triggerEl.focus();
120
133
 
134
+ this.triggerEl.dispatchEvent(new CustomEvent("fs:hidden", { bubbles: true }));
121
135
  this.triggerEl.dispatchEvent(new CustomEvent("fs:offcanvas:hidden", { bubbles: true }));
122
136
  }
123
137
 
@@ -138,4 +152,4 @@ export class Offcanvas {
138
152
  }
139
153
  }
140
154
 
141
- autoInit("offcanvas", Offcanvas);
155
+ autoInit("offcanvas", Offcanvas, { selector: '[data-fs="offcanvas"], [data-fs-toggle="offcanvas"]' });
@@ -0,0 +1,7 @@
1
+ export declare class Pagination {
2
+ constructor(element: HTMLElement);
3
+ static getInstance(element: HTMLElement): Pagination | undefined;
4
+ static getOrCreateInstance(element: HTMLElement): Pagination;
5
+ goTo(page: number): void;
6
+ dispose(): void;
7
+ }
@@ -0,0 +1,38 @@
1
+ import { autoInit, createInstanceRegistry } from "./core/register.js";
2
+
3
+ const instances = createInstanceRegistry();
4
+ export class Pagination {
5
+ constructor(element) {
6
+ this.element = element;
7
+ this.current = Number(element.getAttribute("data-fs-page")) || 1;
8
+ this.total = Number(element.getAttribute("data-fs-pages")) || element.querySelectorAll("[data-fs-page]").length || 1;
9
+ this._click = (event) => {
10
+ const button = event.target.closest("[data-fs-page]");
11
+ if (!button || !this.element.contains(button)) return;
12
+ event.preventDefault();
13
+ this.goTo(Number(button.getAttribute("data-fs-page")));
14
+ };
15
+ element.addEventListener("click", this._click);
16
+ instances.set(element, this);
17
+ this._render();
18
+ }
19
+ static getInstance(el) { return instances.get(el); }
20
+ static getOrCreateInstance(el) { return this.getInstance(el) ?? new Pagination(el); }
21
+ goTo(page) {
22
+ const next = Math.max(1, Math.min(this.total, page));
23
+ if (next === this.current) return;
24
+ this.current = next;
25
+ this.element.setAttribute("data-fs-page", String(next));
26
+ this._render();
27
+ this.element.dispatchEvent(new CustomEvent("fs:pagination:change", { bubbles: true, detail: { page: next, total: this.total } }));
28
+ }
29
+ _render() {
30
+ this.element.querySelectorAll("[data-fs-page]").forEach((item) => {
31
+ const active = Number(item.getAttribute("data-fs-page")) === this.current;
32
+ item.classList.toggle("is-active", active);
33
+ item.setAttribute("aria-current", active ? "page" : "false");
34
+ });
35
+ }
36
+ dispose() { this.element.removeEventListener("click", this._click); instances.delete(this.element); }
37
+ }
38
+ autoInit("pagination", Pagination);
@@ -0,0 +1,7 @@
1
+ export declare class Scrollspy {
2
+ constructor(element: Element);
3
+ static getInstance(element: Element): Scrollspy | undefined;
4
+ static getOrCreateInstance(element: Element): Scrollspy;
5
+ activate(section: Element): void;
6
+ dispose(): void;
7
+ }
@@ -0,0 +1,26 @@
1
+ import { autoInit, createInstanceRegistry } from "./core/register.js";
2
+
3
+ const instances = createInstanceRegistry();
4
+
5
+ export class Scrollspy {
6
+ constructor(navEl) {
7
+ this.navEl = navEl;
8
+ this.links = [...navEl.querySelectorAll('a[href^="#"]')];
9
+ this.sections = this.links.map((link) => document.querySelector(link.getAttribute("href"))).filter(Boolean);
10
+ this.observer = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => entries.forEach((entry) => entry.isIntersecting && this.activate(entry.target)), { rootMargin: "-20% 0px -65%" });
11
+ this.sections.forEach((section) => this.observer?.observe(section));
12
+ this._onScroll = () => {
13
+ if (this.observer) return;
14
+ const current = this.sections.filter((section) => section.getBoundingClientRect().top <= window.innerHeight * 0.35).at(-1);
15
+ if (current) this.activate(current);
16
+ };
17
+ if (!this.observer) window.addEventListener("scroll", this._onScroll, { passive: true });
18
+ instances.set(navEl, this);
19
+ }
20
+ static getInstance(el) { return instances.get(el); }
21
+ static getOrCreateInstance(el) { return this.getInstance(el) ?? new Scrollspy(el); }
22
+ activate(section) { this.links.forEach((link) => link.classList.toggle("is-active", link.getAttribute("href") === `#${section.id}`)); this.navEl.dispatchEvent(new CustomEvent("fs:scrollspy:activate", { bubbles: true, detail: { section } })); }
23
+ dispose() { this.observer?.disconnect(); window.removeEventListener("scroll", this._onScroll); instances.delete(this.navEl); }
24
+ }
25
+
26
+ autoInit("scrollspy", Scrollspy);
@@ -0,0 +1,9 @@
1
+ export declare class Spinner {
2
+ constructor(element: HTMLElement);
3
+ static getInstance(element: HTMLElement): Spinner | undefined;
4
+ static getOrCreateInstance(element: HTMLElement): Spinner;
5
+ start(): void;
6
+ stop(): void;
7
+ toggle(): void;
8
+ dispose(): void;
9
+ }
@@ -0,0 +1,18 @@
1
+ import { autoInit, createInstanceRegistry } from "./core/register.js";
2
+
3
+ const instances = createInstanceRegistry();
4
+ export class Spinner {
5
+ constructor(element) {
6
+ this.element = element;
7
+ this._setLoading(element.hasAttribute("data-fs-loading") && element.getAttribute("data-fs-loading") !== "false");
8
+ instances.set(element, this);
9
+ }
10
+ static getInstance(el) { return instances.get(el); }
11
+ static getOrCreateInstance(el) { return this.getInstance(el) ?? new Spinner(el); }
12
+ _setLoading(value) { this.loading = value; this.element.toggleAttribute("aria-busy", value); this.element.toggleAttribute("data-fs-loading", value); }
13
+ start() { this._setLoading(true); this.element.dispatchEvent(new CustomEvent("fs:loading:start", { bubbles: true })); }
14
+ stop() { this._setLoading(false); this.element.dispatchEvent(new CustomEvent("fs:loading:stop", { bubbles: true })); }
15
+ toggle() { this.loading ? this.stop() : this.start(); }
16
+ dispose() { instances.delete(this.element); }
17
+ }
18
+ autoInit("spinner", Spinner);
@@ -0,0 +1,9 @@
1
+ export declare class Theme {
2
+ constructor(root?: Element);
3
+ static getInstance(root?: Element): Theme | undefined;
4
+ static getOrCreateInstance(root?: Element): Theme;
5
+ static set(mode: "light" | "dark" | "auto", root?: Element): Theme;
6
+ get(): string;
7
+ set(mode: "light" | "dark" | "auto"): Theme;
8
+ dispose(): void;
9
+ }
@@ -0,0 +1,19 @@
1
+ import { createInstanceRegistry } from "./core/register.js";
2
+
3
+ const instances = createInstanceRegistry();
4
+
5
+ export class Theme {
6
+ constructor(root = document.documentElement) { this.root = root; instances.set(root, this); }
7
+ static getInstance(root = document.documentElement) { return instances.get(root); }
8
+ static getOrCreateInstance(root = document.documentElement) { return this.getInstance(root) ?? new Theme(root); }
9
+ static set(mode, root = document.documentElement) { return this.getOrCreateInstance(root).set(mode); }
10
+ get() { return this.root.getAttribute("data-fs-theme") ?? this.root.getAttribute("data-theme") ?? "light"; }
11
+ set(mode) {
12
+ if (!["light", "dark", "auto"].includes(mode)) throw new TypeError("FokusStyles.Theme: modo inválido.");
13
+ this.root.setAttribute("data-fs-theme", mode);
14
+ this.root.setAttribute("data-theme", mode === "dark" ? "dark" : "light");
15
+ this.root.dispatchEvent(new CustomEvent("fs:theme:change", { bubbles: true, detail: { mode } }));
16
+ return this;
17
+ }
18
+ dispose() { instances.delete(this.root); }
19
+ }
@@ -33,9 +33,11 @@ export class Toast {
33
33
 
34
34
  show() {
35
35
  if (this.isOpen) return;
36
+ this.toastEl.dispatchEvent(new CustomEvent("fs:show", { bubbles: true }));
36
37
  this.isOpen = true;
37
38
 
38
39
  expand(this.toastEl).then(() => {
40
+ this.toastEl.dispatchEvent(new CustomEvent("fs:shown", { bubbles: true }));
39
41
  this.toastEl.dispatchEvent(new CustomEvent("fs:toast:shown", { bubbles: true }));
40
42
  });
41
43
 
@@ -46,12 +48,14 @@ export class Toast {
46
48
 
47
49
  hide() {
48
50
  if (!this.isOpen) return;
51
+ this.toastEl.dispatchEvent(new CustomEvent("fs:hide", { bubbles: true }));
49
52
  this.isOpen = false;
50
53
 
51
54
  clearTimeout(this._hideTimer);
52
55
  this._hideTimer = null;
53
56
 
54
57
  collapse(this.toastEl).then(() => {
58
+ this.toastEl.dispatchEvent(new CustomEvent("fs:hidden", { bubbles: true }));
55
59
  this.toastEl.dispatchEvent(new CustomEvent("fs:toast:hidden", { bubbles: true }));
56
60
  });
57
61
  }