@revenuecat/purchases-ui-js 3.11.3 → 3.11.5

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 (49) hide show
  1. package/dist/components/button/ButtonNode.stories.svelte +23 -0
  2. package/dist/components/button/ButtonNode.svelte +114 -2
  3. package/dist/components/button/ButtonNodeTestWrapper.svelte +10 -0
  4. package/dist/components/carousel/Carousel.stories.svelte +19 -0
  5. package/dist/components/carousel/Carousel.svelte +98 -52
  6. package/dist/components/carousel/PageControl.svelte +12 -11
  7. package/dist/components/countdown/Countdown.stories.svelte +101 -71
  8. package/dist/components/icon/Icon.stories.svelte +22 -0
  9. package/dist/components/icon/Icon.svelte +2 -2
  10. package/dist/components/image/Image.stories.svelte +19 -0
  11. package/dist/components/image/Image.svelte +8 -2
  12. package/dist/components/input-text/InputText.svelte +72 -32
  13. package/dist/components/options/InputMultipleChoice.stories.svelte +9 -3
  14. package/dist/components/options/InputOption.stories.svelte +13 -1
  15. package/dist/components/options/InputSingleChoice.stories.svelte +9 -3
  16. package/dist/components/package/Package.stories.svelte +7 -3
  17. package/dist/components/package/Package.svelte +25 -3
  18. package/dist/components/paywall/Paywall.svelte +85 -6
  19. package/dist/components/paywall/Paywall.svelte.d.ts +2 -0
  20. package/dist/components/purchase-button/PurchaseButton.stories.svelte +13 -1
  21. package/dist/components/purchase-button/PurchaseButton.svelte +62 -3
  22. package/dist/components/skeleton-loader/SkeletonLoader.svelte +18 -6
  23. package/dist/components/stack/Stack.stories.svelte +9 -3
  24. package/dist/components/stack/Stack.svelte +2 -2
  25. package/dist/components/tabs/TabControlButton.svelte +1 -1
  26. package/dist/components/tabs/TabControlToggle.svelte +1 -1
  27. package/dist/components/tabs/Tabs.stories.svelte +65 -3
  28. package/dist/components/tabs/Tabs.svelte +39 -12
  29. package/dist/components/tabs/tabs-context.d.ts +1 -1
  30. package/dist/components/text/TextNode.svelte +45 -2
  31. package/dist/components/video/Video.stories.svelte +9 -2
  32. package/dist/components/video/Video.svelte +8 -2
  33. package/dist/components/workflows/Screen.svelte +4 -0
  34. package/dist/components/workflows/Screen.svelte.d.ts +2 -0
  35. package/dist/stores/paywall.d.ts +4 -0
  36. package/dist/stories/paywall-decorator.js +2 -0
  37. package/dist/types/component.d.ts +2 -2
  38. package/dist/types/components/purchase-button.d.ts +24 -0
  39. package/dist/types/overrides.d.ts +8 -2
  40. package/dist/types/paywall-component-interaction.d.ts +50 -0
  41. package/dist/types/paywall-component-interaction.js +1 -0
  42. package/dist/types/variables.d.ts +1 -0
  43. package/dist/types.d.ts +3 -2
  44. package/dist/utils/background-utils.js +4 -1
  45. package/dist/utils/base-utils.d.ts +9 -1
  46. package/dist/utils/base-utils.js +34 -3
  47. package/dist/utils/style-utils.d.ts +4 -1
  48. package/dist/utils/style-utils.js +96 -27
  49. package/package.json +1 -1
@@ -76,6 +76,7 @@
76
76
  id3: "Navigate to",
77
77
  id4: "URL navigation",
78
78
  id5: "Workflow Action",
79
+ id6: "Border",
79
80
  },
80
81
  },
81
82
  }),
@@ -186,3 +187,25 @@
186
187
  },
187
188
  }}
188
189
  />
190
+
191
+ <Story
192
+ name="Border"
193
+ args={{
194
+ stack: {
195
+ ...stackProps(6),
196
+ border: {
197
+ width: 10,
198
+ color: {
199
+ light: {
200
+ type: "linear",
201
+ degrees: 90,
202
+ points: [
203
+ { color: "#000000", percent: 0 },
204
+ { color: "#FF0000", percent: 100 },
205
+ ],
206
+ },
207
+ },
208
+ },
209
+ },
210
+ }}
211
+ />
@@ -1,9 +1,14 @@
1
1
  <script lang="ts">
