@orangesk/orange-design-system 3.2.0 → 3.3.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 (118) hide show
  1. package/build/components/Avatar/style.css +2 -0
  2. package/build/components/Avatar/style.css.map +1 -0
  3. package/build/components/Carousel/style.css +1 -1
  4. package/build/components/Carousel/style.css.map +1 -1
  5. package/build/components/Countdown/style.css +2 -0
  6. package/build/components/Countdown/style.css.map +1 -0
  7. package/build/components/DocumentationSidebar/style.css +1 -1
  8. package/build/components/DocumentationSidebar/style.css.map +1 -1
  9. package/build/components/Footer/style.css +1 -1
  10. package/build/components/Footer/style.css.map +1 -1
  11. package/build/components/Megamenu/style.css +1 -1
  12. package/build/components/Megamenu/style.css.map +1 -1
  13. package/build/components/Pill/style.css +1 -1
  14. package/build/components/Pill/style.css.map +1 -1
  15. package/build/components/Preview/style.css +1 -1
  16. package/build/components/Preview/style.css.map +1 -1
  17. package/build/components/StarRating/style.css +2 -0
  18. package/build/components/StarRating/style.css.map +1 -0
  19. package/build/components/index.js +1 -1
  20. package/build/components/index.js.map +1 -1
  21. package/build/components/tsconfig.tsbuildinfo +1 -1
  22. package/build/components/types/index.d.ts +50 -12
  23. package/build/components/types/src/components/Avatar/Avatar.d.ts +7 -0
  24. package/build/components/types/src/components/Avatar/index.d.ts +2 -0
  25. package/build/components/types/src/components/Carousel/constants.d.ts +2 -0
  26. package/build/components/types/src/components/Countdown/Countdown.d.ts +10 -0
  27. package/build/components/types/src/components/Countdown/Countdown.react.d.ts +8 -0
  28. package/build/components/types/src/components/Countdown/Countdown.shared.d.ts +54 -0
  29. package/build/components/types/src/components/Countdown/Countdown.static.d.ts +21 -0
  30. package/build/components/types/src/components/Countdown/CountdownTag.d.ts +9 -0
  31. package/build/components/types/src/components/Countdown/CountdownTag.static.d.ts +16 -0
  32. package/build/components/types/src/components/Countdown/constants.d.ts +28 -0
  33. package/build/components/types/src/components/Countdown/index.d.ts +3 -0
  34. package/build/components/types/src/components/DocumentationSidebar/DocumentationSidebar.d.ts +1 -0
  35. package/build/components/types/src/components/Preview/PreviewGenerator.d.ts +1 -0
  36. package/build/components/types/src/components/PromoBanner/PromoBanner.d.ts +5 -1
  37. package/build/components/types/src/components/StarRating/StarRating.d.ts +9 -0
  38. package/build/components/types/src/components/StarRating/index.d.ts +1 -0
  39. package/build/components/types/src/components/index.d.ts +4 -2
  40. package/build/components/types/src/scripts/index.d.ts +10 -0
  41. package/build/components/types/src/styles/export/color.d.ts +2 -0
  42. package/build/lib/base.css +1 -1
  43. package/build/lib/base.css.map +1 -1
  44. package/build/lib/components.css +1 -1
  45. package/build/lib/components.css.map +1 -1
  46. package/build/lib/footer.css +1 -1
  47. package/build/lib/footer.css.map +1 -1
  48. package/build/lib/megamenu.css +1 -1
  49. package/build/lib/megamenu.css.map +1 -1
  50. package/build/lib/scripts.js +1 -1
  51. package/build/lib/scripts.js.map +1 -1
  52. package/build/lib/style.css +1 -1
  53. package/build/lib/style.css.map +1 -1
  54. package/build/lib/tsconfig.tsbuildinfo +1 -1
  55. package/build/lib/utilities.css +1 -1
  56. package/build/lib/utilities.css.map +1 -1
  57. package/build/sprite.svg +1 -1
  58. package/package.json +29 -28
  59. package/src/assets/icons/quote.svg +1 -1
  60. package/src/assets/icons/star-empty-thin.svg +4 -0
  61. package/src/components/Avatar/Avatar.tsx +36 -0
  62. package/src/components/Avatar/index.ts +2 -0
  63. package/src/components/Avatar/styles/config.scss +11 -0
  64. package/src/components/Avatar/styles/mixins.scss +53 -0
  65. package/src/components/Avatar/styles/style.scss +20 -0
  66. package/src/components/Avatar/tests/Avatar.conformance.test.jsx +23 -0
  67. package/src/components/Avatar/tests/Avatar.unit.test.jsx +45 -0
  68. package/src/components/Carousel/Carousel.static.ts +17 -3
  69. package/src/components/Carousel/Carousel.tsx +28 -24
  70. package/src/components/Carousel/constants.ts +2 -0
  71. package/src/components/Carousel/styles/config.scss +5 -8
  72. package/src/components/Carousel/styles/mixins.scss +48 -78
  73. package/src/components/Carousel/styles/style.scss +8 -21
  74. package/src/components/Carousel/tests/Carousel.unit.test.jsx +48 -5
  75. package/src/components/Countdown/Countdown.react.ts +67 -0
  76. package/src/components/Countdown/Countdown.shared.ts +228 -0
  77. package/src/components/Countdown/Countdown.static.ts +201 -0
  78. package/src/components/Countdown/Countdown.tsx +105 -0
  79. package/src/components/Countdown/CountdownTag.static.ts +140 -0
  80. package/src/components/Countdown/CountdownTag.tsx +99 -0
  81. package/src/components/Countdown/constants.ts +36 -0
  82. package/src/components/Countdown/index.ts +3 -0
  83. package/src/components/Countdown/styles/config.scss +17 -0
  84. package/src/components/Countdown/styles/mixins.scss +193 -0
  85. package/src/components/Countdown/styles/style.scss +82 -0
  86. package/src/components/{Testimonial/tests/Testimonial.conformance.test.jsx → Countdown/tests/Countdown.conformance.test.jsx} +7 -2
  87. package/src/components/Countdown/tests/Countdown.static.test.jsx +117 -0
  88. package/src/components/Countdown/tests/Countdown.unit.test.jsx +101 -0
  89. package/src/components/Countdown/tests/CountdownTag.conformance.test.jsx +21 -0
  90. package/src/components/Countdown/tests/CountdownTag.static.test.jsx +30 -0
  91. package/src/components/Countdown/tests/CountdownTag.unit.test.jsx +67 -0
  92. package/src/components/DocumentationSidebar/DocumentationSidebar.tsx +14 -1
  93. package/src/components/DocumentationSidebar/styles/style.scss +11 -0
  94. package/src/components/Forms/Autocomplete/Autocomplete.static.ts +14 -1
  95. package/src/components/Pill/styles/style.scss +2 -1
  96. package/src/components/Preview/PreviewGenerator.tsx +137 -103
  97. package/src/components/Preview/styles/style.scss +15 -9
  98. package/src/components/PromoBanner/PromoBanner.tsx +24 -7
  99. package/src/components/PromoBanner/tests/PromoBanner.unit.test.jsx +31 -0
  100. package/src/components/StarRating/StarRating.tsx +52 -0
  101. package/src/components/StarRating/index.ts +1 -0
  102. package/src/components/StarRating/styles/style.scss +28 -0
  103. package/src/components/StarRating/tests/StarRating.unit.test.jsx +62 -0
  104. package/src/components/index.ts +7 -2
  105. package/src/styles/export/color.js +2 -0
  106. package/src/styles/tokens/color.scss +2 -0
  107. package/src/styles/utilities/text.scss +6 -0
  108. package/build/components/Testimonial/style.css +0 -2
  109. package/build/components/Testimonial/style.css.map +0 -1
  110. package/build/components/types/src/components/Testimonial/Testimonial.d.ts +0 -10
  111. package/build/components/types/src/components/Testimonial/index.d.ts +0 -1
  112. package/build/search-index.json +0 -430
  113. package/src/components/Testimonial/Testimonial.tsx +0 -38
  114. package/src/components/Testimonial/index.ts +0 -1
  115. package/src/components/Testimonial/styles/config.scss +0 -42
  116. package/src/components/Testimonial/styles/mixins.scss +0 -44
  117. package/src/components/Testimonial/styles/style.scss +0 -16
  118. package/src/components/Testimonial/tests/Testimonial.unit.test.jsx +0 -43
