wineater-bot 4.0.0-beta.7 → 4.1.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.
@@ -0,0 +1,133 @@
1
+ # ADR: Widget Customization System
2
+
3
+ **Date:** 26.11.2025
4
+ **PR:** https://github.com/Wineater/wineater-bot/pull/2
5
+
6
+ ## Context
7
+
8
+ Clients increasingly request widget customization for their specific brands and use cases. Typical requests include:
9
+
10
+ - Changing colors and overall color scheme.
11
+ - Adjusting fonts and typography.
12
+ - Tweaking styles of individual UI elements (buttons, inputs, popups, etc.).
13
+ - Customizing translations and text copy.
14
+
15
+ Previously, such changes were mostly implemented as one‑off, manual style overrides or ad‑hoc modifications in the codebase. This approach has several drawbacks:
16
+
17
+ - Poor scalability when multiple clients require different customizations.
18
+ - High risk of regressions when editing styles for a single client.
19
+ - No clear system for naming, organizing, or reusing styles and components.
20
+ - Limited ability to evolve towards a proper design system and reusable components.
21
+
22
+ We want to move away from manual, one‑by‑one style hacking and instead provide:
23
+
24
+ - A component library that can be configured at the level of components, not individual DOM elements.
25
+ - A token‑based color scheme and shared design primitives.
26
+ - A single configuration entry point where client‑specific overrides can be described declaratively.
27
+
28
+ ## Decision
29
+
30
+ We introduced a **customization system** for the widget with the following key aspects:
31
+
32
+ 1. **Class naming system using BEM**
33
+ - A consistent naming convention was introduced for widget elements based on the BEM methodology.
34
+ - This makes it easier to:
35
+ - Identify and target elements for styling.
36
+ - Maintain styles over time.
37
+ - Reason about component structure and responsibilities.
38
+
39
+ 2. **Centralized client configuration**
40
+ - Client‑specific behavior and appearance are now configured from a single configuration object.
41
+ - The configuration contains:
42
+ - General options that control various parts of the system (e.g. feature toggles and UI behavior flags).
43
+ - A `customCss` field that allows providing custom CSS for a client.
44
+
45
+ 3. **Custom CSS entry point**
46
+ - The `customCss` property is designed for injecting client‑specific style overrides.
47
+ - To increase specificity and avoid conflicts with default styles, all overrides must start from the `#wr-Custom` root selector.
48
+ - Example pattern:
49
+ - `#wr-Custom .wr-Button { /* client-specific overrides */ }`
50
+
51
+ 4. **Direction towards tokens and components**
52
+ - The current implementation is a first step towards:
53
+ - A shared color system expressed via tokens.
54
+ - A reusable set of components that can support variants (e.g. primary/secondary buttons, different input styles).
55
+ - The long‑term goal is to customize **component variants and design tokens** rather than patching arbitrary DOM elements with manual CSS.
56
+
57
+ ## Alternatives
58
+
59
+ 1. **Styling approach: Tailwind instead of BEM**
60
+ - We considered using Tailwind CSS (utility‑first classes) instead of the BEM naming convention.
61
+ - Pros:
62
+ - Potentially smaller final CSS payload due to tree‑shaking.
63
+ - Easier merging of classes to avoid shipping unused styles.
64
+ - Cons:
65
+ - Would require a full rewrite of existing styles into utility classes.
66
+ - Requires an additional class‑merge utility and Tailwind tooling in the build pipeline.
67
+ - Makes the resulting HTML/CSS less predictable for client‑side overrides, especially when clients are not familiar with Tailwind.
68
+ - Given the current state of the project and the need for predictable element names for customization, BEM was chosen over Tailwind.
69
+
70
+ 2. **Configuration approach: separate CSS file per client**
71
+ - We also considered storing custom styles for each client in a dedicated CSS file.
72
+ - Pros:
73
+ - Clear physical separation of styles per client.
74
+ - Simpler for some hosting scenarios.
75
+ - Cons:
76
+ - Requires injecting a client identifier into the HTML to load the correct file.
77
+ - Increases deployment and configuration complexity (managing many CSS files per environment).
78
+ - Harder to keep styles and other configuration (translations, feature flags) in sync in a single place.
79
+ - Instead, we opted for a single configuration entry with an inline `customCss` field.
80
+
81
+ ## Expectations
82
+
83
+ From this decision we expect the following benefits:
84
+
85
+ 1. **More flexible customization for clients**
86
+ - Ability to adjust translations, color schemes, and key UI elements (buttons, inputs, basic layout) per client.
87
+ - A clear mechanism for styling without touching internal implementation details of the widget.
88
+
89
+ 2. **Move towards a design‑system approach**
90
+ - Over time, introduce configurable component variants instead of ad‑hoc overrides.
91
+ - Establish color tokens and a configurable theme, instead of manually redefining colors in many places.
92
+
93
+ 3. **Safer and more maintainable styling changes**
94
+ - BEM class naming makes selectors more predictable and less fragile.
95
+ - Centralized configuration reduces the chance of forgetting to apply a setting in some part of the system.
96
+
97
+ ## Technical Debt and Planned Improvements
98
+
99
+ The current customization system is intentionally minimal and leaves several areas as explicit technical debt:
100
+
101
+ 1. **Global configuration shared across projects**
102
+ - A single configuration model is used across multiple projects/clients, which means some unused options are shipped everywhere.
103
+ - We plan to introduce a more modular configuration structure so that clients only receive relevant settings.
104
+
105
+ 2. **Manual style rewrites and lack of a change strategy**
106
+ - Many styles are still written and updated manually without a clear evolution path.
107
+ - We need a defined process and guidelines for introducing and deprecating style rules.
108
+
109
+ 3. **Missing color system**
110
+ - There is no dedicated color token system yet.
111
+ - Colors are not centralized, making it hard to define themes and variants.
112
+
113
+ 4. **No component‑level design tokens**
114
+ - Components (buttons, inputs, etc.) do not expose design tokens for spacing, radii, typography, and interaction states.
115
+ - A future step is to define tokens per component and allow them to be configured per client.
116
+
117
+ 5. **Lack of component variants**
118
+ - Components do not yet expose standardized variants (e.g. `primary`, `secondary`, `ghost`).
119
+ - Introducing variants will make customization more structured and reduce the need for one‑off overrides.
120
+
121
+ 6. **Font customization limitations**
122
+ - At the moment, fonts are effectively tied to Google Fonts.
123
+ - There is no supported mechanism for loading custom font files or non‑Google font providers per client.
124
+
125
+ 7. **Icon customization limitations**
126
+ - It is not yet possible to change icon colors or replace icons via configuration.
127
+ - Future work should define an icon system (e.g. via tokens or an icon registry) that can be overridden per client.
128
+
129
+ 8. **No first‑class configuration for core UI elements**
130
+ - Core components such as buttons and inputs are not yet configurable via a structured theme API.
131
+ - Future improvements should expose configuration options for these components (sizes, radii, colors, typography, and states) instead of relying solely on `customCss`.
132
+
133
+ These items are expected to be addressed iteratively as we mature the customization system and move closer to a full design‑system and token‑based theming approach.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "wineater-bot",
3
- "version": "4.0.0-beta.7",
3
+ "version": "4.1.0",
4
4
  "description": "",
