@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
@@ -33,6 +33,19 @@ const addClassToken = (className: string, token: string) => {
33
33
  return [...classTokens, token].join(" ");
34
34
  };
35
35
 
36
+ const isHtmlElement = (element: Element | null): element is HTMLElement => {
37
+ if (!element) {
38
+ return false;
39
+ }
40
+
41
+ const htmlElementConstructor =
42
+ element.ownerDocument?.defaultView?.HTMLElement;
43
+
44
+ return htmlElementConstructor
45
+ ? element instanceof htmlElementConstructor
46
+ : element.nodeType === Node.ELEMENT_NODE;
47
+ };
48
+
36
49
  export default class Autocomplete {
37
50
  private element: HTMLElement;
38
51
  private config: AutocompleteConfig;
@@ -139,7 +152,7 @@ export default class Autocomplete {
139
152
  private getWrapper(): HTMLElement | null {
140
153
  if (this.element.tagName === "SELECT") {
141
154
  const rootElement = this.element.previousElementSibling;
142
- if (!(rootElement instanceof HTMLElement)) {
155
+ if (!isHtmlElement(rootElement)) {
143
156
  return null;
144
157
  }
145
158
 
@@ -12,7 +12,8 @@
12
12
  &.surface-primary,
13
13
  &.surface-subtle,
14
14
  &.surface-moderate,
15
- &.surface-accent {
15
+ &.surface-accent,
16
+ &.fill-accent6 {
16
17
  color: var(--color-text-default);
17
18
  }
18
19
 
@@ -9,7 +9,6 @@ import React, {
9
9
  useRef,
10
10
  useState,
11
11
  } from "react";
12
- import { createPortal } from "react-dom";
13
12
  import reactElementToJSXString from "react-element-to-jsx-string";
14
13
  import breakpoints from "@/styles/export/breakpoint";
15
14
  import { Button } from "../Button";
@@ -87,57 +86,80 @@ interface PreviewRenderContext {
87
86
  }
88
87
 
89
88
  interface PreviewBreakpoint {
89
+ deviceLabel?: string;
90
+ frameClassName?: string;
90
91
  key: string;
91
92
  label: string;
93
+ height: number | null;
92
94
  width: number | null;
93
95
  }
94
96
 
95
97
  const PREVIEW_BREAKPOINTS: PreviewBreakpoint[] = [
96
- { key: "auto", label: "Auto", width: null },
98
+ {
99
+ key: "auto",
100
+ label: "Auto",
101
+ width: null,
102
+ height: null,
103
+ deviceLabel: undefined,
104
+ frameClassName: undefined,
105
+ },
97
106
  ...Object.entries(breakpoints).map(([key, value]) => {
98
107
  const tokenWidth = Number.parseInt(value, 10);
99
108
  // `xs` is 0 in tokens (mobile-first min-width), but preview needs a real viewport size.
100
109
  const width = key === "xs" ? 320 : tokenWidth;
110
+ const height =
111
+ key === "xs"
112
+ ? 640
113
+ : key === "sm"
114
+ ? 812
115
+ : key === "md"
116
+ ? 1024
117
+ : key === "lg"
118
+ ? 900
119
+ : 860;
120
+ const frameClassName =
121
+ key === "xs" || key === "sm"
122
+ ? "isPhone"
123
+ : key === "md"
124
+ ? "isTablet"
125
+ : "isDesktop";
126
+ const deviceLabel =
127
+ key === "xs"
128
+ ? "Phone S"
129
+ : key === "sm"
130
+ ? "Phone L"
131
+ : key === "md"
132
+ ? "Tablet"
133
+ : key === "lg"
134
+ ? "Laptop"
135
+ : "Desktop";
101
136
 
102
137
  return {
138
+ deviceLabel,
139
+ frameClassName,
103
140
  key,
104
- label: `${key.toUpperCase()} (${width}px)`,
141
+ height,
142
+ label: `${deviceLabel} (${width} x ${height})`,
105
143
  width,
106
144
  };
107
145
  }),
108
146
  ];
109
147
 
110
- const IFRAME_VIEWPORT_GUTTER = 16;
111
- const getScreenLikeMinHeight = (width: number | null): number => {
112
- if (width === null) {
113
- return 1;
114
- }
115
-
116
- if (width <= 480) {
117
- return 700;
118
- }
119
-
120
- if (width <= 768) {
121
- return 640;
122
- }
123
-
124
- return 560;
125
- };
126
-
127
148
  interface PreviewViewportFrameProps {
128
- children: ReactNode;
129
149
  className?: string;
130
- minHeight?: number;
150
+ frameClassName?: string;
151
+ html: string;
152
+ height?: number;
131
153
  }
132
154
 
133
155
  const PreviewViewportFrame: React.FC<PreviewViewportFrameProps> = ({
134
- children,
135
156
  className,
136
- minHeight = 1,
157
+ frameClassName,
158
+ height = 1,
159
+ html,
137
160
  }) => {
138
161
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
139
- const [iframeRoot, setIframeRoot] = useState<HTMLElement | null>(null);
140
- const [height, setHeight] = useState(1);
162
+ const [isIframeScriptsReady, setIsIframeScriptsReady] = useState(false);
141
163
 
142
164
  useEffect(() => {
143
165
  const iframe = iframeRef.current;
@@ -148,11 +170,11 @@ const PreviewViewportFrame: React.FC<PreviewViewportFrameProps> = ({
148
170
 
149
171
  doc.open();
150
172
  doc.write(
151
- `<!doctype html><html><head></head><body><div id="preview-iframe-root"></div></body></html>`,
173
+ `<!doctype html><html><head><meta name="viewport" content="width=device-width, initial-scale=1" /></head><body><div id="preview-iframe-root"></div></body></html>`,
152
174
  );
153
175
  doc.close();
154
176
 
155
- // Mirror parent styles so components inside iframe have the same look.
177
+ // Mirror parent styles so components inside iframe have same look.
156
178
  const stylesToClone = document.querySelectorAll(
157
179
  'link[rel="stylesheet"], style',
158
180
  );
@@ -164,78 +186,60 @@ const PreviewViewportFrame: React.FC<PreviewViewportFrameProps> = ({
164
186
  doc.body.style.margin = "0";
165
187
  doc.body.style.padding = "0";
166
188
  doc.body.style.background = "transparent";
167
- doc.body.style.overflow = "visible";
189
+ doc.body.style.overflow = "auto";
168
190
  doc.documentElement.style.margin = "0";
169
191
  doc.documentElement.style.padding = "0";
170
- doc.documentElement.style.overflow = "visible";
192
+ doc.documentElement.style.height = "100%";
193
+ doc.documentElement.style.overflow = "auto";
194
+ doc.body.style.height = "100%";
171
195
 
172
- iframe.setAttribute("scrolling", "no");
196
+ iframe.setAttribute("scrolling", "auto");
173
197
 
174
198
  const root = doc.getElementById("preview-iframe-root");
175
199
  if (!root) return;
176
200
 
201
+ const script = doc.createElement("script");
202
+ script.src = "/ods-preview-scripts.js";
203
+ script.addEventListener("load", () => {
204
+ setIsIframeScriptsReady(true);
205
+ });
206
+ doc.head.appendChild(script);
207
+
177
208
  root.style.width = "100%";
178
- root.style.minHeight = "1px";
209
+ root.style.minHeight = "100%";
179
210
  root.style.boxSizing = "border-box";
180
- root.style.paddingInline = `${IFRAME_VIEWPORT_GUTTER}px`;
181
211
  root.style.overflow = "visible";
182
- setIframeRoot(root);
183
212
  }, []);
184
213
 
185
214
  useEffect(() => {
186
- if (!iframeRoot) return;
215
+ const root = iframeRef.current?.contentDocument?.getElementById(
216
+ "preview-iframe-root",
217
+ );
187
218
 
188
- const doc = iframeRoot.ownerDocument;
189
- const frameWindow = doc.defaultView;
219
+ if (!root) return;
190
220
 
191
- const updateHeight = () => {
192
- const rootRect = iframeRoot.getBoundingClientRect();
221
+ root.innerHTML = html;
222
+ }, [html]);
193
223
 
194
- // Include absolutely positioned descendants that may not contribute to
195
- // scrollHeight/offsetHeight (e.g. sliders/popovers in docs previews).
196
- let maxDescendantBottom = rootRect.bottom;
197
- iframeRoot.querySelectorAll("*").forEach((node) => {
198
- const nodeRect = node.getBoundingClientRect();
199
- if (nodeRect.bottom > maxDescendantBottom) {
200
- maxDescendantBottom = nodeRect.bottom;
201
- }
202
- });
224
+ useEffect(() => {
225
+ const iframe = iframeRef.current;
226
+ const doc = iframe?.contentDocument;
227
+ if (!doc || !isIframeScriptsReady) return;
203
228
 
204
- const nextHeight = Math.max(
205
- Math.ceil(iframeRoot.getBoundingClientRect().height),
206
- iframeRoot.scrollHeight,
207
- Math.ceil(maxDescendantBottom - rootRect.top),
208
- minHeight,
209
- 1,
210
- );
211
- // Update only when there is a meaningful delta to avoid feedback loops.
212
- setHeight((prevHeight) =>
213
- Math.abs(prevHeight - nextHeight) <= 1 ? prevHeight : nextHeight,
214
- );
215
- };
229
+ const frameWindow = doc.defaultView as
230
+ | (Window & {
231
+ ODS?: {
232
+ initModulesWithin?: (container: ParentNode) => void;
233
+ };
234
+ })
235
+ | null;
216
236
 
217
- updateHeight();
218
-
219
- const rafId = frameWindow?.requestAnimationFrame(updateHeight);
220
- const timeoutId = frameWindow?.setTimeout(updateHeight, 100);
221
- const resizeObserver =
222
- typeof ResizeObserver !== "undefined"
223
- ? new ResizeObserver(updateHeight)
224
- : null;
225
- resizeObserver?.observe(iframeRoot);
226
-
227
- const mutationObserver =
228
- typeof MutationObserver !== "undefined"
229
- ? new MutationObserver(updateHeight)
230
- : null;
231
- mutationObserver?.observe(iframeRoot, {
232
- subtree: true,
233
- childList: true,
234
- attributes: true,
235
- characterData: true,
236
- });
237
+ const init = () => {
238
+ frameWindow?.ODS?.initModulesWithin?.(doc);
239
+ };
237
240
 
238
- frameWindow?.addEventListener("resize", updateHeight);
241
+ const rafId = frameWindow?.requestAnimationFrame(init);
242
+ const timeoutId = frameWindow?.setTimeout(init, 0);
239
243
 
240
244
  return () => {
241
245
  if (typeof rafId === "number" && frameWindow) {
@@ -244,22 +248,16 @@ const PreviewViewportFrame: React.FC<PreviewViewportFrameProps> = ({
244
248
  if (typeof timeoutId === "number" && frameWindow) {
245
249
  frameWindow.clearTimeout(timeoutId);
246
250
  }
247
- resizeObserver?.disconnect();
248
- mutationObserver?.disconnect();
249
- frameWindow?.removeEventListener("resize", updateHeight);
250
251
  };
251
- }, [children, iframeRoot, minHeight]);
252
+ }, [html, isIframeScriptsReady]);
252
253
 
253
254
  return (
254
- <>
255
- <iframe
256
- ref={iframeRef}
257
- title="Preview viewport"
258
- className={cx("previewViewportFrame", className)}
259
- style={{ height }}
260
- />
261
- {iframeRoot && createPortal(children, iframeRoot)}
262
- </>
255
+ <iframe
256
+ ref={iframeRef}
257
+ title="Preview viewport"
258
+ className={cx("previewViewportFrame", className, frameClassName)}
259
+ style={{ height }}
260
+ />
263
261
  );
264
262
  };
265
263
 
@@ -270,6 +268,7 @@ interface PreviewProps {
270
268
  bgThemeExcludedColors?: string[];
271
269
  children?: ReactNode | ((context: PreviewRenderContext) => ReactNode);
272
270
  className?: string;
271
+ preserveDividerSpacing?: boolean;
273
272
  code?: ReactNode | string | ((context: PreviewRenderContext) => ReactNode);
274
273
  codeJSXOptions?: Record<string, any>;
275
274
  codeTypes?: string[];
@@ -344,6 +343,7 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
344
343
  bgThemeExcludedColors = [],
345
344
  children,
346
345
  className,
346
+ preserveDividerSpacing = false,
347
347
  code,
348
348
  codeJSXOptions,
349
349
  codeTypes = ["html", "jsx"],
@@ -360,6 +360,8 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
360
360
  const [isCodeShown, setIsCodeShown] = useState(false);
361
361
  const [isFullscreen, setIsFullscreen] = useState(false);
362
362
  const [isDarkMode, setIsDarkMode] = useState(isDark);
363
+ const [iframeMarkup, setIframeMarkup] = useState(html || "");
364
+ const iframeMarkupSourceRef = useRef<HTMLDivElement | null>(null);
363
365
  // Ensure unique dropdown id per Preview instance to avoid id collisions (SSR-safe)
364
366
  const backgroundDropdownId = `background-select-${useId()}`;
365
367
  const breakpointDropdownId = `breakpoint-select-${useId()}`;
@@ -457,6 +459,9 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
457
459
  const wrapperClasses = cx("previewWrapper", {
458
460
  isFullscreen: isFullscreen,
459
461
  });
462
+ const liveCardClasses = cx(classes, {
463
+ "preview--preserve-divider-spacing": preserveDividerSpacing,
464
+ });
460
465
 
461
466
  const actions: ReactNode[] = [];
462
467
 
@@ -628,16 +633,35 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
628
633
  // Use toCode for HTML rendering
629
634
  const codeForHTML = codeContent;
630
635
 
636
+ useEffect(() => {
637
+ if (html) {
638
+ setIframeMarkup(html);
639
+ return;
640
+ }
641
+
642
+ if (iframeMarkupSourceRef.current) {
643
+ setIframeMarkup(iframeMarkupSourceRef.current.innerHTML);
644
+ }
645
+ }, [
646
+ childrenToRender,
647
+ html,
648
+ isDarkMode,
649
+ previewBackground,
650
+ previewBreakpoint,
651
+ ]);
652
+
631
653
  const previewViewportWidth = disableBreakpoints
632
654
  ? null
633
655
  : previewBreakpoint.width;
634
- const previewViewportMinHeight = getScreenLikeMinHeight(previewViewportWidth);
656
+ const previewViewportHeight = disableBreakpoints
657
+ ? null
658
+ : previewBreakpoint.height;
635
659
  const shouldUseIframeViewport = previewViewportWidth !== null;
636
660
 
637
661
  return (
638
662
  <div className={wrapperClasses}>
639
663
  <PreviewTitleBar actions={actions} />
640
- <Card className={classes} {...other}>
664
+ <Card className={liveCardClasses} {...other}>
641
665
  <div
642
666
  className={cx("previewLiveViewportArea", {
643
667
  hasFixedViewport: shouldUseIframeViewport,
@@ -657,13 +681,23 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
657
681
  {!shouldUseIframeViewport ? (
658
682
  <div className="previewLive">{toRender}</div>
659
683
  ) : (
660
- <PreviewViewportFrame
661
- key={previewBreakpoint.key}
662
- className={themeClass}
663
- minHeight={previewViewportMinHeight}
664
- >
665
- <div className="previewLive">{toRender}</div>
666
- </PreviewViewportFrame>
684
+ <>
685
+ <div
686
+ ref={iframeMarkupSourceRef}
687
+ hidden
688
+ aria-hidden="true"
689
+ style={{ display: "none" }}
690
+ >
691
+ <div className="previewLive">{toRender}</div>
692
+ </div>
693
+ <PreviewViewportFrame
694
+ key={previewBreakpoint.key}
695
+ className={themeClass}
696
+ frameClassName={previewBreakpoint.frameClassName}
697
+ height={previewViewportHeight ?? 1}
698
+ html={iframeMarkup}
699
+ />
700
+ </>
667
701
  )}
668
702
  </div>
669
703
  </div>
@@ -1,4 +1,6 @@
1
+ @use "../../../styles/tokens/breakpoint";
1
2
  @use "../../../styles/tokens/color" as *;
3
+ @use "../../../styles/tokens/space";
2
4
 
3
5
  @layer components {
4
6
  // ===== PREVIEW GRID STYLES =====
@@ -61,6 +63,18 @@
61
63
  overflow: visible !important;
62
64
  }
63
65
 
66
+ .card.preview--preserve-divider-spacing {
67
+ .divider {
68
+ padding-top: space.get();
69
+ padding-bottom: space.get();
70
+
71
+ @include breakpoint.get("md") {
72
+ padding-top: space.get("large");
73
+ padding-bottom: space.get("large");
74
+ }
75
+ }
76
+ }
77
+
64
78
  .previewLive {
65
79
  transition: all 200ms ease-in-out;
66
80
  overflow: visible;
@@ -82,13 +96,6 @@
82
96
 
83
97
  .previewLiveViewport {
84
98
  background: var(--color-background-primary);
85
- border: 1px solid
86
- color-mix(in srgb, var(--color-border-strong), transparent 25%);
87
- box-shadow:
88
- 0 0 0 1px
89
- color-mix(in srgb, var(--color-border-strong), transparent 60%),
90
- 0 12px 28px
91
- color-mix(in srgb, var(--color-text-default), transparent 88%);
92
99
  }
93
100
  }
94
101
  }
@@ -106,8 +113,7 @@
106
113
  width: 100%;
107
114
  border: 0;
108
115
  display: block;
109
- background: transparent;
110
- padding-top: 16px;
116
+ background: var(--color-background-primary);
111
117
  }
112
118
 
113
119
  .selectWrapper {
@@ -3,9 +3,14 @@ import React from "react";
3
3
  import { Grid, GridCol } from "../Grid";
4
4
  import { Image } from "../Image";
5
5
 
6
+ interface PromoBannerImageSrc {
7
+ src: string;
8
+ srcSet?: string;
9
+ }
10
+
6
11
  interface PromoBannerProps {
7
12
  className?: string;
8
- image?: string;
13
+ image?: string | PromoBannerImageSrc;
9
14
  alignImage?: "bottom" | "top";
10
15
  /** Remove top padding from item on sm and smaller breakpoints */
11
16
  flushTopSmDown?: boolean;
@@ -21,6 +26,15 @@ interface PromoBannerProps {
21
26
  }
22
27
 
23
28
  const CLASS_ROOT = "promo-banner";
29
+
30
+ const getImageProps = (image?: string | PromoBannerImageSrc) => {
31
+ if (!image) return null;
32
+
33
+ return typeof image === "string"
34
+ ? { src: image }
35
+ : { src: image.src, srcSet: image.srcSet };
36
+ };
37
+
24
38
  export const PromoBanner = ({
25
39
  image,
26
40
  className,
@@ -35,16 +49,17 @@ export const PromoBanner = ({
35
49
  imageFullWidth,
36
50
  ...other
37
51
  }: PromoBannerProps) => {
52
+ const imageProps = getImageProps(image);
38
53
  const classes = cx(CLASS_ROOT, className, {
39
54
  [`${CLASS_ROOT}--flush-top-sm-down`]: flushTopSmDown,
40
55
  [`${CLASS_ROOT}--flush-bottom-sm-down`]: flushBottomSmDown,
41
- [`${CLASS_ROOT}--reverse`]: variant === "reverse" && !!image,
56
+ [`${CLASS_ROOT}--reverse`]: variant === "reverse" && !!imageProps?.src,
42
57
  [`${CLASS_ROOT}--vertical`]: variant === "vertical",
43
58
  });
44
59
  return (
45
60
  <div className={classes} {...other}>
46
61
  <Grid columnGapSize="large">
47
- {variant === "reverse" && image && (
62
+ {variant === "reverse" && imageProps?.src && (
48
63
  <GridCol
49
64
  size={{ xs: 12, md: 6 }}
50
65
  className={cx(
@@ -54,7 +69,8 @@ export const PromoBanner = ({
54
69
  )}
55
70
  >
56
71
  <Image
57
- src={image}
72
+ src={imageProps.src}
73
+ srcSet={imageProps.srcSet}
58
74
  alt="Obrázok"
59
75
  className={cx("mb-none", { fullwidth: imageFullWidth })}
60
76
  />
@@ -65,7 +81,7 @@ export const PromoBanner = ({
65
81
  size={
66
82
  variant === "vertical"
67
83
  ? { xs: 12, sm: 10, md: 8, lg: 6 }
68
- : { xs: 12, md: image ? 6 : 7 }
84
+ : { xs: 12, md: imageProps?.src ? 6 : 7 }
69
85
  }
70
86
  className={cx(
71
87
  `${CLASS_ROOT}__item`,
@@ -79,7 +95,7 @@ export const PromoBanner = ({
79
95
  {children}
80
96
  </GridCol>
81
97
 
82
- {image && variant !== "reverse" && (
98
+ {imageProps?.src && variant !== "reverse" && (
83
99
  <GridCol
84
100
  size={variant === "vertical" ? { xs: 12 } : { xs: 12, md: 6 }}
85
101
  className={cx(
@@ -95,7 +111,8 @@ export const PromoBanner = ({
95
111
  )}
96
112
  >
97
113
  <Image
98
- src={image}
114
+ src={imageProps.src}
115
+ srcSet={imageProps.srcSet}
99
116
  alt="Obrázok"
100
117
  className={cx("mb-none", { fullwidth: imageFullWidth })}
101
118
  />
@@ -315,6 +315,37 @@ describe("PromoBanner", () => {
315
315
  expect(image).toHaveAttribute("src", "https://example.com/test.jpg");
316
316
  });
317
317
 
318
+ it("renders image from object src when image object is provided", () => {
319
+ render(
320
+ <PromoBanner
321
+ image={{ src: "https://example.com/test.jpg", srcSet: "test 1x" }}
322
+ >
323
+ Content
324
+ </PromoBanner>,
325
+ );
326
+ const image = screen.getByRole("img");
327
+ expect(image).toHaveAttribute("src", "https://example.com/test.jpg");
328
+ });
329
+
330
+ it("passes srcSet to Image when image object is provided", () => {
331
+ render(
332
+ <PromoBanner
333
+ image={{
334
+ src: "https://example.com/test.jpg",
335
+ srcSet:
336
+ "https://example.com/test.jpg 1x, https://example.com/test@2x.jpg 2x",
337
+ }}
338
+ >
339
+ Content
340
+ </PromoBanner>,
341
+ );
342
+ const image = screen.getByRole("img");
343
+ expect(image).toHaveAttribute(
344
+ "srcSet",
345
+ "https://example.com/test.jpg 1x, https://example.com/test@2x.jpg 2x",
346
+ );
347
+ });
348
+
318
349
  it("applies default alt text", () => {
319
350
  render(<PromoBanner image="test.jpg">Content</PromoBanner>);
320
351
  const image = screen.getByRole("img");
@@ -0,0 +1,52 @@
1
+ import cx from "classnames";
2
+ import React from "react";
3
+
4
+ import { Icon } from "../Icon";
5
+
6
+ export interface StarRatingProps
7
+ extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
8
+ /** Extra class name for root element. */
9
+ className?: string;
10
+ /** Rating value. Rounded to nearest whole star. */
11
+ rating: number;
12
+ }
13
+
14
+ const CLASS_ROOT = "star-rating";
15
+ const STAR_COUNT = 5;
16
+ const FILLED_ICON_NAME = "star-favourite";
17
+ const EMPTY_ICON_NAME = "star-empty-thin";
18
+
19
+ const clampRating = (rating: number) =>
20
+ Math.max(0, Math.min(Math.round(rating), STAR_COUNT));
21
+
22
+ const StarRating: React.FC<StarRatingProps> = ({
23
+ className,
24
+ rating,
25
+ ...other
26
+ }) => {
27
+ const filledStars = clampRating(rating);
28
+
29
+ return (
30
+ <span className={cx(CLASS_ROOT, className)} {...other}>
31
+ {Array.from({ length: STAR_COUNT }, (_, index) => {
32
+ const isFilled = index < filledStars;
33
+
34
+ return (
35
+ <Icon
36
+ key={`${isFilled ? FILLED_ICON_NAME : EMPTY_ICON_NAME}-${index}`}
37
+ className={cx(`${CLASS_ROOT}__icon`, {
38
+ [`${CLASS_ROOT}__icon--filled`]: isFilled,
39
+ [`${CLASS_ROOT}__icon--empty`]: !isFilled,
40
+ })}
41
+ name={isFilled ? FILLED_ICON_NAME : EMPTY_ICON_NAME}
42
+ size="large"
43
+ />
44
+ );
45
+ })}
46
+ </span>
47
+ );
48
+ };
49
+
50
+ StarRating.displayName = "StarRating";
51
+
52
+ export { StarRating };
@@ -0,0 +1 @@
1
+ export { StarRating } from "./StarRating";
@@ -0,0 +1,28 @@
1
+ @use "../../../styles/tools/convert";
2
+ @use "../../../styles/tokens/space";
3
+ @use "../../../styles/tokens/breakpoint";
4
+
5
+ @layer components {
6
+ .star-rating {
7
+ display: flex;
8
+ margin-bottom: space.get("large");
9
+ width: fit-content;
10
+ gap: convert.to-rem(5px);
11
+
12
+ &__icon--filled {
13
+ color: var(--color-icon-brand);
14
+ }
15
+
16
+ &__icon--empty {
17
+ color: var(--color-icon-disabled);
18
+ }
19
+
20
+ &__icon {
21
+ --icon-size: #{convert.to-rem(24px)};
22
+
23
+ @include breakpoint.get(md) {
24
+ --icon-size: #{convert.to-rem(32px)};
25
+ }
26
+ }
27
+ }
28
+ }