@nutin/cli 1.3.0 → 1.3.1

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 (35) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +8 -9
  3. package/lib/index.mjs +18 -18
  4. package/lib/src/context-builder.mjs +21 -16
  5. package/lib/src/print.mjs +1 -0
  6. package/lib/src/project-generator.mjs +5 -5
  7. package/lib/src/prompts.mjs +13 -85
  8. package/lib/src/version.mjs +1 -1
  9. package/package.json +1 -1
  10. package/templates/base/builder.config.js.hbs +1 -0
  11. package/templates/base/src/app/globals.d.ts.hbs +12 -8
  12. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +25 -8
  13. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +34 -20
  14. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +34 -0
  15. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +11 -12
  16. package/templates/base/src/core/base-classes/component/component.ts.hbs +13 -17
  17. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +17 -0
  18. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +2 -1
  19. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +0 -33
  20. package/templates/base/src/core/services/router/router.ts.hbs +0 -1
  21. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +35 -7
  22. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +31 -8
  23. package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +197 -0
  24. package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +75 -0
  25. package/templates/base/src/libs/popover/popover.ts.hbs +84 -91
  26. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +6 -3
  27. package/templates/base/src/styles/base/_variables.scss.hbs +1 -0
  28. package/templates/base/src/styles/core/_anchors.scss.hbs +10 -0
  29. package/templates/base/src/styles/core/_index.scss.hbs +6 -0
  30. package/templates/base/src/styles/core/_popover.scss.hbs +96 -0
  31. package/templates/base/src/styles/core/_snackbar.scss.hbs +50 -0
  32. package/templates/base/src/styles/main.scss.hbs +1 -1
  33. package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +41 -8
  34. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +14 -0
  35. package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
@@ -1,4 +1,6 @@
1
- import { View, ButtonManager, BaseButton, ViewRenderManager } from '../../core/index.js';
1
+ import { View, ButtonManager, BaseButton, ComponentConfig, AppPipeRegistry, CatalogConfig, AppEventBus } from '../../core/index.js';
2
+ import { FocusTrapHelper, IFocusTrapOptions } from './helpers/focus-trap.helper.js';
3
+ import { IPopoverDomElements, PopoverDomHelper } from './helpers/popover-dom.helper.js';
2
4
 