2
2
  import Stack from "../stack/Stack.svelte";
3
3
  import { getInputValidationContext } from "../../stores/inputValidation";
4
+ import { getLocalizationContext } from "../../stores/localization";
4
5
  import { getPaywallContext } from "../../stores/paywall";
5
6
  import { getSelectedStateContext } from "../../stores/selected";
6
7
  import type { ButtonProps } from "../../types/components/button";
8
+ import type {
9
+ ButtonInteractionData,
10
+ PackageSelectionSheetInteractionData,
11
+ } from "../../types/paywall-component-interaction";
7
12
  import { getActiveStateProps } from "../../utils/style-utils";
8
13
  import { readable } from "svelte/store";
9
14
 
@@ -17,8 +22,14 @@
17
22
  };
18
23
  });
19
24
 
20
- const { onButtonAction, hideBackButtons } = getPaywallContext();
25
+ const {
26
+ emitComponentInteraction,
27
+ onButtonAction,
28
+ hideBackButtons,
29
+ selectedPackageId,
30
+ } = getPaywallContext();
21
31
  const validationContext = getInputValidationContext();
32
+ const { getLocalizedString } = getLocalizationContext();
22
33
 
23
34
  // Reactive store for input satisfaction (defaults to true if no validation context)
24
35
  const inputSatisfied = validationContext?.isSatisfied ?? readable(true);
@@ -29,10 +40,111 @@
29
40
  !$inputSatisfied,
30
41
  );
31
42
 
43
+ const getButtonInteractionData = (): ButtonInteractionData => {
44
+ switch (action.type) {
45
+ case "workflow":
46
+ return {
47
+ componentType: "button",
48
+ componentName: props.name,
49
+ componentValue: "workflow",
50
+ };
51
+ case "complete_workflow": {
52
+ const url = getLocalizedString(action.url.url_lid);
53
+ return {
54
+ componentType: "button",
55
+ componentName: props.name,
56
+ componentValue: "navigate_to_url",
57
+ ...(url ? { componentURL: url } : {}),
58
+ };
59
+ }
60
+ case "navigate_back":
61
+ return {
62
+ componentType: "button",
63
+ componentName: props.name,
64
+ componentValue: "navigate_back",
65
+ };
66
+ case "restore_purchases":
67
+ return {
68
+ componentType: "button",
69
+ componentName: props.name,
70
+ componentValue: "restore_purchases",
71
+ };
72
+ }
73
+
74
+ switch (action.destination) {
75
+ case "customer_center":
76
+ return {
77
+ componentType: "button",
78
+ componentName: props.name,
79
+ componentValue: "navigate_to_customer_center",
80
+ };
81
+ case "screen_redirect":
82
+ return {
83
+ componentType: "button",
84
+ componentName: props.name,
85
+ componentValue: "screen_redirect",
86
+ };
87
+ case "privacy_policy":
88
+ case "terms":
89
+ case "url": {
90
+ const url = getLocalizedString(action.url.url_lid);
91
+ return {
92
+ componentType: "button",
93
+ componentName: props.name,
94
+ componentValue:
95
+ action.destination === "privacy_policy"
96
+ ? "navigate_to_privacy_policy"
97
+ : action.destination === "terms"
98
+ ? "navigate_to_terms"
99
+ : "navigate_to_url",
100
+ ...(url ? { componentURL: url } : {}),
101
+ };
102
+ }
103
+ case "sheet":
104
+ // Unreachable: sheet open/close paths short-circuit in onclick above
105
+ // and emit via getSheetOpenInteractionData() / Paywall.onButtonAction.
106
+ // Kept to preserve switch exhaustiveness over Action["destination"].
107
+ throw new Error(
108
+ "Sheet actions must be handled by the sheet branches in onclick",
109
+ );
110
+ case "offer_code":
111
+ return {
112
+ componentType: "button",
113
+ componentName: props.name,
114
+ componentValue: "navigate_to_offer_code",
115
+ };
116
+ case "web_paywall_link":
117
+ return {
118
+ componentType: "button",
119
+ componentName: props.name,
120
+ componentValue: "navigate_to_web_paywall_link",
121
+ };
122
+ }
123
+ };
124
+
125
+ const getSheetOpenInteractionData =
126
+ (): PackageSelectionSheetInteractionData => ({
127
+ componentType: "package_selection_sheet",
128
+ componentName: action.sheet?.name ?? props.name,
129
+ componentValue: "open",
130
+ currentPackageId: $selectedPackageId,
131
+ });
132
+
32
133
  const onclick = () => {
33
134
  if (isDisabled) return;
34
135
  const actionId = props.triggers?.on_press;
35
- onButtonAction(props.action, actionId);
136
+ const isSheetAction =
137
+ action.type === "navigate_to" && action.destination === "sheet";
138
+ const isSheetOpenAction = isSheetAction && action.sheet != null;
139
+ const isSheetCloseAction = isSheetAction && action.sheet == null;
140
+
141
+ if (isSheetOpenAction) {
142
+ emitComponentInteraction(getSheetOpenInteractionData());
143
+ } else if (!isSheetCloseAction) {
144
+ emitComponentInteraction(getButtonInteractionData());
145
+ }
146
+
147
+ onButtonAction(action, actionId);
36
148
  };