@@ -11,8 +11,6 @@
11
11
  @use "../../Button/styles/mixins" as button-mixins;
12
12
  @use "./config";
13
13
 
14
- /* stylelint-disable value-no-vendor-prefix, property-no-vendor-prefix */
15
-
16
14
  @mixin base {
17
15
  display: flex;
18
16
  flex-direction: column;
@@ -21,8 +19,8 @@
21
19
  }
22
20
 
23
21
  @mixin viewport-wrapper {
24
- // wraps viewport and arrows to center them vertically
25
- position: relative;
22
+ width: 100%;
23
+ max-width: 100%;
26
24
  }
27
25
 
28
26
  @mixin viewport {
@@ -34,26 +32,15 @@
34
32
  -ms-overflow-style: none;
35
33
  transform: translateZ(0);
36
34
  scrollbar-width: none; // hides scrollbar on FF
37
-
38
- @each $breakpoint, $size in config.$viewport-horizontal-spacing {
39
- @include breakpoint.get($breakpoint) {
40
- margin-left: $size;
41
- margin-right: $size;
42
- }
43
- }
35
+ min-width: 0;
36
+ width: 100%;
37
+ max-width: 100%;
38
+ margin-bottom: convert.to-rem(30px);
44
39
 
45
40
  .carousel--scrollbar & {
46
- margin-left: 0;
47
- margin-right: 0;
48
- }
49
-
50
- @include breakpoint.get("sm", "down") {
51
- padding-bottom: math.div(config.$space, 2);
52
- }
53
-
54
- @include breakpoint.get("md") {
55
- margin-bottom: config.$space;
56
- overflow-x: hidden; // hide scrollbar on larger screens
41
+ @include breakpoint.get("md", "down") {
42
+ margin-bottom: convert.to-rem(20px);
43
+ }
57
44
  }
58
45
 
59
46
  &.is-draggable {
@@ -68,6 +55,21 @@
68
55
  }
69
56
  }
