fokus-styles 2.6.2 → 2.7.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 (82) hide show
  1. package/README.md +5 -0
  2. package/dist/css/components.css +534 -27
  3. package/dist/css/components.min.css +1 -1
  4. package/dist/css/fokus-components.css +534 -27
  5. package/dist/css/fokus-components.min.css +1 -1
  6. package/dist/css/fokus-core.css +42 -0
  7. package/dist/css/fokus-core.min.css +1 -1
  8. package/dist/css/fokus-dark.css +42 -0
  9. package/dist/css/fokus-dark.min.css +1 -1
  10. package/dist/css/fokus-helpers.css +646 -0
  11. package/dist/css/fokus-helpers.min.css +2 -0
  12. package/dist/css/fokus-rtl.css +9667 -2716
  13. package/dist/css/fokus-rtl.min.css +1 -1
  14. package/dist/css/fokus-utilities.css +9175 -2682
  15. package/dist/css/fokus-utilities.min.css +1 -1
  16. package/dist/css/fokus.css +9893 -2799
  17. package/dist/css/fokus.min.css +1 -1
  18. package/dist/css/forms.css +206 -28
  19. package/dist/css/forms.min.css +1 -1
  20. package/dist/css/helpers.css +9175 -2682
  21. package/dist/css/helpers.min.css +1 -1
  22. package/dist/css/layout.css +42 -0
  23. package/dist/css/layout.min.css +1 -1
  24. package/dist/js/fokus.js +91 -69
  25. package/dist/js/fokus.min.js +19 -19
  26. package/package.json +2 -1
  27. package/packages/fokus-components/scss/components/_button-group.scss +12 -0
  28. package/packages/fokus-components/scss/components/_buttons.scss +39 -6
  29. package/packages/fokus-components/scss/components/_datepicker.scss +47 -1
  30. package/packages/fokus-components/scss/components/_dropdown.scss +212 -6
  31. package/packages/fokus-components/scss/components/_index.scss +1 -0
  32. package/packages/fokus-components/scss/components/_menu.scss +95 -0
  33. package/packages/fokus-components/scss/components/_pagination.scss +43 -16
  34. package/packages/fokus-components/scss/components/_segmented-control.scss +16 -1
  35. package/packages/fokus-components/scss/components/_tabs.scss +58 -0
  36. package/packages/fokus-components/scss/components/_tag.scss +32 -0
  37. package/packages/fokus-components/scss/forms/_forms.scss +155 -28
  38. package/packages/fokus-components/scss/forms/_range.scss +29 -0
  39. package/packages/fokus-core/scss/base/_motion.scss +3 -1
  40. package/packages/fokus-core/scss/settings/_config.scss +39 -1
  41. package/packages/fokus-core/scss/settings/_index.scss +1 -0
  42. package/packages/fokus-core/scss/settings/_utilities.scss +86 -0
  43. package/packages/fokus-core/scss/themes/_dark.scss +9 -0
  44. package/packages/fokus-core/scss/tokens/_root.scss +23 -0
  45. package/packages/fokus-js/js/combobox.js +2 -6
  46. package/packages/fokus-js/js/command-palette.js +2 -6
  47. package/packages/fokus-js/js/core/positioning.d.ts +2 -0
  48. package/packages/fokus-js/js/core/positioning.js +2 -2
  49. package/packages/fokus-js/js/core/theme.js +8 -0
  50. package/packages/fokus-js/js/datatable.js +6 -2
  51. package/packages/fokus-js/js/datepicker.js +13 -5
  52. package/packages/fokus-js/js/dropdown.js +37 -5
  53. package/packages/fokus-js/js/nested-menu.js +2 -6
  54. package/packages/fokus-js/js/notification-center.js +2 -6
  55. package/packages/fokus-js/js/pagination.js +15 -9
  56. package/packages/fokus-js/js/popover.js +2 -6
  57. package/packages/fokus-js/js/select.js +4 -0
  58. package/packages/fokus-js/js/tabs.js +4 -4
  59. package/packages/fokus-js/js/tooltip.js +2 -6
  60. package/packages/fokus-utilities/scss/utilities/_api.scss +62 -69
  61. package/packages/fokus-utilities/scss/utilities/_borders.scss +39 -0
  62. package/packages/fokus-utilities/scss/utilities/_colors.scss +27 -0
  63. package/packages/fokus-utilities/scss/utilities/_display.scss +20 -16
  64. package/packages/fokus-utilities/scss/utilities/_effects.scss +53 -0
  65. package/packages/fokus-utilities/scss/utilities/_flex.scss +50 -35
  66. package/packages/fokus-utilities/scss/utilities/_generated.scss +21 -0
  67. package/packages/fokus-utilities/scss/utilities/_grid.scss +25 -0
  68. package/packages/fokus-utilities/scss/utilities/_helpers.scss +35 -0
  69. package/packages/fokus-utilities/scss/utilities/_index.scss +11 -0
  70. package/packages/fokus-utilities/scss/utilities/_interactivity.scss +51 -0
  71. package/packages/fokus-utilities/scss/utilities/_layout.scss +61 -0
  72. package/packages/fokus-utilities/scss/utilities/_motion.scss +51 -0
  73. package/packages/fokus-utilities/scss/utilities/_position.scss +6 -6
  74. package/packages/fokus-utilities/scss/utilities/_print.scss +22 -18
  75. package/packages/fokus-utilities/scss/utilities/_shadow.scss +7 -3
  76. package/packages/fokus-utilities/scss/utilities/_sizing.scss +46 -0
  77. package/packages/fokus-utilities/scss/utilities/_spacing.scss +40 -113
  78. package/packages/fokus-utilities/scss/utilities/_states.scss +26 -10
  79. package/packages/fokus-utilities/scss/utilities/_transforms.scss +67 -0
  80. package/packages/fokus-utilities/scss/utilities/_typography.scss +72 -94
  81. package/scss/entries/helpers-entry.scss +5 -0
  82. package/scss/entries/rtl-entry.scss +6 -3