37
149
 
38
150
  const visible = $derived.by(() => {
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { setLocalizationContext } from "../../stores/localization";
2
3
  import { setPaywallContext } from "../../stores/paywall";
3
4
  import { writable, readable } from "svelte/store";
4
5
  import ButtonNode from "./ButtonNode.svelte";
@@ -10,12 +11,21 @@
10
11
 
11
12
  const { hideBackButtons = false, ...buttonProps }: Props = $props();
12
13
 
14
+ setLocalizationContext(() => ({
15
+ defaultLocale: "en_US",
16
+ localizations: {
17
+ en_US: {},
18
+ },
19
+ }));
20
+
13
21
  setPaywallContext({
22
+ defaultPackageId: undefined,
14
23
  selectedPackageId: writable(undefined),
15
24
  variablesPerPackage: readable(undefined),
16
25
  baseVariables: readable(undefined),
17
26
  infoPerPackage: readable(undefined),
18
27
  onPurchase: () => {},
28
+ emitComponentInteraction: () => {},
19
29
  onButtonAction: () => {},
20
30
  uiConfig: {} as never,
21
31
  hideBackButtons,
@@ -1037,3 +1037,22 @@
1037
1037
  ],
1038
1038
  }}
1039
1039
  />
1040
+
1041
+ <Story
1042
+ name="Border"
1043
+ args={{
1044
+ border: {
1045
+ width: 10,
1046
+ color: {
1047
+ light: {
1048
+ type: "linear",
1049
+ degrees: 90,
1050
+ points: [
1051
+ { color: "#000000", percent: 0 },
1052
+ { color: "#FF0000", percent: 100 },
1053
+ ],
1054
+ },
1055
+ },
1056
+ },
1057
+ }}
1058
+ />
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getColorModeContext } from "../../stores/color-mode";
3
+ import { getPaywallContext } from "../../stores/paywall";
3
4
  import { getSelectedStateContext } from "../../stores/selected";
4
5
  import type { CarouselProps } from "../../types/components/carousel";
5
6
  import { mapBackground } from "../../utils/background-utils";
@@ -43,13 +44,14 @@
43
44
 
44
45
  const getColorMode = getColorModeContext();
45
46
  const colorMode = $derived(getColorMode());
47
+ const { emitComponentInteraction } = getPaywallContext();
46
48
 