70
57
 
58
+ @mixin navigation {
59
+ display: grid;
60
+ grid-template-columns: minmax(0, 1fr) auto;
61
+ align-items: center;
62
+ column-gap: config.$navigation-gap;
63
+ row-gap: convert.to-rem(12px);
64
+ }
65
+
66
+ @mixin controls {
67
+ display: flex;
68
+ flex: 0 0 auto;
69
+ align-items: center;
70
+ gap: convert.to-rem(8px);
71
+ }
72
+
71
73
  @mixin track {
72
74
  transform: translateZ(0);
73
75
  width: 100%;
@@ -77,28 +79,6 @@
77
79
  z-index: 1;
78
80
  }
79
81
 
80
- @function get-breakout-media-query($min, $max) {
81
- @return if(
82
- $max,
83
- "(min-width: #{$min}) and (max-width: #{$max})",
84
- "(min-width: #{$min})"
85
- );
86
- }
87
-
88
- @mixin base-breakout-buttons {
89
- @each $min, $max in config.$breakout-buttons-breakpoints {
90
- @media only screen and #{get-breakout-media-query($min, $max)} {
91
- margin-left: -#{convert.to-rem(60px)};
92
- margin-right: -#{convert.to-rem(60px)};
93
- }
94
- }
95
-
96
- &.carousel--bleed-right {
97
- margin-left: 0;
98
- margin-right: 0;
99
- }
100
- }
101
-
102
82
  @mixin slide-base {
103
83
  display: flex;
104
84
  flex-direction: column;
@@ -122,33 +102,6 @@
122
102
  }
123
103
  }
124
104
 
125
- @mixin button-base {
126
- @include button-mixins.base(button-config.$base);
127
- @include button-mixins.square-base();
128
- @include button-mixins.square-size("default");
129
- @include base.focusring-round();
130
-
131
- position: absolute;
132
- top: 50%;
133
- z-index: 10;
134
- margin: 0;
135
- border-radius: 100%;
136
-
137
- @include breakpoint.get("sm", "down") {
138
- display: none;
139
- }
140
- }
141
-
142
- @mixin button-prev {
143
- left: 0;
144
- transform: translateY(-50%);
145
- }
146
-
147
- @mixin button-next {
148
- right: 0;
149
- transform: translateY(-50%);
150
- }
151
-
152
105
  @mixin button-disabled {
153
106
  @include generate.css-map(sass-map.get(button-config.$base, "disabled"));
154
107
  }
