@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.
- package/CHANGELOG.md +34 -0
- package/README.md +8 -9
- package/lib/index.mjs +18 -18
- package/lib/src/context-builder.mjs +21 -16
- package/lib/src/print.mjs +1 -0
- package/lib/src/project-generator.mjs +5 -5
- package/lib/src/prompts.mjs +13 -85
- package/lib/src/version.mjs +1 -1
- package/package.json +1 -1
- package/templates/base/builder.config.js.hbs +1 -0
- package/templates/base/src/app/globals.d.ts.hbs +12 -8
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +25 -8
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +34 -20
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +34 -0
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +11 -12
- package/templates/base/src/core/base-classes/component/component.ts.hbs +13 -17
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +17 -0
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +2 -1
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +0 -33
- package/templates/base/src/core/services/router/router.ts.hbs +0 -1
- package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +35 -7
- package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +31 -8
- package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +197 -0
- package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +75 -0
- package/templates/base/src/libs/popover/popover.ts.hbs +84 -91
- package/templates/base/src/libs/snackbar/snackbar.ts.hbs +6 -3
- package/templates/base/src/styles/base/_variables.scss.hbs +1 -0
- package/templates/base/src/styles/core/_anchors.scss.hbs +10 -0
- package/templates/base/src/styles/core/_index.scss.hbs +6 -0
- package/templates/base/src/styles/core/_popover.scss.hbs +96 -0
- package/templates/base/src/styles/core/_snackbar.scss.hbs +50 -0
- package/templates/base/src/styles/main.scss.hbs +1 -1
- package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +41 -8
- package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +14 -0
- package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { View, ButtonManager, BaseButton,
|
|
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
|
|
16
|
-
private
|
|
17
|
-
private
|
|
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
|
|
33
|
+
viewName,
|
|
34
|
+
components = [],
|
|
35
|
+
catalogs = [],
|
|
36
|
+
focusTrapOptions = {}
|
|
24
37
|
}: PopoverOptions) {
|
|
25
|
-
super({template, tagName: 'div', mountTarget: 'body'
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
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.
|
|
46
|
-
this.appendFooterButtons(content);
|
|
70
|
+
const { wrapper, overlay } = this.createPopoverDomElements();
|
|
47
71
|
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
document.body.appendChild(this.overlay);
|
|
72
|
+
this._focusTrap = this.createFocusTrap(wrapper, overlay);
|
|
73
|
+
this._focusTrap.activate();
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
this.
|
|
54
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
108
|
+
private createPopoverDomElements(): IPopoverDomElements {
|
|
108
109
|
const content = super.render();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
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
|
-
|
|
48
|
-
|
|
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 = `
|
|
@@ -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
|
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.
|
|
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
|
|
64
|
-
const lang = this._LANGUAGES.includes(
|
|
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
|
|
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}}
|