47
49
  const carouselStyle = $derived(
48
50
  css({
49
51
  margin: mapSpacing(margin),
50
52
  padding: mapSpacing(padding),
51
53
  ...mapBackground(colorMode, null, background),
52
- border: mapBorder(colorMode, border),
54
+ ...mapBorder(colorMode, border),
53
55
  "border-radius": mapBorderRadius(shape),
54
56
  "box-shadow": mapShadow(colorMode, shadow),
55
57
  "transition-duration": `${auto_advance?.ms_transition_time ?? 0}ms`,
@@ -102,6 +104,23 @@
102
104
  let carouselWidth = $state(0);
103
105
  let pageOffset = $derived(2 * page_peek + page_spacing);
104
106
  let pageWidth = $derived(carouselWidth - pageOffset);
107
+ let gestureStartPage = $state<number | null>(null);
108
+
109
+ function getDefaultPageIndex() {
110
+ return pages.at(initial_page_index) !== undefined ? initial_page_index : 0;
111
+ }
112
+
113
+ function normalizePageIndex(index: number) {
114
+ if (pages.length === 0) {
115
+ return 0;
116
+ }
117
+
118
+ return ((index % pages.length) + pages.length) % pages.length;
119
+ }
120
+
121
+ function getPageContextName(index: number) {
122
+ return pages[index]?.name;
123
+ }
105
124
 
106
125
  function startTransition(withDelay: boolean = true, durationMs?: number) {
107
126
  if (slider === null) {
@@ -154,8 +173,7 @@
154
173
  }
155
174
 
156
175
  onMount(() => {
157
- const initialPage =
158
- pages.at(initial_page_index) !== undefined ? initial_page_index : 0;
176
+ const initialPage = getDefaultPageIndex();
159
177
  currentPage = initialPage;
160
178
 
161
179
  stopTransition();
@@ -185,6 +203,7 @@
185
203
  const target = event.currentTarget as HTMLElement;
186
204
  target.setPointerCapture(event.pointerId);
187
205
  startTranslation = getTranslation(slider);
206
+ gestureStartPage = currentPage;
188
207
  }
189
208
 
190
209
  function onpointerup(event: PointerEvent) {
@@ -193,6 +212,22 @@
193
212
 
194
213
  const translation = getTranslation(slider);
195
214
  const page = getPageFromTranslation(translation);
215
+ const destinationPage = normalizePageIndex(page);
216
+
217
+ if (gestureStartPage !== null && gestureStartPage !== destinationPage) {
218
+ emitComponentInteraction({
219
+ componentType: "carousel",
220
+ componentName: props.name,
221
+ componentValue: destinationPage.toString(),
222
+ originIndex: gestureStartPage,
223
+ destinationIndex: destinationPage,
224
+ originContextName: getPageContextName(gestureStartPage),
225
+ destinationContextName: getPageContextName(destinationPage),
226
+ defaultIndex: getDefaultPageIndex(),
227
+ });
228
+ }
229
+
230
+ gestureStartPage = null;
196
231
  startTransition(false, 500);
197
232
  moveTo(page);
198
233
  }
@@ -217,56 +252,58 @@
217
252
  }
218
253
  </script>
219
254
 
220
- <div class="carousel" style={carouselStyle}>
221
- <div
222
- bind:this={slider}
223
- bind:clientWidth={carouselWidth}
224
- {ontransitionend}
225
- {onpointerdown}
226
- {onpointerup}
227
- {onpointermove}
228
- class="slider"
229
- style={sliderStyle}
230
- role="region"
231
- aria-label="Feature carousel"
232
- aria-roledescription="carousel"
233
- aria-live="polite"
234
- >
235
- {#each prevPages as index}
236
- <CarouselPage
237
- page={pages[index]}
238
- {index}
239
- pages={pages.length}
240
- selected={currentPage === index}
241
- hidden
255
+ <div class="carousel rc-gradient-border" style={carouselStyle}>
256
+ <div class="carousel-clip">
257
+ <div
258
+ bind:this={slider}
259
+ bind:clientWidth={carouselWidth}
260
+ {ontransitionend}
261
+ {onpointerdown}
262
+ {onpointerup}
263
+ {onpointermove}
264
+ class="slider"
265
+ style={sliderStyle}
266
+ role="region"
267
+ aria-label="Feature carousel"
268
+ aria-roledescription="carousel"
269
+ aria-live="polite"
270
+ >
271
+ {#each prevPages as index}
272
+ <CarouselPage
273
+ page={pages[index]}
274
+ {index}
275
+ pages={pages.length}
276
+ selected={currentPage === index}
277
+ hidden
278
+ />
279
+ {/each}
280
+ {#each pages as page, index}
281
+ <CarouselPage
282
+ {page}
283
+ {index}
284
+ pages={pages.length}
285
+ selected={currentPage === index}
286
+ />
287
+ {/each}
288
+ {#each nextPages as index}
289
+ <CarouselPage
290
+ page={pages[index]}
291
+ {index}
292
+ pages={pages.length}
293
+ selected={currentPage === index}
294
+ hidden
295
+ />
296
+ {/each}
297
+ </div>
298
+
299
+ {#if page_control != null}
300
+ <PageControl
301
+ {...page_control}
302
+ length={pages.length}
303
+ selected={currentPage}
242
304
  />
243
- {/each}
244
- {#each pages as page, index}
245
- <CarouselPage
246
- {page}
247
- {index}
248
- pages={pages.length}
249
- selected={currentPage === index}
250
- />
251
- {/each}
252
- {#each nextPages as index}
253
- <CarouselPage
254
- page={pages[index]}
255
- {index}
256
- pages={pages.length}
257
- selected={currentPage === index}
258
- hidden
259
- />
260
- {/each}
305
+ {/if}
261
306
  </div>
262
-
263
- {#if page_control != null}
264
- <PageControl
265
- {...page_control}
266
- length={pages.length}
267
- selected={currentPage}
268
- />
269
- {/if}
270
307
  </div>
271
308
 
272
309
  <style>
@@ -274,10 +311,19 @@
274
311
  max-width: 100%;
275
312
  display: flex;
276
313
  flex-direction: column;
277
- overflow: hidden;
278
314
  flex-shrink: 0;
279
315
  }
280
316
 
317
+ .carousel-clip {
318
+ display: flex;
319
+ flex-direction: column;
320
+ flex: 1;
321
+ min-width: 0;
322
+ min-height: 0;
323
+ overflow: hidden;
324
+ border-radius: inherit;
325
+ }
326
+
281
327
  .slider {
282
328
  display: flex;
283
329
  flex-direction: row;
@@ -39,14 +39,15 @@
39
39
  const colorMode = $derived(getColorMode());
40
40
 
41
41
  function mapIndicatorBorder(indicator: PageControlIndicator) {
42
- if (indicator.stroke_width == null || indicator.stroke_color == null) {
43
- return "none";
44
- }
45
-
46
- return mapBorder(colorMode, {
47
- width: indicator.stroke_width,
48
- color: indicator.stroke_color,
49
- });
42
+ return mapBorder(
43
+ colorMode,
44
+ indicator.stroke_width == null || indicator.stroke_color == null
45
+ ? null
46
+ : {
47
+ width: indicator.stroke_width,
48
+ color: indicator.stroke_color,
49
+ },
50
+ );
50
51
  }
51
52
 
52
53
  function mapIndicatorStyle(indicator: PageControlIndicator) {
@@ -54,7 +55,7 @@
54
55
  width: px(indicator.width),
55
56
  height: px(indicator.height),
56
57
  background: mapColor(colorMode, indicator.color),
57
- border: mapIndicatorBorder(indicator),
58
+ ...mapIndicatorBorder(indicator),
58
59
  "border-radius": "9999px",
59
60
  });
60
61
  }
@@ -69,14 +70,14 @@
69
70
  margin: mapSpacing(margin),
70
71
  padding: mapSpacing(padding),
71
72
  ...mapBackground(colorMode, background_color, null),
72
- border: mapBorder(colorMode, border),
73
+ ...mapBorder(colorMode, border),
73
74
  "border-radius": mapBorderRadius(shape),
74
75
  "box-shadow": mapShadow(colorMode, shadow),
75
76
  }),
76
77
  );
77
78
  </script>
78
79
 
79
- <div class="control" {style}>
80
+ <div class="control rc-gradient-border" {style}>
80
81
  {#each { length }, index}
81
82
  <div style={index === selected ? activeStyle : defaultStyle}></div>
82
83
  {/each}
@@ -8,6 +8,7 @@
8
8
  import { variablesDecorator } from "../../stories/variables-decorator";
9
9
  import { DEFAULT_TEXT_COLOR } from "../../utils/constants";
10
10
  import { defineMeta } from "@storybook/addon-svelte-csf";
11
+ import type { StackProps } from "../../types/components/stack";
11
12
 
12
13
  const defaultLocale = Object.keys(localizations)[0];
13
14
 
@@ -33,22 +34,64 @@
33
34
  const pastDate = new Date(frozenDate);
34
35
  pastDate.setHours(pastDate.getHours() - 1);
35
36
 
36
- const { Story } = defineMeta({
37
- title: "Components/Countdown",
38
- component: Countdown,
39
- decorators: [
40
- mockDateDecorator,
41
- componentDecorator(),
42
- localizationDecorator({
43
- defaultLocale,
44
- localizations,
45
- }),
46
- variablesDecorator(undefined),
47
- ],
48
- parameters: {
49
- date: frozenDate,
50
- },
51
- args: {
37
+ const countdownStackProps = () =>
38
+ ({
39
+ type: "stack",
40
+ id: "countdown-stack",
41
+ name: "Countdown Stack",
42
+ components: [
43
+ {
44
+ type: "text",
45
+ id: "countdown-text",
46
+ name: "Countdown Text",
47
+ text_lid: "countdown",
48
+ color: {
49
+ light: { type: "hex", value: DEFAULT_TEXT_COLOR },
50
+ },
51
+ font_size: "heading_xl",
52
+ font_weight: "bold",
53
+ horizontal_alignment: "center",
54
+ size: {
55
+ width: { type: "fit" },
56
+ height: { type: "fit" },
57
+ },
58
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
59
+ padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
60
+ background_color: null,
61
+ },
62
+ ],
63
+ size: {
64
+ width: { type: "fit" },
65
+ height: { type: "fit" },
66
+ },
67
+ dimension: {
68
+ type: "vertical",
69
+ alignment: "center",
70
+ distribution: "center",
71
+ },
72
+ spacing: 8,
73
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
74
+ padding: { top: 16, trailing: 16, bottom: 16, leading: 16 },
75
+ background_color: {
76
+ light: { type: "hex", value: "#F0F0F0" },
77
+ },
78
+ background: null,
79
+ border: null,
80
+ shape: {
81
+ type: "rectangle",
82
+ corners: {
83
+ top_leading: 8,
84
+ top_trailing: 8,
85
+ bottom_leading: 8,
86
+ bottom_trailing: 8,
87
+ },
88
+ },
89
+ shadow: null,
90
+ badge: null,
91
+ }) satisfies StackProps;
92
+
93
+ const countdownProps = () =>
94
+ ({
52
95
  type: "countdown",
53
96
  id: "countdown",
54
97
  name: "Countdown",
@@ -56,60 +99,7 @@
56
99
  type: "date",
57
100
  date: futureDate.toISOString(),
58
101
  },
59
- countdown_stack: {
60
- type: "stack",
61
- id: "countdown-stack",
62
- name: "Countdown Stack",
63
- components: [
64
- {
65
- type: "text",
66
- id: "countdown-text",
67
- name: "Countdown Text",
68
- text_lid: "countdown",
69
- color: {
70
- light: { type: "hex", value: DEFAULT_TEXT_COLOR },
71
- },
72
- font_size: "heading_xl",
73
- font_weight: "bold",
74
- horizontal_alignment: "center",
75
- size: {
76
- width: { type: "fit" },
77
- height: { type: "fit" },
78
- },
79
- margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
80
- padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
81
- background_color: null,
82
- },
83
- ],
84
- size: {
85
- width: { type: "fit" },
86
- height: { type: "fit" },
87
- },
88
- dimension: {
89
- type: "vertical",
90
- alignment: "center",
91
- distribution: "center",
92
- },
93
- spacing: 8,
94
- margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
95
- padding: { top: 16, trailing: 16, bottom: 16, leading: 16 },
96
- background_color: {
97
- light: { type: "hex", value: "#F0F0F0" },
98
- },
99
- background: null,
100
- border: null,
101
- shape: {
102
- type: "rectangle",
103
- corners: {
104
- top_leading: 8,
105
- top_trailing: 8,
106
- bottom_leading: 8,
107
- bottom_trailing: 8,
108
- },
109
- },
110
- shadow: null,
111
- badge: null,
112
- },
102
+ countdown_stack: countdownStackProps(),
113
103
  end_stack: {
114
104
  type: "stack",
115
105
  id: "end-stack",
@@ -164,7 +154,24 @@
164
154
  shadow: null,
165
155
  badge: null,
166
156
  },
167
- } satisfies CountdownProps,
157
+ }) satisfies CountdownProps;
158
+
159
+ const { Story } = defineMeta({
160
+ title: "Components/Countdown",
161
+ component: Countdown,
162
+ decorators: [
163
+ mockDateDecorator,
164
+ componentDecorator(),
165
+ localizationDecorator({
166
+ defaultLocale,
167
+ localizations,
168
+ }),
169
+ variablesDecorator(undefined),
170
+ ],
171
+ parameters: {
172
+ date: frozenDate,
173
+ },
174
+ args: countdownProps(),
168
175
  });
169
176
  </script>
170
177
 
@@ -363,3 +370,26 @@
363
370
  },
364
371
  }}
365
372
  />
373
+
374
+ <Story
375
+ name="Border"
376
+ args={{
377
+ ...countdownProps(),
378
+ countdown_stack: {
379
+ ...countdownStackProps(),
380
+ border: {
381
+ width: 10,
382
+ color: {
383
+ light: {
384
+ type: "linear",
385
+ degrees: 90,
386
+ points: [
387
+ { color: "#000000", percent: 0 },
388
+ { color: "#FF0000", percent: 100 },
389
+ ],
390
+ },
391
+ },
392
+ },
393
+ },
394
+ }}
395
+ />