3
5
  export interface PopoverButton extends BaseButton {
4
6
  // if needed for Popover-specific properties
@@ -6,29 +8,56 @@ export interface PopoverButton extends BaseButton {
6
8
 
7
9
  export interface PopoverOptions {
8
10
  template: string;
11
+ components?: ComponentConfig[];
12
+ catalogs?: CatalogConfig[];
9
13
  viewName?: string;
10
14
  onClose?: () => void;
11
15
  buttons?: PopoverButton[];
16
+ focusTrapOptions?: IFocusTrapOptions;
12
17
  }
13
18
 
14
19
  export class PopoverView extends View {
15
- private onClose?: () => void;
16
- private overlay: HTMLElement | null = null;
17
- private buttonManager: ButtonManager;
20
+ private _onClose?: () => void;
21
+ private _overlay: HTMLElement | null = null;
22
+ private _buttonManager: ButtonManager;
23
+ private _prevTitle: string | undefined;
24
+ private _catalogs: CatalogConfig[];
25
+ private _focusTrap: FocusTrapHelper | null = null;
26
+ private _focusTrapOptions: IFocusTrapOptions;
27
+ private _components: ComponentConfig[];
18
28
 
19
29
  constructor({
20
30
  template,
21
31
  buttons = [],
22
32
  onClose,
23
- viewName = 'popover',
33
+ viewName,
34
+ components = [],
35
+ catalogs = [],
36
+ focusTrapOptions = {}
24
37
  }: PopoverOptions) {
25
- super({template, tagName: 'div', mountTarget: 'body', viewName});
26
- this.onClose = onClose;
27
- this.buttonManager = new ButtonManager(
28
- this,
29
- this.wrapButtonCallbacks(buttons),
30
- { containerClassName: 'popover-buttons' }
31
- );
38
+ super({ template, tagName: 'div', mountTarget: 'body' });
39
+ this._onClose = onClose;
40
+ this._components = components;
41
+ this._catalogs = catalogs;
42
+ this._buttonManager = this.createButtonManager(buttons);
43
+ this.setOptionalViewName(viewName);
44
+ this._focusTrapOptions = focusTrapOptions;
45
+ AppEventBus.subscribe('popover-close', () => this.destroy());
46
+ }
47
+
48
+ public childConfigs(): ComponentConfig[] {
49
+ return [
50
+ ...this._components,
51
+ ...this.catalogConfigs()
52
+ ];
53
+ }
54
+
55
+ public catalogConfigs(): ComponentConfig[] {
56
+ const configs: ComponentConfig[] = []
57
+ this._catalogs.map((catalog) => {
58
+ configs.push(...this.catalogConfig(catalog));
59
+ })
60
+ return configs;
32
61
  }
33
62
 
34
63
  public override shouldUpdateMetaContent(): boolean {
@@ -36,89 +65,63 @@ export class PopoverView extends View {
36
65
  }
37
66
 
38
67
  public override render(): HTMLElement {
39
- document.body.classList.add('no-scroll');
40
- this.overlay = this.createOverlay();
41
-
42
- const wrapper = this.createWrapper();
43
- const content = this.createContent();
68
+ document.documentElement.classList.add('no-scroll');
44
69
 
45
- this.appendCloseButton(content);
46
- this.appendFooterButtons(content);
70
+ const { wrapper, overlay } = this.createPopoverDomElements();
47
71
 
48
- wrapper.appendChild(content);
49
- this.overlay.appendChild(wrapper);
50
- document.body.appendChild(this.overlay);
72
+ this._focusTrap = this.createFocusTrap(wrapper, overlay);
73
+ this._focusTrap.activate();
51
74
 
52
- this.autoBindEvents();
53
- this.parseDataAttributes();
54
- this.animateIn(wrapper);
55
-
56
- this.bindOverlayClose();
57
-
58
- return this.overlay;
75
+ AppEventBus.emit('popover-opened');
76
+ this._overlay = overlay;
77
+ return overlay;
59
78
  }
60
79
 
61
80
  public override destroy(): void {
62
- if (this.overlay) {
63
- const wrapper = this.overlay.querySelector('.popover-wrapper');
64
- this.overlay.classList.remove('show');
65
- wrapper?.classList.remove('show');
66
-
67
- setTimeout(() => {
68
- wrapper?.remove()
69
- this.overlay?.remove();
70
- this.overlay = null;
71
- }, 250); // match transition duration
72
- }
73
-
74
- document.body.classList.remove('no-scroll');
75
- super.destroy();
76
-
77
- if (typeof this.onClose === 'function') {
78
- this.onClose();
79
- }
80
- }
81
-
82
- // Wraps button callbacks to include destroy() call
83
- private wrapButtonCallbacks(buttons: PopoverButton[]): PopoverButton[] {
84
- return buttons.map(btn => ({
85
- ...btn,
86
- callback: () => {
87
- if (btn.callback) {
88
- btn.callback();
89
- }
90
- this.destroy();
81
+ this._focusTrap?.deactivate();
82
+ if (this._prevTitle) document.title = this._prevTitle;
83
+
84
+ this._overlay = PopoverDomHelper.removeDomElements(this._overlay, () => {
85
+ document.documentElement.classList.remove('no-scroll');
86
+ super.destroy();
87
+ if (typeof this._onClose === 'function') {
88
+ this._onClose();
91
89
  }
92
- }));
90
+ });
93
91
  }
94
92
 
95
- private createOverlay(): HTMLElement {
96
- const overlay = document.createElement('div');
97
- overlay.className = 'popover-overlay';
98
- return overlay;
93
+ private setOptionalViewName(viewName: string | undefined): void {
94
+ if (viewName) {
95
+ this._prevTitle = document.title;
96
+ document.title = AppPipeRegistry.apply('capitalize', viewName);
97
+ }
99
98
  }
100
99
 
101
- private createWrapper(): HTMLElement {
102
- const wrapper = document.createElement('div');
103
- wrapper.className = 'popover-wrapper';
104
- return wrapper;
100
+ private createButtonManager(buttons: PopoverButton[]): ButtonManager {
101
+ return new ButtonManager(
102
+ this,
103
+ buttons,
104
+ { containerClassName: 'popover-buttons' }
105
+ );
105
106
  }
106
107
 
107
- private createContent(): HTMLElement {
108
+ private createPopoverDomElements(): IPopoverDomElements {
108
109
  const content = super.render();
109
- content.classList.add('popover-content');
110
- ViewRenderManager.cleanupOptionalContent();
111
- return content;
110
+ const { overlay, wrapper } = PopoverDomHelper.createDomElements(content, this._buttonManager);
111
+
112
+ this.autoBindEvents();
113
+ this.parseDataAttributes();
114
+ this.animateIn(wrapper);
115
+
116
+ return { overlay, wrapper };
112
117
  }
113
118
 
114
- private appendCloseButton(content: HTMLElement): void {
115
- const closeButton = document.createElement('button');
116
- closeButton.className = 'popover-close-button';
117
- closeButton.innerHTML = '×';
118
- closeButton.setAttribute('aria-label', 'Close');
119
- // bind method onCloseClick
120
- closeButton.setAttribute('data-event', 'click:onCloseClick');
121
- content.prepend(closeButton);
119
+ private createFocusTrap(wrapper: HTMLElement, overlay: HTMLElement): FocusTrapHelper {
120
+ return new FocusTrapHelper({
121
+ container: wrapper,
122
+ overlay: overlay,
123
+ options: this._focusTrapOptions
124
+ });
122
125
  }
123
126
 
124
127
  // Handles the close button (via data-event bound in appendCloseButton)
@@ -126,20 +129,10 @@ export class PopoverView extends View {
126
129
  this.destroy();
127
130
  }
128
131
 
129
- private appendFooterButtons(content: HTMLElement): void {
130
- this.buttonManager.appendTo(content);
131
- }
132
-
133
- private bindOverlayClose(): void {
134
- this.overlay?.addEventListener('click', (e: MouseEvent) => {
135
- if (e.target === this.overlay) this.destroy();
136
- });
137
- }
138
-
139
132
  private animateIn(wrapper: HTMLElement): void {
140
133
  requestAnimationFrame(() => {
141
- this.overlay?.classList.add('show');
134
+ this._overlay?.classList.add('show');
142
135
  wrapper.classList.add('show');
143
136
  });
144
137
  }
145
- }
138
+ }
@@ -1,3 +1,5 @@
1
+ import {SecurityHelper} from "../../core/base-classes/base-component/helpers/security.helper.js";
2
+
1
3
  export type NotifyOptions = {
2
4
  type?: 'info' | 'success' | 'error';
3
5
  position?: 'top' | 'bottom';
@@ -44,9 +46,10 @@ function displayNext(): void {
44
46
  }
45
47
 
46
48
  isDisplaying = true;
47
- const { message, options } = queue.shift()!;
48
- const { type = 'info', position = 'bottom', duration = 3000, actionText, onAction } = options;
49
-
49
+ let { message, options } = queue.shift()!;
50
+ let { type = 'info', position = 'bottom', duration = 3000, actionText, onAction } = options;
51
+ message = SecurityHelper.sanitizeTemplate(message);
52
+ actionText = SecurityHelper.sanitizeTemplate(actionText);
50
53
  const snackbar = document.createElement('div');
51
54
  snackbar.className = `app-snackbar app-snackbar--${type} app-snackbar--${position}`;
52
55
  snackbar.innerHTML = `
@@ -8,6 +8,7 @@ $background-color: rgb(255 255 255); // WHITE
8
8
  $text-color: rgb(0 0 0); // BLACK
9
9
  $primary-color: rgb(0 0 0); // BLACK
10
10
  $secondary-color: rgb(0 0 0); // BLACK
11
+ $accent-color: rgb(0 0 0); // BLACK
11
12
 
12
13
  // snackbar colours
13
14
  $snack-success: rgb(76 175 80);
@@ -0,0 +1,10 @@
1
+ .sr-only {
2
+ position: absolute;
3
+ width: 1px;
4
+ height: 1px;
5
+ overflow: hidden;
6
+ clip: rect(1px, 1px, 1px, 1px);
7
+ white-space: nowrap;
8
+ text-decoration: none;
9
+ outline: none;
10
+ }
@@ -0,0 +1,6 @@
1
+ @forward "anchors";
2
+ @forward "popover";
3
+ @forward "snackbar";
4
+ {{#if stylinNutin}}
5
+ @forward "utilities";
6
+ {{/if}}
@@ -0,0 +1,96 @@
1
+ /* Prevent scroll when popover is active */
2
+ html.no-scroll {
3
+ overflow: hidden;
4
+ }
5
+
6
+ .popover-overlay {
7
+ z-index: 99;
8
+ position: fixed;
9
+ inset: 0;
10
+ backdrop-filter: blur(5px);
11
+ background-color: rgba(0, 0, 0, .1);
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ opacity: 0;
16
+ animation: hide-pop 0.3s ease forwards;
17
+ &.show {
18
+ animation: show-pop 0.3s ease forwards;
19
+ }
20
+ }
21
+
22
+ .popover-wrapper {
23
+ overflow-y: auto;
24
+ position: relative;
25
+ overscroll-behavior: none;
26
+ border-radius: 1rem;
27
+ width: 90%;
28
+ height: 90%;
29
+ transform: scale(0.95);
30
+ opacity: 0;
31
+ transition: opacity 0.3s ease, transform 0.3s ease;
32
+ -moz-transition: opacity 0.3s ease, -moz-transform 0.3s ease;
33
+ -webkit-transition: opacity 0.3s ease, -webkit-transform 0.3s ease;
34
+ -o-transition: opacity 0.3s ease, -o-transform 0.3s ease;
35
+ &.show {
36
+ transform: scale(1);
37
+ -moz-transform: scale(1);
38
+ -webkit-transform: scale(1);
39
+ -o-transform: scale(1);
40
+ opacity: 1;
41
+ }
42
+ }
43
+
44
+ .popover-content {
45
+ position: relative;
46
+ height: 100%;
47
+ overflow-y: auto;
48
+ -webkit-overflow-scrolling: touch;
49
+ overscroll-behavior: contain;
50
+ }
51
+
52
+ .popover-close-button {
53
+ position: absolute;
54
+ top: 0.5rem;
55
+ right: 0.75rem;
56
+ background: none;
57
+ border: none;
58
+ font-size: 1.5rem;
59
+ cursor: pointer;
60
+ padding: 0 1rem;
61
+ &:focus:not(:focus-visible) {
62
+ }
63
+ &:focus-visible {
64
+ }
65
+ }
66
+
67
+ .popover-buttons {
68
+ display: flex;
69
+ justify-content: flex-end;
70
+ gap: 1rem;
71
+ margin-top: 1.5rem;
72
+ }
73
+
74
+ .popover-buttons button {
75
+ background-color: inherit;
76
+ transition: ease-in-out 0.3s;
77
+ padding: 0.5rem 1rem;
78
+ font-size: 1rem;
79
+ cursor: pointer;
80
+ &:hover {
81
+ }
82
+ @media (hover: none) {
83
+ &:hover {
84
+ }
85
+ }
86
+ }
87
+
88
+ @keyframes show-pop {
89
+ from { opacity: 0 }
90
+ to { opacity: 1 }
91
+ }
92
+
93
+ @keyframes hide-pop {
94
+ from { opacity: 1 }
95
+ to { opacity: 0 }
96
+ }
@@ -0,0 +1,50 @@
1
+ @use "sass:map";
2
+ @use "variables" as *;
3
+ {{#if stylinNutin}}
4
+ @use "mixins";
5
+ {{/if}}
6
+
7
+ .app-snackbar {
8
+ position: fixed;
9
+ left: 50%;
10
+ transform: translateX(-50%);
11
+ {{#if stylinNutin}}
12
+ @include mixins.flex-center;
13
+ {{else}}
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ {{/if}}
18
+ background: $snack-default;
19
+ color: white;
20
+ padding: 0.75rem 1.5rem;
21
+ border-radius: 0.5rem;
22
+ box-shadow: 0 2px 6px rgba(0,0,0,0.3);
23
+ gap: 1rem;
24
+ animation: fade-in 0.3s ease;
25
+ {{#if stylinNutin}}
26
+ @include mixins.respond-to(medium) {
27
+ min-width: 80%;
28
+ }
29
+ {{/if}}
30
+ }
31
+
32
+ .app-snackbar--bottom { bottom: 1rem; }
33
+ .app-snackbar--top { top: 1rem; }
34
+
35
+ .app-snackbar--success { background: $snack-success; }
36
+ .app-snackbar--error { background: $snack-error; }
37
+ .app-snackbar--info { background: $snack-info; }
38
+
39
+ .app-snackbar button {
40
+ background: transparent;
41
+ border: none;
42
+ color: #fff;
43
+ font-weight: bold;
44
+ cursor: pointer;
45
+ }
46
+
47
+ @keyframes fade-in {
48
+ from { opacity: 0; transform: translateX(-50%) translateY(10px); }
49
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
50
+ }
@@ -1,5 +1,5 @@
1
1
  // Libraries styles (snackbar, popover){{#if transition}}, view transition styles{{/if}}.
2
- @use "libs";
2
+ @use "core";
3
3
 
4
4
  {{#if stylinNutin}}
5
5
  // built-in classes
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_LANGUAGE, LANGUAGES, Language, Translations } from './languages.js';
2
- import { Service } from '../../index.js';
2
+ import { AppEventBus, Service } from '../../index.js';
3
3
 
4
4
  export class I18n extends Service<I18n> {
5
5
  private readonly _DEFAULT_LANGUAGE: Language = DEFAULT_LANGUAGE;
@@ -7,11 +7,12 @@ export class I18n extends Service<I18n> {
7
7
  private _currentLanguage: Language = this._DEFAULT_LANGUAGE;
8
8
  private _translations: Translations = {};
9
9
  private _defaultTranslations: Translations = {};
10
+ private readonly _localStorageKey = 'nutin-fav-lang';
10
11
 
11
12
  constructor() {
12
13
  super();
13
14
  this.registerCleanup(this.resetTranslations);
14
- this._currentLanguage = this.getBrowserLanguage();
15
+ this._currentLanguage = this.getPreferredLanguage();
15
16
  }
16
17
 
17
18
  public get currentLanguage(): Language {
@@ -26,6 +27,17 @@ export class I18n extends Service<I18n> {
26
27
  return this._LANGUAGES;
27
28
  }
28
29
 
30
+ public get localStorageKey(): string {
31
+ return this._localStorageKey;
32
+ }
33
+
34
+ public async setCurrentLanguage(lang: Language): Promise<void> {
35
+ this._currentLanguage = lang;
36
+ this.savePreferences();
37
+ await this.loadTranslations(lang);
38
+ AppEventBus.emit('language-changed');
39
+ }
40
+
29
41
  public async loadTranslations(lang: Language): Promise<void> {
30
42
  try {
31
43
  const response = await fetch(`/locales/${lang}.json`);
@@ -39,10 +51,11 @@ export class I18n extends Service<I18n> {
39
51
  await this.loadDefaultTranslations();
40
52
  }
41
53
 
42
- document.documentElement.lang = lang;
54
+ document.documentElement.lang = lang;
55
+ this.savePreferences();
43
56
  } catch (error) {
44
57
  console.error('Translation load error:', error);
45
- }
58
+ }
46
59
  }
47
60
 
48
61
  public translate(key: string, textContent?: string | null): string {
@@ -60,8 +73,8 @@ export class I18n extends Service<I18n> {
60
73
  }
61
74
 
62
75
  public async initTranslations(): Promise<void> {
63
- const browserLang = this.getBrowserLanguage();
64
- const lang = this._LANGUAGES.includes(browserLang) ? browserLang : this._DEFAULT_LANGUAGE;
76
+ const preferredLang = this.getPreferredLanguage();
77
+ const lang = this._LANGUAGES.includes(preferredLang) ? preferredLang : this._DEFAULT_LANGUAGE;
65
78
 
66
79
  // Always load default translations first
67
80
  if (lang !== this._DEFAULT_LANGUAGE) {
@@ -71,6 +84,18 @@ export class I18n extends Service<I18n> {
71
84
  await this.loadTranslations(lang);
72
85
  }
73
86
 
87
+ public getTranslationObject<T = any>(key: string): T | null {
88
+ const keys = key.split('.');
89
+
90
+ let value = this.getNestedValue(this._translations, keys);
91
+
92
+ if (!value && this._currentLanguage !== this._DEFAULT_LANGUAGE) {
93
+ value = this.getNestedValue(this._defaultTranslations, keys);
94
+ }
95
+
96
+ return value || null;
97
+ }
98
+
74
99
  public resetTranslations(): void {
75
100
  this._translations = {};
76
101
  this._defaultTranslations = {};
@@ -82,8 +107,8 @@ export class I18n extends Service<I18n> {
82
107
  this.dispose();
83
108
  }
84
109
 
85
- private getBrowserLanguage() {
86
- return navigator.language.split('-')[0] as Language;
110
+ private getPreferredLanguage() {
111
+ return this.getPreferences() || navigator.language.split('-')[0] as Language;
87
112
  };
88
113
 
89
114
  private async loadDefaultTranslations(): Promise<void> {
@@ -106,6 +131,14 @@ export class I18n extends Service<I18n> {
106
131
  }
107
132
  return value;
108
133
  }
134
+
135
+ private savePreferences(): void {
136
+ localStorage.setItem(this._localStorageKey, this._currentLanguage);
137
+ }
138
+
139
+ private getPreferences(): string | null {
140
+ return localStorage.getItem(this._localStorageKey);
141
+ }
109
142
  }
110
143
 
111
144
  export const I18nService = I18n.getInstance();
@@ -60,6 +60,16 @@
60
60
  }
61
61
  }
62
62
 
63
+ .u-hide-on-small {
64
+ @include mixins.respond-to(medium) {
65
+ display: none;
66
+ }
67
+ }
68
+
69
+ .u-hide {
70
+ display: none;
71
+ }
72
+
63
73
  // color
64
74
  .u-color-base {
65
75
  color: $text-color;
@@ -73,6 +83,10 @@
73
83
  color: $secondary-color;
74
84
  }
75
85
 
86
+ .u-color-acc {
87
+ color: $accent-color;
88
+ }
89
+
76
90
  .u-color-bg {
77
91
  color: $background-color;
78
92
  }
@@ -1,132 +0,0 @@
1
- @use "sass:map";
2
- @use "variables" as *;
3
-
4
- // snackbar
5
- .app-snackbar {
6
- position: fixed;
7
- left: 50%;
8
- transform: translateX(-50%);
9
- background: $snack-default;
10
- color: white;
11
- padding: 0.75rem 1.5rem;
12
- border-radius: 0.5rem;
13
- box-shadow: 0 2px 6px rgba(0,0,0,0.3);
14
- display: flex;
15
- gap: 1rem;
16
- align-items: center;
17
- // z-index: 1000;
18
- animation: fade-in 0.3s ease;
19
- }
20
-
21
- .app-snackbar--bottom { bottom: 1rem; }
22
- .app-snackbar--top { top: 1rem; }
23
-
24
- .app-snackbar--success { background: $snack-success; }
25
- .app-snackbar--error { background: $snack-error; }
26
- .app-snackbar--info { background: $snack-info; }
27
-
28
- .app-snackbar button {
29
- background: transparent;
30
- border: none;
31
- color: #fff;
32
- font-weight: bold;
33
- cursor: pointer;
34
- }
35
-
36
- @keyframes fade-in {
37
- from { opacity: 0; transform: translateX(-50%) translateY(10px); }
38
- to { opacity: 1; transform: translateX(-50%) translateY(0); }
39
- }
40
-
41
- // popover
42
- /* Prevent scroll when popover is active */
43
- .no-scroll {
44
- overflow: hidden;
45
- }
46
-
47
- .popover-overlay {
48
- background: linear-gradient(to right, rgba($primary-color, .7), rgba($secondary-color, .7));
49
- position: fixed;
50
- inset: 0;
51
- backdrop-filter: blur(5px);
52
- display: flex;
53
- align-items: center;
54
- justify-content: center;
55
- // z-index: 9999;
56
- opacity: 0;
57
- transition: opacity 0.25s ease;
58
- &.show {
59
- opacity: 1;
60
- }
61
- }
62
-
63
- .popover-wrapper {
64
- position: relative;
65
- background-color: $background-color;
66
- border-radius: 10px transparent;
67
- max-width: 600px;
68
- width: 90%;
69
- max-height: 90%;
70
- overflow: auto;
71
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
72
- transform: scale(0.95);
73
- opacity: 0;
74
- transition: opacity 1s ease, transform 1s ease;
75
- &.show {
76
- transform: scale(1);
77
- opacity: 1;
78
- }
79
- }
80
-
81
- .popover-content {
82
- position: relative;
83
- }
84
-
85
- .popover-close-button {
86
- position: absolute;
87
- top: 0.5rem;
88
- right: 0.75rem;
89
- background: none;
90
- border: none;
91
- font-size: 1.5rem;
92
- cursor: pointer;
93
- }
94
-
95
- .popover-buttons {
96
- display: flex;
97
- justify-content: flex-end;
98
- gap: 1rem;
99
- margin-top: 1.5rem;
100
- }
101
-
102
- .popover-buttons button {
103
- background-color: inherit;
104
- transition: ease-in-out 0.3s;
105
- padding: 0.5rem 1rem;
106
- font-size: 1rem;
107
- cursor: pointer;
108
- &:hover {
109
- background-color: rgba($secondary-color, .7);
110
- color: $background-color;
111
- }
112
- }
113
-
114
- {{#if transition}}
115
- // view transition
116
- .app-view-transition {
117
- opacity: 0;
118
- transform: scale(0.95);
119
- transition: all 300ms ease;
120
-
121
- &.enter-active {
122
- opacity: 1;
123
- transform: scale(1);
124
- }
125
-
126
- &.exit-active {
127
- opacity: 0;
128
- transform: scale(0.98);
129
- }
130
- }
131
-
132
- {{/if}}