@@ -1,6 +1,7 @@
1
1
  import { computePosition, applyPosition } from "./core/positioning.js";
2
2
  import { onClickOutside } from "./core/overlay.js";
3
3
  import { onEscapeKey } from "./core/focus.js";
4
+ import { applyThemeContext } from "./core/theme.js";
4
5
  import { autoInit, createInstanceRegistry } from "./core/register.js";
5
6
 
6
7
  const instances = createInstanceRegistry();
@@ -175,12 +176,7 @@ export class Combobox {
175
176
  if (this.isOpen) return;
176
177
  this.isOpen = true;
177
178
 
178
- const theme = this.inputEl.closest("[data-theme]")?.getAttribute("data-theme");
179
- if (theme) {
180
- this.listboxEl.setAttribute("data-theme", theme);
181
- } else {
182
- this.listboxEl.removeAttribute("data-theme");
183
- }
179
+ applyThemeContext(this.listboxEl, this.inputEl);
184
180
 
185
181
  this.listboxEl.classList.add("is-open");
186
182
  this.listboxEl.style.width = `${this.inputEl.offsetWidth}px`;
@@ -1,5 +1,6 @@
1
1
  import { lockScroll, unlockScroll, onClickOutside } from "./core/overlay.js";
2
2
  import { createFocusTrap, onEscapeKey } from "./core/focus.js";
3
+ import { applyThemeContext } from "./core/theme.js";
3
4
  import { autoInit, createInstanceRegistry } from "./core/register.js";
4
5
 
5
6
  const instances = createInstanceRegistry();
@@ -196,12 +197,7 @@ export class CommandPalette {
196
197
  if (this.isOpen) return;
197
198
  this.isOpen = true;
198
199
 
199
- const theme = this.triggerEl.closest("[data-theme]")?.getAttribute("data-theme");
200
- if (theme) {
201
- this.paletteEl.setAttribute("data-theme", theme);
202
- } else {
203
- this.paletteEl.removeAttribute("data-theme");
204
- }
200
+ applyThemeContext(this.paletteEl, this.triggerEl);
205
201
 
206
202
  this.paletteEl.classList.add("is-open");
207
203
  lockScroll();
@@ -6,12 +6,14 @@ export interface ComputePositionOptions {
6
6
  align?: Align;
7
7
  offset?: number;
8
8
  padding?: number;
9
+ fallbackPlacements?: Placement[];
9
10
  }
10
11
 
11
12
  export interface Position {
12
13
  top: number;
13
14
  left: number;
14
15
  placement: Placement;
16
+ arrowOffset?: string;
15
17
  }
16
18
 
17
19
  export function computePosition(
@@ -41,8 +41,8 @@ export function computePosition(referenceEl, floatingEl, options = {}) {
41
41
 
42
42
  const candidates = [...new Set(fallbackPlacements ?? [placement, OPPOSITE_PLACEMENT[placement], "bottom", "top", "right", "left"])]
43
43
  .filter(Boolean);
44
- const primaryCandidates = candidates.slice(0, 2);
45
- const finalPlacement = primaryCandidates.find((candidate) => fitsPlacement(candidate, referenceRect, floatingRect, offset, viewportWidth, viewportHeight))
44
+ const placementCandidates = fallbackPlacements ? candidates : candidates.slice(0, 2);
45
+ const finalPlacement = placementCandidates.find((candidate) => fitsPlacement(candidate, referenceRect, floatingRect, offset, viewportWidth, viewportHeight))
46
46
  ?? placement;
47
47
 
48
48
  let top;
@@ -0,0 +1,8 @@
1
+ export function applyThemeContext(targetEl, sourceEl) {
2
+ targetEl.removeAttribute("data-theme");
3
+ targetEl.removeAttribute("data-fs-theme");
4
+
5
+ const themeAttr = sourceEl.hasAttribute("data-fs-theme") ? "data-fs-theme" : "data-theme";
6
+ const theme = sourceEl.closest(`[${themeAttr}]`)?.getAttribute(themeAttr);
7
+ if (theme) targetEl.setAttribute(themeAttr, theme);
8
+ }
@@ -51,7 +51,10 @@ export class DataTable {
51
51
  this.tableEl = tableEl;
52
52
  this.theadEl = tableEl.querySelector("thead");
53
53
  this.tbodyEl = tbodyEl;
54
- this.pageSize = Number(rootEl.getAttribute("data-fs-page-size")) || options.pageSize || 10;
54
+ const configuredPageSize = Number(rootEl.getAttribute("data-fs-page-size") ?? options.pageSize ?? 10);
55
+ this.pageSize = Number.isFinite(configuredPageSize) && configuredPageSize > 0
56
+ ? Math.floor(configuredPageSize)
57
+ : 10;
55
58
 
56
59
  this.filterInput = rootEl.querySelector("[data-fs-datatable-filter]");
57
60
  this.emptyEl = rootEl.querySelector("[data-fs-datatable-empty]");
@@ -191,7 +194,8 @@ export class DataTable {
191
194
 
192
195
  goToPage(page) {
193
196
  const pageCount = this.pageCount;
194
- const next = Math.min(Math.max(1, page), pageCount);
197
+ const requested = Number(page);
198
+ const next = Number.isFinite(requested) ? Math.min(Math.max(1, Math.floor(requested)), pageCount) : 1;
195
199
  if (next === this.currentPage) return;
196
200
 
197
201
  this.currentPage = next;
@@ -43,6 +43,12 @@ function isSameDay(a, b) {
43
43
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
44
44
  }
45
45
 
46
+ function addMonthsClamped(date, months) {
47
+ const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
48
+ const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
49
+ return new Date(target.getFullYear(), target.getMonth(), Math.min(date.getDate(), lastDay));
50
+ }
51
+
46
52
  function buildMonthCells(viewDate) {
47
53
  const year = viewDate.getFullYear();
48
54
  const month = viewDate.getMonth();
@@ -246,13 +252,13 @@ export class Datepicker {
246
252
  break;
247
253
  case "PageUp":
248
254
  nextDate = event.shiftKey
249
- ? new Date(currentDate.getFullYear() - 1, currentDate.getMonth(), currentDate.getDate())
250
- : new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate());
255
+ ? addMonthsClamped(currentDate, -12)
256
+ : addMonthsClamped(currentDate, -1);
251
257
  break;
252
258
  case "PageDown":
253
259
  nextDate = event.shiftKey
254
- ? new Date(currentDate.getFullYear() + 1, currentDate.getMonth(), currentDate.getDate())
255
- : new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate());
260
+ ? addMonthsClamped(currentDate, 12)
261
+ : addMonthsClamped(currentDate, 1);
256
262
  break;
257
263
  case "Enter":
258
264
  case " ":
@@ -328,6 +334,7 @@ export class Datepicker {
328
334
  return (
329
335
  `<button type="button" class="${classes.join(" ")}" role="gridcell" ` +
330
336
  `tabindex="${isTabbable ? "0" : "-1"}" aria-selected="${isSelected}" ` +
337
+ `${isToday ? 'aria-current="date" ' : ""}` +
331
338
  `data-year="${cell.year}" data-month="${cell.month}" data-day="${cell.day}">${cell.day}</button>`
332
339
  );
333
340
  })
@@ -344,7 +351,8 @@ export class Datepicker {
344
351
  if (this.isOpen) return;
345
352
  this.isOpen = true;
346
353
 
347
- const theme = this.inputEl.closest("[data-theme]")?.getAttribute("data-theme");
354
+ const themeRoot = this.inputEl.closest("[data-theme], [data-fs-theme]");
355
+ const theme = themeRoot?.getAttribute("data-theme") ?? themeRoot?.getAttribute("data-fs-theme");
348
356
  if (theme) {
349
357
  this.panelEl.setAttribute("data-theme", theme);
350
358
  } else {
@@ -26,20 +26,28 @@ export class Dropdown {
26
26
 
27
27
  document.body.appendChild(this.menuEl);
28
28
 
29
+ if (!this.toggleEl.id) this.toggleEl.id = `fs-dropdown-toggle-${Math.random().toString(36).slice(2)}`;
29
30
  this.toggleEl.setAttribute("aria-haspopup", "menu");
30
31
  this.toggleEl.setAttribute("aria-expanded", "false");
31
32
  this.toggleEl.setAttribute("aria-controls", this.menuEl.id || `fs-dropdown-${Math.random().toString(36).slice(2)}`);
32
33
  if (!this.menuEl.id) this.menuEl.id = this.toggleEl.getAttribute("aria-controls");
33
- this.menuEl.setAttribute("role", "menu");
34
+ if (!this.menuEl.hasAttribute("role")) this.menuEl.setAttribute("role", "menu");
35
+ this.menuEl.setAttribute("aria-orientation", "vertical");
36
+ this.menuEl.setAttribute("aria-labelledby", this.toggleEl.id || this.toggleEl.getAttribute("aria-controls"));
34
37
  this.menuEl.querySelectorAll(".fs-dropdown-item").forEach((item) => {
35
38
  if (!item.hasAttribute("role")) item.setAttribute("role", "menuitem");
39
+ if (this._isDisabled(item)) item.setAttribute("aria-disabled", "true");
36
40
  });
41
+ this.menuEl.querySelectorAll(".fs-dropdown-divider").forEach((divider) => divider.setAttribute("role", "separator"));
42
+ this.menuEl.querySelectorAll(".fs-dropdown-header").forEach((header) => header.setAttribute("role", "presentation"));
37
43
 
38
44
  this._handleToggleClick = this._handleToggleClick.bind(this);
45
+ this._handleToggleKeydown = this._handleToggleKeydown.bind(this);
39
46
  this._handleMenuKeydown = this._handleMenuKeydown.bind(this);
40
47
  this._handleMenuClick = this._handleMenuClick.bind(this);
41
48
 
42
49
  this.toggleEl.addEventListener("click", this._handleToggleClick);
50
+ this.toggleEl.addEventListener("keydown", this._handleToggleKeydown);
43
51
  this.menuEl.addEventListener("keydown", this._handleMenuKeydown);
44
52
  this.menuEl.addEventListener("click", this._handleMenuClick);
45
53
 
@@ -67,15 +75,25 @@ export class Dropdown {
67
75
  this.toggle();
68
76
  }
69
77
 
78
+ _handleToggleKeydown(event) {
79
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
80
+
81
+ event.preventDefault();
82
+ if (!this.isOpen) this.show();
83
+
84
+ const items = this._items();
85
+ (event.key === "ArrowUp" ? items.at(-1) : items[0])?.focus();
86
+ }
87
+
70
88
  _handleMenuClick(event) {
71
- if (event.target.closest(".fs-dropdown-item")) {
89
+ if (event.target.closest(".fs-dropdown-item, [data-fs-dropdown-dismiss]")) {
72
90
  this.hide();
73
91
  this.toggleEl.focus();
74
92
  }
75
93
  }
76
94
 
77
95
  _handleMenuKeydown(event) {
78
- const items = Array.from(this.menuEl.querySelectorAll(".fs-dropdown-item:not(.is-disabled)"));
96
+ const items = this._items();
79
97
  const currentIndex = items.indexOf(document.activeElement);
80
98
 
81
99
  if (event.key === "ArrowDown") {
@@ -101,7 +119,8 @@ export class Dropdown {
101
119
  if (this.isOpen) return;
102
120
  this.isOpen = true;
103
121
 
104
- const theme = this.toggleEl.closest("[data-theme]")?.getAttribute("data-theme");
122
+ const themeRoot = this.toggleEl.closest("[data-theme], [data-fs-theme]");
123
+ const theme = themeRoot?.getAttribute("data-theme") ?? themeRoot?.getAttribute("data-fs-theme");
105
124
  if (theme) {
106
125
  this.menuEl.setAttribute("data-theme", theme);
107
126
  } else {
@@ -116,6 +135,8 @@ export class Dropdown {
116
135
  offset: 4,
117
136
  });
118
137
  applyPosition(this.menuEl, position);
138
+ this.menuEl.dataset.placement = position.placement;
139
+ this.menuEl.dataset.align = this.align;
119
140
  this._positionCleanup = watchPosition(this.toggleEl, this.menuEl, {
120
141
  placement: this.placement,
121
142
  align: this.align,
@@ -124,7 +145,7 @@ export class Dropdown {
124
145
 
125
146
  this.toggleEl.setAttribute("aria-expanded", "true");
126
147
 
127
- const firstItem = this.menuEl.querySelector(".fs-dropdown-item:not(.is-disabled)");
148
+ const firstItem = this._items()[0];
128
149
  firstItem?.focus();
129
150
 
130
151
  this._outsideClickCleanup = onClickOutside(this.menuEl, (event) => {
@@ -144,6 +165,8 @@ export class Dropdown {
144
165
  this.menuEl.style.removeProperty("position");
145
166
  this.menuEl.style.removeProperty("top");
146
167
  this.menuEl.style.removeProperty("left");
168
+ delete this.menuEl.dataset.placement;
169
+ delete this.menuEl.dataset.align;
147
170
  this.toggleEl.setAttribute("aria-expanded", "false");
148
171
 
149
172
  this._outsideClickCleanup?.();
@@ -163,10 +186,19 @@ export class Dropdown {
163
186
  }
164
187
  }
165
188
 
189
+ _isDisabled(item) {
190
+ return item.classList.contains("is-disabled") || item.disabled || item.getAttribute("aria-disabled") === "true";
191
+ }
192
+
193
+ _items() {
194
+ return Array.from(this.menuEl.querySelectorAll(".fs-dropdown-item")).filter((item) => !this._isDisabled(item));
195
+ }
196
+
166
197
  dispose() {
167
198
  this.hide();
168
199
  this._removeEscapeListener();
169
200
  this.toggleEl.removeEventListener("click", this._handleToggleClick);
201
+ this.toggleEl.removeEventListener("keydown", this._handleToggleKeydown);
170
202
  this.menuEl.removeEventListener("keydown", this._handleMenuKeydown);
171
203
  this.menuEl.removeEventListener("click", this._handleMenuClick);
172
204
  if (this._originalParent) {
@@ -1,6 +1,7 @@
1
1
  import { computePosition, applyPosition } from "./core/positioning.js";
2
2
  import { onClickOutside } from "./core/overlay.js";
3
3
  import { onEscapeKey } from "./core/focus.js";
4
+ import { applyThemeContext } from "./core/theme.js";
4
5
  import { autoInit, createInstanceRegistry } from "./core/register.js";
5
6
 
6
7
  const instances = createInstanceRegistry();
@@ -243,12 +244,7 @@ export class NestedMenu {
243
244
  if (this.isOpen) return;
244
245
  this.isOpen = true;
245
246
 
246
- const theme = this.toggleEl.closest("[data-theme]")?.getAttribute("data-theme");
247
- if (theme) {
248
- this.rootMenu.setAttribute("data-theme", theme);
249
- } else {
250
- this.rootMenu.removeAttribute("data-theme");
251
- }
247
+ applyThemeContext(this.rootMenu, this.toggleEl);
252
248
 
253
249
  this.rootMenu.classList.add("is-open");
254
250
 
@@ -2,6 +2,7 @@ import { Toast } from "./toast.js";
2
2
  import { computePosition, applyPosition } from "./core/positioning.js";
3
3
  import { onClickOutside } from "./core/overlay.js";
4
4
  import { onEscapeKey } from "./core/focus.js";
5
+ import { applyThemeContext } from "./core/theme.js";
5
6
  import { autoInit, createInstanceRegistry } from "./core/register.js";
6
7
 
7
8
  const instances = createInstanceRegistry();
@@ -217,12 +218,7 @@ export class NotificationCenter {
217
218
  if (this.isOpen) return;
218
219
  this.isOpen = true;
219
220
 
220
- const theme = this.triggerEl.closest("[data-theme]")?.getAttribute("data-theme");
221
- if (theme) {
222
- this.panelEl.setAttribute("data-theme", theme);
223
- } else {
224
- this.panelEl.removeAttribute("data-theme");
225
- }
221
+ applyThemeContext(this.panelEl, this.triggerEl);
226
222
 
227
223
  this.panelEl.classList.add("is-open");
228
224
 
@@ -4,13 +4,13 @@ const instances = createInstanceRegistry();
4
4
  export class Pagination {
5
5
  constructor(element) {
6
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;
7
+ this.total = Math.max(1, this._normalizePage(element.getAttribute("data-fs-pages"), element.querySelectorAll("[data-fs-page]").length || 1));
8
+ this.current = Math.min(this.total, this._normalizePage(element.getAttribute("data-fs-page"), 1));
9
9
  this._click = (event) => {
10
10
  const button = event.target.closest("[data-fs-page]");
11
11
  if (!button || !this.element.contains(button)) return;
12
12
  event.preventDefault();
13
- this.goTo(Number(button.getAttribute("data-fs-page")));
13
+ this.goTo(button.getAttribute("data-fs-page"));
14
14
  };
15
15
  element.addEventListener("click", this._click);
16
16
  instances.set(element, this);
@@ -19,20 +19,26 @@ export class Pagination {
19
19
  static getInstance(el) { return instances.get(el); }
20
20
  static getOrCreateInstance(el) { return this.getInstance(el) ?? new Pagination(el); }
21
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));
22
+ const next = this._normalizePage(page, this.current);
23
+ const bounded = Math.max(1, Math.min(this.total, next));
24
+ if (bounded === this.current) return;
25
+ this.current = bounded;
26
+ this.element.setAttribute("data-fs-page", String(bounded));
26
27
  this._render();
27
- this.element.dispatchEvent(new CustomEvent("fs:pagination:change", { bubbles: true, detail: { page: next, total: this.total } }));
28
+ this.element.dispatchEvent(new CustomEvent("fs:pagination:change", { bubbles: true, detail: { page: bounded, total: this.total } }));
28
29
  }
29
30
  _render() {
30
31
  this.element.querySelectorAll("[data-fs-page]").forEach((item) => {
31
32
  const active = Number(item.getAttribute("data-fs-page")) === this.current;
32
33
  item.classList.toggle("is-active", active);
33
- item.setAttribute("aria-current", active ? "page" : "false");
34
+ if (active) item.setAttribute("aria-current", "page");
35
+ else item.removeAttribute("aria-current");
34
36
  });
35
37
  }
38
+ _normalizePage(value, fallback) {
39
+ const parsed = Number(value);
40
+ return Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : fallback;
41
+ }
36
42
  dispose() { this.element.removeEventListener("click", this._click); instances.delete(this.element); }
37
43
  }
38
44
  autoInit("pagination", Pagination);
@@ -1,6 +1,7 @@
1
1
  import { computePosition, applyPosition, watchPosition } from "./core/positioning.js";
2
2
  import { onClickOutside } from "./core/overlay.js";
3
3
  import { onEscapeKey } from "./core/focus.js";
4
+ import { applyThemeContext } from "./core/theme.js";
4
5
  import { autoInit, createInstanceRegistry } from "./core/register.js";
5
6
 
6
7
  const instances = createInstanceRegistry();
@@ -132,12 +133,7 @@ export class Popover {
132
133
  if (this.isOpen) return;
133
134
  this.isOpen = true;
134
135
 
135
- const theme = this.triggerEl.closest("[data-theme]")?.getAttribute("data-theme");
136
- if (theme) {
137
- this.popoverEl.setAttribute("data-theme", theme);
138
- } else {
139
- this.popoverEl.removeAttribute("data-theme");
140
- }
136
+ applyThemeContext(this.popoverEl, this.triggerEl);
141
137
 
142
138
  this.popoverEl.classList.add("is-open");
143
139
 
@@ -37,6 +37,10 @@ export class Select {
37
37
  return instances.get(el);
38
38
  }
39
39
 
40
+ get isOpen() {
41
+ return this.dropdown?.isOpen ?? false;
42
+ }
43
+
40
44
  _buildMarkup() {
41
45
  const wrapper = document.createElement("div");
42
46
  wrapper.className = "fs-dropdown fs-form-select-dropdown";
@@ -6,7 +6,7 @@ let idCounter = 0;
6
6
  export class Tabs {
7
7
  constructor(tablistEl) {
8
8
  this.tablistEl = tablistEl;
9
- this.tabs = Array.from(tablistEl.querySelectorAll(".fs-nav-link"));
9
+ this.tabs = Array.from(tablistEl.querySelectorAll(".fs-nav-link, [role=tab], [data-fs-tab-target]"));
10
10
  this.manualActivation = tablistEl.getAttribute("data-tabs-activation") === "manual";
11
11
 
12
12
  tablistEl.setAttribute("role", "tablist");
@@ -21,7 +21,7 @@ export class Tabs {
21
21
  tab.setAttribute("aria-selected", String(isActive));
22
22
  tab.setAttribute("tabindex", isActive ? "0" : "-1");
23
23
 
24
- const paneSelector = tab.getAttribute("data-fs-target");
24
+ const paneSelector = tab.getAttribute("data-fs-target") || tab.getAttribute("data-fs-tab-target");
25
25
  const pane = paneSelector ? document.querySelector(paneSelector) : null;
26
26
 
27
27
  if (pane) {
@@ -50,7 +50,7 @@ export class Tabs {
50
50
  static dispose(el) { return this.getInstance(el)?.dispose(); }
51
51
 
52
52
  _handleClick(event) {
53
- const tab = event.target.closest(".fs-nav-link:not(.is-disabled)");
53
+ const tab = event.target.closest(".fs-nav-link:not(.is-disabled), [role=tab]:not(.is-disabled), [data-fs-tab-target]:not(.is-disabled)");
54
54
  if (!tab || !this.tabs.includes(tab)) return;
55
55
  this.show(tab);
56
56
  }
@@ -84,7 +84,7 @@ export class Tabs {
84
84
  otherTab.setAttribute("aria-selected", String(isActive));
85
85
  otherTab.setAttribute("tabindex", isActive ? "0" : "-1");
86
86
 
87
- const paneSelector = otherTab.getAttribute("data-fs-target");
87
+ const paneSelector = otherTab.getAttribute("data-fs-target") || otherTab.getAttribute("data-fs-tab-target");
88
88
  const pane = paneSelector ? document.querySelector(paneSelector) : null;
89
89
  pane?.classList.toggle("is-active", isActive);
90
90
  if (pane) pane.hidden = !isActive;
@@ -1,5 +1,6 @@
1
1
  import { computePosition, applyPosition, watchPosition } from "./core/positioning.js";
2
2
  import { onEscapeKey } from "./core/focus.js";
3
+ import { applyThemeContext } from "./core/theme.js";
3
4
  import { autoInit, createInstanceRegistry } from "./core/register.js";
4
5
 
5
6
  const instances = createInstanceRegistry();
@@ -137,12 +138,7 @@ export class Tooltip {
137
138
  openInstance = this;
138
139
  this.isOpen = true;
139
140
 
140
- const theme = this.referenceEl.closest("[data-theme]")?.getAttribute("data-theme");
141
- if (theme) {
142
- this.tooltipEl.setAttribute("data-theme", theme);
143
- } else {
144
- this.tooltipEl.removeAttribute("data-theme");
145
- }
141
+ applyThemeContext(this.tooltipEl, this.referenceEl);
146
142
 
147
143
  this.tooltipEl.classList.add("is-open");
148
144
 
@@ -1,96 +1,89 @@
1
- /* stylelint-disable declaration-block-single-line-max-declarations */
1
+ @forward "../../../fokus-core/scss/settings/utilities";
2
2
  @use "sass:map";
3
- @use "../../fokus-core/scss/settings" as *;
4
- @use "../../fokus-core/scss/tools" as tools;
3
+ @use "../../../fokus-core/scss/settings" as *;
4
+ @use "../../../fokus-core/scss/tools" as *;
5
5
 
6
- // Utility API do Fokus Styles: os mapas são públicos e podem ser estendidos
7
- // antes do @use do entry-point pelo consumidor.
8
- $fs-utility-colors: $theme-colors !default;
9
- $fs-utility-spacers: $spacers !default;
10
- $fs-utility-sizes: (25: 25%, 50: 50%, 75: 75%, 100: 100%, auto: auto) !default;
11
- $fs-utility-displays: (block, inline, inline-block, flex, grid, none) !default;
12
- $fs-utility-flex-directions: (row, column, row-reverse, column-reverse) !default;
6
+ // API declarativa inspirada na Utility API do Bootstrap. A API aceita um
7
+ // subconjunto estático e previsível: valores oficiais, sem JIT e sem classes
8
+ // arbitrárias. Categorias históricas permanecem em seus módulos próprios para
9
+ // preservar aliases e a ordem do CSS público.
10
+ @mixin fs-generate-utility($name, $definition) {
11
+ $property: map.get($definition, property);
12
+ $class: map.get($definition, class);
13
+ $values: map.get($definition, values);
14
+ $category: map.get($definition, category);
15
+ $important: map.get($definition, important);
16
+ $responsive: false;
17
+ $print: false;
18
+ $states: ();
19
+ $declaration-property: $property;
13
20
 
14
- @layer utilities {
15
- @if fs-config-list-enabled(colors, color) {
16
- @each $name, $value in $fs-utility-colors {
17
- .fs-u-color-#{$name} { color: var(--fs-color-#{$name}, #{$value}) !important; }
18
- .fs-u-bg-#{$name} { background-color: var(--fs-color-#{$name}, #{$value}) !important; }
19
- }
21
+ @if map.has-key($definition, responsive) { $responsive: map.get($definition, responsive); }
22
+
23
+ @if map.has-key($definition, print) { $print: map.get($definition, print); }
24
+
25
+ @if map.has-key($definition, states) { $states: map.get($definition, states); }
26
+
27
+ @if map.has-key($definition, css-var) and map.get($definition, css-var) {
28
+ $declaration-property: --#{map.get($definition, css-variable-name)};
20
29
  }
30
+ $suffix: "";
21
31
 
22
- @if fs-config-list-enabled(spacing, margin) and fs-config-list-enabled(spacing, padding) {
23
- @each $name, $value in $fs-utility-spacers {
24
- .fs-u-m-#{$name} { margin: $value !important; }
25
- .fs-u-mx-#{$name} { margin-inline: $value !important; }
26
- .fs-u-my-#{$name} { margin-block: $value !important; }
27
- .fs-u-p-#{$name} { padding: $value !important; }
28
- .fs-u-px-#{$name} { padding-inline: $value !important; }
29
- .fs-u-py-#{$name} { padding-block: $value !important; }
30
- }
32
+ @if $important and fs-feature-enabled(important-utilities, true) {
33
+ $suffix: " !important";
31
34
  }
32
35
 
33
- @if fs-config-list-enabled(sizing, width) {
34
- @each $name, $value in $fs-utility-sizes {
35
- .fs-u-w-#{$name} { width: $value !important; }
36
- .fs-u-h-#{$name} { height: $value !important; }
36
+ @each $key, $value in $values {
37
+ .fs-u-#{$class}-#{$key} {
38
+ #{$declaration-property}: #{$value}#{$suffix};
37
39
  }
38
40
  }
39
41
 
40
- @if fs-config-get(responsive, true) {
42
+ @if $responsive and fs-feature-enabled(responsive, true) {
41
43
  @each $breakpoint, $min in $breakpoints {
44
+ $infix: breakpoint-infix($breakpoint);
45
+
42
46
  @if $min != 0 {
43
- @media (min-width: $min) {
44
- .fs-u-d-#{$breakpoint}-flex { display: flex !important; }
45
- .fs-u-d-#{$breakpoint}-none { display: none !important; }
46
- .fs-u-text-#{$breakpoint}-start { text-align: start !important; }
47
+ @include media-breakpoint-up($breakpoint) {
48
+ @each $key, $value in $values {
49
+ .fs-u-#{$class}#{$infix}-#{$key} {
50
+ #{$declaration-property}: #{$value}#{$suffix};
51
+ }
52
+ }
47
53
  }
48
54
  }
49
55
  }
50
56
  }
51
57
 
52
- @if fs-config-list-enabled(display, display) {
53
- @each $display in $fs-utility-displays {
54
- .fs-u-d-#{$display} { display: $display !important; }
58
+ @each $state, $pseudo in $states {
59
+ @each $key, $value in $values {
60
+ .fs-u-#{$class}-#{$key}-#{$state}:#{$pseudo} {
61
+ #{$declaration-property}: #{$value}#{$suffix};
62
+ }
55
63
  }
56
64
  }
57
65
 
58
- @if fs-config-list-enabled(flex, flex) {
59
- @each $direction in $fs-utility-flex-directions {
60
- .fs-u-flex-#{$direction} { flex-direction: $direction !important; }
66
+ @if $print and fs-feature-enabled(print, true) {
67
+ @media print {
68
+ @each $key, $value in $values {
69
+ .fs-u-#{$class}-#{$key}-print {
70
+ #{$declaration-property}: #{$value}#{$suffix};
71
+ }
72
+ }
61
73
  }
62
- .fs-u-flex-wrap { flex-wrap: wrap !important; }
63
- .fs-u-flex-nowrap { flex-wrap: nowrap !important; }
64
- .fs-u-justify-start { justify-content: flex-start !important; }
65
- .fs-u-justify-center { justify-content: center !important; }
66
- .fs-u-justify-end { justify-content: flex-end !important; }
67
- .fs-u-justify-between { justify-content: space-between !important; }
68
- .fs-u-align-start { align-items: flex-start !important; }
69
- .fs-u-align-center { align-items: center !important; }
70
- .fs-u-align-end { align-items: flex-end !important; }
71
74
  }
75
+ }
72
76
 
73
- @if fs-config-list-enabled(gap, gap) {
74
- @each $name, $value in $fs-utility-spacers {
75
- .fs-u-gap-#{$name} { gap: $value !important; }
76
- .fs-u-row-gap-#{$name} { row-gap: $value !important; }
77
- .fs-u-column-gap-#{$name} { column-gap: $value !important; }
78
- }
79
- }
77
+ @mixin fs-generate-utilities($utilities: $fs-utilities) {
78
+ @each $name, $definition in $utilities {
79
+ $category: utilities;
80
80
 
81
- @if fs-config-list-enabled(typography, typography) {
82
- .fs-u-text-start { text-align: start !important; }
83
- .fs-u-text-center { text-align: center !important; }
84
- .fs-u-text-end { text-align: end !important; }
85
- .fs-u-text-wrap { white-space: normal !important; }
86
- .fs-u-text-nowrap { white-space: nowrap !important; }
87
- .fs-u-fw-normal { font-weight: 400 !important; }
88
- .fs-u-fw-semibold { font-weight: 600 !important; }
89
- .fs-u-fw-bold { font-weight: 700 !important; }
90
- }
81
+ @if map.has-key($definition, category) {
82
+ $category: map.get($definition, category);
83
+ }
91
84
 
92
- @if fs-config-list-enabled(states, states) {
93
- .fs-u-focus-ring:focus-visible { outline: var(--fs-focus-width) solid var(--fs-focus-color); outline-offset: var(--fs-focus-offset); }
94
- .fs-u-disabled:disabled, .fs-u-disabled[aria-disabled="true"] { pointer-events: none; opacity: var(--fs-disabled-opacity); }
85
+ @if fs-config-list-enabled($category, $name) {
86
+ @include fs-generate-utility($name, $definition);
87
+ }
95
88
  }
96
89
  }