5
- "main": "dist/wineater-chatbot.umd.js",
5
+ "main": "dist/wineater-chatbot.mjs",
6
6
  "module": "src/index.js",
7
7
  "type": "module",
8
8
  "scripts": {
package/src/App.svelte CHANGED
@@ -12,6 +12,7 @@
12
12
  import WineTile from './components/WineTile.svelte';
13
13
  import { appStore } from './services/store/app-store.svelte';
14
14
  import { getTranslation, translateTexts } from './services/i18n/i18n';
15
+ import { generateInlineStyle } from './services/ui-config/utils';
15
16
 
16
17
  function initCookies() {
17
18
  setCookie('chatbotToken', window.wineaterData?.CLIENT_TOKEN || DEMO_TOKEN, 365);
@@ -41,6 +42,7 @@
41
42
  let apiKey = $derived(appStore.getApiKey());
42
43
  let storeId = $derived(appStore.getStoreId());
43
44
  let isStoreInitialized = $derived(appStore.isInitialized());
45
+ let uiConfig = $derived(appStore.getUiConfig());
44
46
 
45
47
  function setSearchError() {
46
48
  errorMessage = getTranslation({ key: 'errorMessage', lang: appLanguage });
@@ -225,7 +227,7 @@
225
227
 
226
228
  function onTileClick(event, wine: Wine | RecommendedWine) {
227
229
  // Don't track clicks on buy buttons or close buttons
228
- if (event.target.closest('.buy-btn') || event.target.closest('.card-popup-close')) {
230
+ if (event.target.closest('.js-WineTileAction') || event.target.closest('.js-WinePopupClose')) {
229
231
  return;
230
232
  }
231
233
 
@@ -237,7 +239,7 @@
237
239
 
238
240
  function handleClickOutside(event: MouseEvent) {
239
241
  const target = event.target as HTMLElement;
240
- const isInsidePopup = target.closest('.card-popup') || target.closest('.buy-btn');
242
+ const isInsidePopup = target.closest('.js-WinePopup') || target.closest('.js-WineTileAction');
241
243
 
242
244
  if (!isInsidePopup) {
243
245
  selectedWineId = null;
@@ -249,6 +251,10 @@
249
251
  await appStore.initializeStore();
250
252
  if (!isStoreInitialized) return;
251
253
 
254
+ uiConfig = appStore.getUiConfig();
255
+ if (uiConfig.customCss) {
256
+ generateInlineStyle(uiConfig.customCss);
257
+ }
252
258
  // Set dynamic color variable from widget data
253
259
  const customColor = window?.wineaterData?.COLOR || '#7B2BEB';
254
260
  document.documentElement.style.setProperty('--wineater-color', customColor);
@@ -267,54 +273,57 @@
267
273
  </script>
268
274
 
269
275
  {#if isStoreInitialized}
270
- <div id="wine-widget-container" class="svelte-wineater-widget-container">
271
- <div class="wine-widget-wrapper">
272
- <Header />
273
-
274
- <Search bind:searchQuery onSubmit={handleSearch} {initialized} />
275
-
276
- {#if isLoading}
277
- <Loader />
278
- {/if}
279
-
280
- <div id="wine-widget-right">
281
- {#if errorMessage}
282
- <div class="error-message">
283
- <p>{errorMessage}</p>
284
- </div>
285
- {:else if results && results.length > 0}
286
- <div id="wine-placeholder">
287
- {#each results as wine, index}
288
- <WineTile
289
- {wine}
290
- {index}
291
- {hoveredWineId}
292
- {selectedWineId}
293
- {isAnimating}
294
- translatedPopup={translatedPopups[index]}
295
- onBuyClick={handleBuyBtnClick}
296
- onPopupClose={closePopup}
297
- {onTileEnter}
298
- {onTileLeave}
299
- {onTileClick}
300
- />
301
- {/each}
302
- </div>
303
- {/if}
276
+ <!-- wr-Custom is used only for custom styling -->
277
+ <!-- id is used to increase specificity -->
278
+ <div id="wr-Custom" class="wr-App">
279
+ {#if uiConfig.logoUrl}
280
+ <div class="wr-App-picture">
281
+ <img class="wr-App-logo" src={uiConfig.logoUrl} alt="" />
304
282
  </div>
305
-
306
- <Footer {countryCode} />
283
+ {/if}
284
+ <Header />
285
+
286
+ <Search bind:searchQuery onSubmit={handleSearch} {initialized} />
287
+
288
+ {#if isLoading}
289
+ <Loader />
290
+ {/if}
291
+
292
+ <div class="wr-App-content">
293
+ {#if errorMessage}
294
+ <div class="wr-App-error">
295
+ <p>{errorMessage}</p>
296
+ </div>
297
+ {:else if results && results.length > 0}
298
+ <div class="wr-App-placeholder">
299
+ {#each results as wine, index}
300
+ <WineTile
301
+ {wine}
302
+ {index}
303
+ {hoveredWineId}
304
+ {selectedWineId}
305
+ {isAnimating}
306
+ translatedPopup={translatedPopups[index]}
307
+ onBuyClick={handleBuyBtnClick}
308
+ onPopupClose={closePopup}
309
+ {onTileEnter}
310
+ {onTileLeave}
311
+ {onTileClick}
312
+ />
313
+ {/each}
314
+ </div>
315
+ {/if}
307
316
  </div>
317
+
318
+ <Footer {countryCode} />
308
319
  </div>
309
320
  {/if}
310
321
 
311
322
  <style>
312
- /* ===================== 0) Font Import ===================== */
313
- @import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap');
314
-
315
323
  /* ===================== 1) CSS Variables ===================== */
316
324
  :global(:root) {
317
325
  --wineater-corp-color: #0044ff;
326
+ /* TODO: get color from config */
318
327
  --wineater-color: #7b2beb;
319
328
  }
320
329
 
@@ -327,11 +336,15 @@
327
336
  display: none;
328
337
  }
329
338
 
339
+ :global(body) {
340
+ margin: 0;
341
+ padding: 0;
342
+
343
+ font-family: var(--wineater-font-primary, sans-serif);
344
+ }
345
+
330
346
  /* ===================== 3) Widget Container ===================== */
331
- #wine-widget-container {
332
- display: flex;
333
- gap: 24px;
334
- font-family: 'Bricolage Grotesque', sans-serif;
347
+ .wr-App {
335
348
  padding: 12px 12px 0px;
336
349
  border-radius: 16px;
337
350
  background-color: #fff;
@@ -341,24 +354,28 @@
341
354
  min-height: 500px;
342
355
  margin: 40px auto;
343
356
  position: relative;
344
- box-sizing: border-box;
345
357
  }
346
358
 
347
- #wine-widget-container :global([class^='icon-']),
348
- #wine-widget-container :global([class*=' icon-']) {
349
- font-family: 'Bricolage Grotesque', sans-serif !important;
359
+ .wr-App-picture {
360
+ display: flex;
361
+ justify-content: center;
362
+ align-items: center;
363
+ margin-bottom: 12px;
350
364
  }
351
365
 
352
- .wine-widget-wrapper {
353
- width: 100%;
366
+ .wr-App-logo {
367
+ max-width: 100%;
368
+ height: auto;
369
+ width: auto;
370
+ max-height: 60px;
354
371
  }
355
372
 
356
- #wine-widget-right {
373
+ .wr-App-content {
357
374
  position: relative;
358
375
  min-height: 300px;
359
376
  }
360
377
 
361
- #wine-placeholder {
378
+ .wr-App-placeholder {
362
379
  display: grid;
363
380
  grid-template-columns: repeat(4, 1fr);
364
381
  gap: 0;
@@ -367,7 +384,7 @@
367
384
  }
368
385
 
369
386
  /* ===================== 12) Error Message ===================== */
370
- .error-message {
387
+ .wr-App-error {
371
388
  text-align: center;
372
389
  padding: 24px;
373
390
  color: #666;
@@ -375,13 +392,13 @@
375
392
 
376
393
  /* ===================== 13) Mobile Styles ===================== */
377
394
  @media screen and (max-width: 768px) {
378
- #wine-widget-container {
395
+ .wr-App {
379
396
  flex-direction: column;
380
397
  padding: 0px 16px;
381
398
  min-width: 100vw;
382
399
  }
383
400
 
384
- #wine-placeholder {
401
+ .wr-App-placeholder {
385
402
  display: flex;
386
403
  gap: 8px;
387
404
  overflow-x: auto;
@@ -27,81 +27,70 @@
27
27
  }
28
28
  </script>
29
29
 
30
- <div id="powered-by-widget">
31
- <div class="powered-by-center">
32
- <a href="https://wineater.com" target="_blank">
33
- <span>{getTranslation({ key: 'poweredBy', lang: appLanguage })}</span>
30
+ <footer class="wr-Footer">
31
+ <div class="wr-Footer-center">
32
+ <a href="https://wineater.com" class="wr-Footer-link" target="_blank">
33
+ <span class="wr-Footer-text">{getTranslation({ key: 'poweredBy', lang: appLanguage })}</span>
34
34
  <img
35
+ class="wr-Footer-logo"
35
36
  src={countryCode === 'ru'
36
37
  ? 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/divino_logo.svg?x=5'
37
38
  : 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/logo_medium_gray.svg'}
38
39
  alt="Wineater Logo"
39
40
  />
40
- <span class="powered-by-sommelier"
41
- >{getTranslation({ key: 'ai_sommelier', lang: appLanguage })}</span
42
- >
41
+ <span class="wr-Footer-text wr-Footer-text--sommelier">
42
+ {getTranslation({ key: 'ai_sommelier', lang: appLanguage })}
43
+ </span>
43
44
  </a>
44
45
  </div>
45
- <div class="powered-by-lang">
46
- {#if supportedLanguages.length > 1}
47
- <select
48
- id="language-dropdown"
49
- class="language-dropdown"
50
- value={appLanguage}
51
- onchange={handleLanguageChange}
52
- >
53
- {#each supportedLanguages as lang}
54
- <option value={lang} selected={lang === appLanguage}>
55
- {lang.toUpperCase()}
56
- </option>
57
- {/each}
58
- </select>
59
- {/if}
60
- </div>
61
- </div>
46
+ {#if supportedLanguages.length > 1}
47
+ <select class="wr-Footer-dropdown" value={appLanguage} onchange={handleLanguageChange}>
48
+ {#each supportedLanguages as lang}
49
+ <option class="wr-Footer-lang" value={lang} selected={lang === appLanguage}>
50
+ {lang.toUpperCase()}
51
+ </option>
52
+ {/each}
53
+ </select>
54
+ {/if}
55
+ </footer>
62
56
 
63
57
  <style>
64
- #powered-by-widget {
58
+ .wr-Footer {
65
59
  position: relative;
66
60
  border-top: 2px solid #f6f7fd;
67
- font-family: 'Bricolage Grotesque', sans-serif;
68
61
  font-weight: 200;
69
62
  padding: 12px 24px;
70
63
  min-height: 40px;
71
64
  }
72
65
 
73
- #powered-by-widget .powered-by-center {
66
+ .wr-Footer-center {
74
67
  position: absolute;
75
68
  left: 50%;
76
69
  transform: translateX(-50%);
77
70
  text-align: center;
78
71
  }
79
72
 
80
- #powered-by-widget .powered-by-lang {
73
+ .wr-Footer-dropdown {
81
74
  position: absolute;
82
75
  right: 0px;
83
76
  top: 50%;
84
77
  transform: translateY(-50%);
85
- }
86
-
87
- .language-dropdown {
88
78
  padding: 4px;
89
79
  border-radius: 6px;
90
80
  border: 2px solid var(--wineater-color);
91
81
  background-color: white;
92
82
  color: var(--wineater-color);
93
83
  font-weight: 500;
94
- font-family: 'Bricolage Grotesque', sans-serif;
95
84
  font-size: 12px;
96
85
  cursor: pointer;
97
86
  transition: all 0.2s ease;
98
87
  }
99
88
 
100
- .language-dropdown:hover {
89
+ .wr-Footer-dropdown:hover {
101
90
  box-shadow: 0 4px 8px rgba(123, 43, 235, 0.2);
102
91
  }
103
92
 
104
- #powered-by-widget a {
93
+ .wr-Footer-link {
105
94
  text-decoration: none;
106
95
  display: flex;
107
96
  align-items: center;
@@ -113,18 +102,18 @@
113
102
  transition: opacity 0.2s ease;
114
103
  }
115
104
 
116
- #powered-by-widget a:hover {
105
+ .wr-Footer-link:hover {
117
106
  opacity: 0.8;
118
107
  }
119
108
 
120
- #powered-by-widget img {
109
+ .wr-Footer-logo {
121
110
  height: 16px;
122
111
  width: auto;
123
112
  color: #a8a8a8;
124
113
  }
125
114
 
126
115
  @media screen and (max-width: 768px) {
127
- .powered-by-sommelier {
116
+ .wr-Footer-text--sommelier {
128
117
  display: none;
129
118
  }
130
119
  }
@@ -6,29 +6,27 @@
6
6
  const titleText = window?.wineaterData?.TITLE_TEXT || null;
7
7
  </script>
8
8
 
9
- <div id="wine-widget-header">
9
+ <div class="wr-Header">
10
10
  {#if titleText}
11
- <h1 class="wine-widget-big-title">{titleText}</h1>
11
+ <h1 class="wr-Header-title">{titleText}</h1>
12
12
  {:else}
13
- <!-- TODO: translation is not working -->
14
- <h1 class="wine-widget-big-title">
13
+ <h1 class="wr-Header-title">
15
14
  {getTranslation({ key: 'headerLine1', lang })}
16
- <span class="icon icon-meal"></span>
17
- <span class="icon-label">{getTranslation({ key: 'headerMeal', lang })}</span>,
18
- <span class="icon icon-mood"></span>
19
- <span class="icon-label">{getTranslation({ key: 'headerMood', lang })}</span>
15
+ <span class="wr-Header-icon wr-Header-icon--meal"></span>
16
+ <span class="wr-Header-label">{getTranslation({ key: 'headerMeal', lang })}</span>,
17
+ <span class="wr-Header-icon wr-Header-icon--mood"></span>
18
+ <span class="wr-Header-label">{getTranslation({ key: 'headerMood', lang })}</span>
20
19
  <br />
21
20
  {getTranslation({ key: 'headerOr', lang })}
22
- <span class="icon icon-occasion"></span>
23
- <span class="icon-label">{getTranslation({ key: 'headerOccasion', lang })}</span>
21
+ <span class="wr-Header-icon wr-Header-icon--occasion"></span>
22
+ <span class="wr-Header-label">{getTranslation({ key: 'headerOccasion', lang })}</span>
24
23
  {getTranslation({ key: 'headerLine2', lang })}
25
24
  </h1>
26
25
  {/if}
27
26
  </div>
28
27
 
29
28
  <style>
30
- #wine-widget-header h1 {
31
- font-family: 'Bricolage Grotesque', serif;
29
+ .wr-Header-title {
32
30
  font-weight: 600;
33
31
  font-size: 24px;
34
32
  margin: 0 auto;
@@ -37,15 +35,7 @@
37
35
  text-align: center;
38
36
  }
39
37
 
40
- #wine-widget-header p {
41
- font-size: 16px;
42
- color: #666;
43
- margin-top: 8px;
44
- text-align: center;
45
- padding: 0 15%;
46
- }
47
-
48
- .icon {
38
+ .wr-Header-icon {
49
39
  display: inline-block;
50
40
  width: 1.2em;
51
41
  height: 1.2em;
@@ -55,37 +45,34 @@
55
45
  background-size: contain;
56
46
  }
57
47
 
58
- .icon-meal {
48
+ .wr-Header-icon--meal {
59
49
  background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//meal1.svg');
60
50
  }
61
51
 
62
- .icon-meal:hover {
52
+ .wr-Header-icon--meal:hover {
63
53
  background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//meal3.svg');
64
54
  }
65
55
 
66
- .icon-mood {
56
+ .wr-Header-icon--mood {
67
57
  background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//mood.svg');
68
58
  }
69
59
 
70
- .icon-budget {
60
+ /* Not used, old icon */
61
+ .wr-Header-icon--budget {
71
62
  background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//budget.svg');
72
63
  }
73
64
 
74
- .icon-occasion {
65
+ .wr-Header-icon--occasion {
75
66
  background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//ocasion.svg');
76
67
  }
77
68
 
78
- .icon-label {
69
+ .wr-Header-label {
79
70
  color: var(--wineater-color);
80
71
  font-weight: 500;
81
72
  }
82
73
 
83
74
  @media screen and (max-width: 768px) {
84
- #wine-widget-header h1 {
85
- display: none;
86
- }
87
-
88
- #wine-widget-header p {
75
+ .wr-Header-title {
89
76
  display: none;
90
77
  }
91
78
  }
@@ -1,5 +1,5 @@
1
- <div id="loading-indicator">
2
- <div id="typing-indicator" class="typing-indicator">
1
+ <div class="wr-Loader">
2
+ <div class="wr-Loader-indicator">
3
3
  <span></span>
4
4
  <span></span>
5
5
  <span></span>
@@ -7,7 +7,7 @@
7
7
  </div>
8
8
 
9
9
  <style>
10
- #loading-indicator {
10
+ .wr-Loader {
11
11
  position: absolute;
12
12
  top: 50%;
13
13
  left: 50%;
@@ -16,7 +16,7 @@
16
16
  text-align: center;
17
17
  }
18
18
 
19
- .typing-indicator {
19
+ .wr-Loader-indicator {
20
20
  display: flex;
21
21
  justify-content: center;
22
22
  align-items: center;
@@ -25,7 +25,7 @@
25
25
  background-color: white;
26
26
  }
27
27
 
28
- .typing-indicator span {
28
+ .wr-Loader-indicator span {
29
29
  display: block;
30
30
  width: 8px;
31
31
  height: 8px;
@@ -48,15 +48,15 @@
48
48
  }
49
49
  }
50
50
 
51
- .typing-indicator span:nth-child(1) {
51
+ .wr-Loader-indicator span:nth-child(1) {
52
52
  animation-delay: 0s;
53
53
  }
54
54
 
55
- .typing-indicator span:nth-child(2) {
55
+ .wr-Loader-indicator span:nth-child(2) {
56
56
  animation-delay: 0.15s;
57
57
  }
58
58
 
59
- .typing-indicator span:nth-child(3) {
59
+ .wr-Loader-indicator span:nth-child(3) {
60
60
  animation-delay: 0.3s;
61
61
  }
62
62
  </style>