@@ -156,32 +109,49 @@
156
109
  @mixin pagination {
157
110
  user-select: none;
158
111
  display: flex;
159
- flex-wrap: wrap;
160
- justify-content: center;
161
- margin: 0 auto;
112
+ margin: 0;
162
113
  padding: 0;
114
+ min-width: 0;
115
+ flex-wrap: wrap;
116
+ align-items: center;
117
+ justify-content: flex-start;
163
118
  }
164
119
 
165
120
  @mixin pagination-item {
121
+ appearance: none;
166
122
  background: none;
167
- border: 0;
168
- padding: 0;
169
123
  user-select: none;
170
124
  display: block;
171
125
  cursor: pointer;
172
126
  border-radius: 100%;
127
+ border: 0;
128
+ box-shadow: none;
129
+ padding: 0;
130
+ -webkit-tap-highlight-color: transparent;
173
131
 
174
132
  @include generate.css-map(config.$dot-layout);
175
133
  @include base.focusring-round();
176
-
177
134
  background-color: sass-map.get(config.$dot, "default");
178
135
 
179
136
  &:hover {
180
137
  background-color: sass-map.get(config.$dot, "hover");
181
138
  }
182
139
 
140
+ &:active {
141
+ background-color: sass-map.get(config.$dot, "hover");
142
+ }
143
+
183
144
  &.is-active {
184
- background-color: sass-map.get(config.$dot, "active");
145
+ background-color: transparent;
146
+ box-shadow: 0 0 0 2px var(--color-border-accent);
147
+ }
148
+
149
+ &.is-active:hover,
150
+ &.is-active:active,
151
+ &.is-active:focus,
152
+ &.is-active:focus-visible {
153
+ background-color: transparent;
154
+ box-shadow: 0 0 0 2px var(--color-border-accent);
185
155
  }
186
156
  }
187
157
 
@@ -13,10 +13,6 @@
13
13
  @include mixins.bleed-right-variant;
14
14
  }
15
15
 
16
- .container > & {
17
- @include mixins.base-breakout-buttons;
18
- }
19
-
20
16
  &__viewport-wrapper {
21
17
  @include mixins.viewport-wrapper;
22
18
  }
@@ -25,29 +21,20 @@
25
21
  @include mixins.viewport;
26
22
  }
27
23
 
28
- &__track {
29
- @include mixins.track;
24
+ &__navigation {
25
+ @include mixins.navigation;
30
26
  }
31
27
 
32
- &__slide {
33
- @include mixins.slide-base;
28
+ &__controls {
29
+ @include mixins.controls;
34
30
  }
35
31
 
36
- &__prev,
37
- &__next {
38
- @include mixins.button-base;
39
-
40
- &.is-disabled {
41
- @include mixins.button-disabled;
42
- }
43
- }
44
-
45
- &__prev {
46
- @include mixins.button-prev;
32
+ &__track {
33
+ @include mixins.track;
47
34
  }
48
35
 
49
- &__next {
50
- @include mixins.button-next;
36
+ &__slide {
37
+ @include mixins.slide-base;
51
38
  }
52
39
 
53
40
  &__pagination {
@@ -36,11 +36,11 @@ describe("rendering Carousel", () => {
36
36
  1,
37
37
  );
38
38
  });
39
- it("has default class carousel__scrollbar", () => {
39
+ it("does not render scrollbar by default", () => {
40
40
  const { container } = render(<Carousel items={[]} />);
41
41
  expect(
42
42
  container.getElementsByClassName("carousel__scrollbar").length,
43
- ).toBe(1);
43
+ ).toBe(0);
44
44
  });
45
45
  it("has default class carousel__pagination", () => {
46
46
  const { container } = render(<Carousel items={[]} />);
@@ -48,6 +48,18 @@ describe("rendering Carousel", () => {
48
48
  container.getElementsByClassName("carousel__pagination").length,
49
49
  ).toBe(1);
50
50
  });
51
+ it("has default class carousel__navigation", () => {
52
+ const { container } = render(<Carousel items={[]} />);
53
+ expect(
54
+ container.getElementsByClassName("carousel__navigation").length,
55
+ ).toBe(1);
56
+ });
57
+ it("has default class carousel__controls", () => {
58
+ const { container } = render(<Carousel items={[]} />);
59
+ expect(
60
+ container.getElementsByClassName("carousel__controls").length,
61
+ ).toBe(1);
62
+ });
51
63
  it("has button with aria-label Naspäť", () => {
52
64
  const { container } = render(<Carousel items={[]} />);
53
65
  expect(
@@ -97,18 +109,36 @@ describe("rendering Carousel", () => {
97
109
  <p key="item2">item2</p>,
98
110
  <p key="item3">item3</p>,
99
111
  ];
100
- it("has scrollbar element", () => {
112
+ it("does not render scrollbar element without showScrollbar", () => {
101
113
  const { container } = render(<Carousel items={[]} />);
102
114
  expect(
103
115
  container.getElementsByClassName("carousel__scrollbar").length,
104
- ).toBe(1);
116
+ ).toBe(0);
105
117
  });
106
- it("has scrollbar drag element", () => {
118
+ it("does not render scrollbar drag element without showScrollbar", () => {
107
119
  const { container } = render(<Carousel items={[]} />);
120
+ expect(
121
+ container.getElementsByClassName("carousel__scrollbar-drag").length,
122
+ ).toBe(0);
123
+ });
124
+ it("renders scrollbar when showScrollbar is true", () => {
125
+ const { container } = render(<Carousel items={items} showScrollbar />);
126
+ expect(
127
+ container.getElementsByClassName("carousel__scrollbar").length,
128
+ ).toBe(1);
108
129
  expect(
109
130
  container.getElementsByClassName("carousel__scrollbar-drag").length,
110
131
  ).toBe(1);
111
132
  });
133
+ it("does not render controls row when showScrollbar is true", () => {
134
+ const { container } = render(<Carousel items={items} showScrollbar />);
135
+ expect(
136
+ container.getElementsByClassName("carousel__navigation").length,
137
+ ).toBe(0);
138
+ expect(
139
+ container.getElementsByClassName("carousel__controls").length,
140
+ ).toBe(0);
141
+ });
112
142
  it("pagination container has role tablist", () => {
113
143
  const { container } = render(<Carousel items={items} />);
114
144
  expect(
@@ -166,6 +196,19 @@ describe("rendering Carousel", () => {
166
196
  );
167
197
  expect(getByTestId("slide-1").textContent).toBe("item1");
168
198
  });
199
+
200
+ it("uses Controls component defaults for built-in navigation", () => {
201
+ const { container } = render(<Carousel items={items} />);
202
+
203
+ expect(
204
+ container.querySelector(
205
+ `.carousel__prev.controls[aria-label="Naspäť"]`,
206
+ ),
207
+ ).toBeInTheDocument();
208
+ expect(
209
+ container.querySelector(`.carousel__next.controls[aria-label="Ďalej"]`),
210
+ ).toBeInTheDocument();
211
+ });
169
212
  });
170
213
  describe("swiperOptions prop", () => {
171
214
  const items = [
@@ -0,0 +1,67 @@
1
+ import {
2
+ CountdownConfig,
3
+ CountdownLabels,
4
+ CountdownUnit,
5
+ countdownUnits,
6
+ } from "./Countdown.static";
7
+
8
+ const defaultCountdownLabels: Required<CountdownLabels> = {
9
+ days: {
10
+ many: "dni",
11
+ few: "dni",
12
+ one: "deň",
13
+ },
14
+ hours: "hod",
15
+ minutes: "min",
16
+ seconds: "sek",
17
+ };
18
+
19
+ const defaultCountdownTagLabels: Required<CountdownLabels> = {
20
+ days: "d",
21
+ hours: "h",
22
+ minutes: "m",
23
+ seconds: "s",
24
+ };
25
+
26
+ const normalizeUnits = (units?: CountdownUnit[]): CountdownUnit[] => {
27
+ if (!units?.length) {
28
+ return [...countdownUnits];
29
+ }
30
+
31
+ return countdownUnits.filter((unit) => units.includes(unit));
32
+ };
33
+
34
+ const getLabelText = (
35
+ unit: CountdownUnit,
36
+ labels: CountdownLabels | undefined,
37
+ fallback: Required<CountdownLabels>,
38
+ ): string => {
39
+ if (unit === "days") {
40
+ const value = labels?.days ?? fallback.days;
41
+ return typeof value === "string" ? value : value.many;
42
+ }
43
+
44
+ return labels?.[unit] ?? fallback[unit];
45
+ };
46
+
47
+ const buildCountdownConfig = (
48
+ targetDate: string,
49
+ units: CountdownUnit[],
50
+ labels?: CountdownLabels,
51
+ ): CountdownConfig => ({
52
+ labels,
53
+ targetDate,
54
+ units: normalizeUnits(units),
55
+ });
56
+
57
+ const getAriaLive = (units: CountdownUnit[]): "off" | "polite" =>
58
+ units.includes("seconds") ? "off" : "polite";
59
+
60
+ export {
61
+ buildCountdownConfig,
62
+ defaultCountdownLabels,
63
+ defaultCountdownTagLabels,
64
+ getAriaLive,
65
+ getLabelText,
66
+ normalizeUnits,
67
+ };
@@ -0,0 +1,228 @@
1
+ import { ATTR_COUNTDOWN_CONFIG, SELECTOR_ANNOUNCEMENT } from "./constants";
2
+
3
+ export const countdownUnits = ["days", "hours", "minutes", "seconds"] as const;
4
+
5
+ export type CountdownUnit = (typeof countdownUnits)[number];
6
+
7
+ export type DayLabels = {
8
+ many: string;
9
+ few: string;
10
+ one: string;
11
+ };
12
+
13
+ export interface CountdownLabels {
14
+ days?: string | DayLabels;
15
+ hours?: string;
16
+ minutes?: string;
17
+ seconds?: string;
18
+ }
19
+
20
+ export interface CountdownConfig {
21
+ targetDate: string;
22
+ units?: CountdownUnit[];
23
+ labels?: CountdownLabels;
24
+ }
25
+
26
+ export interface CountdownState {
27
+ days: number;
28
+ hours: number;
29
+ minutes: number;
30
+ seconds: number;
31
+ }
32
+
33
+ const unitMs: Record<CountdownUnit, number> = {
34
+ days: 24 * 60 * 60 * 1000,
35
+ hours: 60 * 60 * 1000,
36
+ minutes: 60 * 1000,
37
+ seconds: 1000,
38
+ };
39
+
40
+ export const getVisibleUnits = (units?: CountdownUnit[]): CountdownUnit[] => {
41
+ if (!units?.length) {
42
+ return [...countdownUnits];
43
+ }
44
+
45
+ return countdownUnits.filter((unit) => units.includes(unit));
46
+ };
47
+
48
+ export const normalizeDayLabels = (
49
+ value: string | DayLabels | undefined,
50
+ fallback: DayLabels,
51
+ ): DayLabels => {
52
+ if (typeof value === "string" && value.trim()) {
53
+ return {
54
+ many: value,
55
+ few: value,
56
+ one: value,
57
+ };
58
+ }
59
+
60
+ if (
61
+ value &&
62
+ typeof value === "object" &&
63
+ typeof value.many === "string" &&
64
+ typeof value.few === "string" &&
65
+ typeof value.one === "string"
66
+ ) {
67
+ return value;
68
+ }
69
+
70
+ return fallback;
71
+ };
72
+
73
+ export const getTargetDate = (value: string): number => {
74
+ const timestamp = new Date(value).getTime();
75
+ return Number.isNaN(timestamp) ? 0 : timestamp;
76
+ };
77
+
78
+ export const calculateState = (
79
+ targetDate: string,
80
+ units: CountdownUnit[],
81
+ ): CountdownState => {
82
+ const target = getTargetDate(targetDate);
83
+ let remaining = Math.max(0, target - Date.now());
84
+
85
+ const values: CountdownState = {
86
+ days: 0,
87
+ hours: 0,
88
+ minutes: 0,
89
+ seconds: 0,
90
+ };
91
+
92
+ units.forEach((unit) => {
93
+ const currentValue = Math.floor(remaining / unitMs[unit]);
94
+ values[unit] = currentValue;
95
+ remaining -= currentValue * unitMs[unit];
96
+ });
97
+
98
+ return values;
99
+ };
100
+
101
+ export abstract class BaseCountdownStatic {
102
+ element: HTMLElement;
103
+ config: CountdownConfig;
104
+ liveRegion: HTMLElement | null;
105
+ timerId?: number;
106
+ observer?: IntersectionObserver;
107
+ private instanceKey: string;
108
+ private defaultConfig: CountdownConfig;
109
+
110
+ constructor(
111
+ element: HTMLElement,
112
+ config: Partial<CountdownConfig> | undefined,
113
+ options: {
114
+ defaultConfig: CountdownConfig;
115
+ instanceKey: string;
116
+ },
117
+ ) {
118
+ this.element = element;
119
+ this.defaultConfig = options.defaultConfig;
120
+ this.instanceKey = options.instanceKey;
121
+ this.config = { ...options.defaultConfig, ...(config ?? {}) };
122
+ this.liveRegion = null;
123
+
124
+ this.tick = this.tick.bind(this);
125
+ this.start = this.start.bind(this);
126
+ this.stop = this.stop.bind(this);
127
+
128
+ (this.element as any)[this.instanceKey] = this;
129
+ }
130
+
131
+ protected readDataConfig(): Partial<CountdownConfig> {
132
+ const attr = this.element.getAttribute(ATTR_COUNTDOWN_CONFIG);
133
+
134
+ if (!attr) {
135
+ return {};
136
+ }
137
+
138
+ try {
139
+ return JSON.parse(attr);
140
+ } catch (error) {
141
+ console.warn("Invalid JSON in data-countdown-config attribute", error);
142
+ return {};
143
+ }
144
+ }
145
+
146
+ protected getLiveRegion(): HTMLElement | null {
147
+ return this.element.querySelector(SELECTOR_ANNOUNCEMENT);
148
+ }
149
+
150
+ protected scheduleNextTick(): void {
151
+ this.stop();
152
+ const nextDelay = 1000 - (Date.now() % 1000) || 1000;
153
+ this.timerId = window.setTimeout(this.tick, nextDelay);
154
+ }
155
+
156
+ private tick(): void {
157
+ this.render();
158
+ this.scheduleNextTick();
159
+ }
160
+
161
+ private setupObserver(): void {
162
+ if (typeof IntersectionObserver === "undefined") {
163
+ this.start();
164
+ return;
165
+ }
166
+
167
+ this.observer = new IntersectionObserver((entries) => {
168
+ entries.forEach((entry) => {
169
+ if (entry.isIntersecting) {
170
+ this.start();
171
+ } else {
172
+ this.stop();
173
+ }
174
+ });
175
+ });
176
+
177
+ this.observer.observe(this.element);
178
+ }
179
+
180
+ init(): void {
181
+ const elementConfig = this.readDataConfig();
182
+
183
+ this.config = {
184
+ ...this.defaultConfig,
185
+ ...elementConfig,
186
+ ...this.config,
187
+ };
188
+
189
+ this.liveRegion = this.getLiveRegion();
190
+ this.collectElements();
191
+ this.render();
192
+ this.setupObserver();
193
+ }
194
+
195
+ start(): void {
196
+ this.render();
197
+ this.scheduleNextTick();
198
+ }
199
+
200
+ stop(): void {
201
+ if (this.timerId) {
202
+ window.clearTimeout(this.timerId);
203
+ this.timerId = undefined;
204
+ }
205
+ }
206
+
207
+ update(config?: Partial<CountdownConfig>): void {
208
+ this.stop();
209
+ this.observer?.disconnect();
210
+ this.destroyElements();
211
+ this.config = {
212
+ ...this.config,
213
+ ...(config ?? {}),
214
+ };
215
+ this.init();
216
+ }
217
+
218
+ destroy(): void {
219
+ this.stop();
220
+ this.observer?.disconnect();
221
+ this.destroyElements();
222
+ (this.element as any)[this.instanceKey] = null;
223
+ }
224
+
225
+ protected abstract collectElements(): void;
226
+ protected abstract render(): void;
227
+ protected abstract destroyElements(): void;
228
+ }