@retalia/pos-components 0.0.4 → 0.0.6

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/README.md +24 -1
  2. package/fesm2022/retalia-pos-components-services.mjs +171 -0
  3. package/fesm2022/retalia-pos-components-services.mjs.map +1 -0
  4. package/fesm2022/retalia-pos-components.mjs +1089 -0
  5. package/fesm2022/retalia-pos-components.mjs.map +1 -0
  6. package/package.json +43 -17
  7. package/types/retalia-pos-components-services.d.ts +168 -0
  8. package/types/retalia-pos-components.d.ts +300 -0
  9. package/eslint.config.js +0 -32
  10. package/ng-package.json +0 -14
  11. package/src/lib/basket/pos-basket.html +0 -124
  12. package/src/lib/basket/pos-basket.scss +0 -285
  13. package/src/lib/basket/pos-basket.spec.ts +0 -198
  14. package/src/lib/basket/pos-basket.stories.ts +0 -114
  15. package/src/lib/basket/pos-basket.ts +0 -142
  16. package/src/lib/item-entry/pos-item-entry.html +0 -41
  17. package/src/lib/item-entry/pos-item-entry.scss +0 -145
  18. package/src/lib/item-entry/pos-item-entry.spec.ts +0 -123
  19. package/src/lib/item-entry/pos-item-entry.stories.ts +0 -31
  20. package/src/lib/item-entry/pos-item-entry.ts +0 -81
  21. package/src/lib/login/pos-login.html +0 -82
  22. package/src/lib/login/pos-login.scss +0 -238
  23. package/src/lib/login/pos-login.spec.ts +0 -89
  24. package/src/lib/login/pos-login.stories.ts +0 -36
  25. package/src/lib/login/pos-login.ts +0 -150
  26. package/src/lib/receipt/pos-receipt.html +0 -116
  27. package/src/lib/receipt/pos-receipt.scss +0 -367
  28. package/src/lib/receipt/pos-receipt.spec.ts +0 -188
  29. package/src/lib/receipt/pos-receipt.stories.ts +0 -163
  30. package/src/lib/receipt/pos-receipt.ts +0 -129
  31. package/src/lib/tender/pos-tender.html +0 -79
  32. package/src/lib/tender/pos-tender.scss +0 -318
  33. package/src/lib/tender/pos-tender.spec.ts +0 -150
  34. package/src/lib/tender/pos-tender.stories.ts +0 -47
  35. package/src/lib/tender/pos-tender.ts +0 -154
  36. package/src/lib/tokens/pos-theme-css.spec.ts +0 -75
  37. package/src/lib/tokens/pos-theme-css.ts +0 -123
  38. package/src/lib/tokens/pos-token-catalog.ts +0 -514
  39. package/src/lib/tokens/pos-tokens.html +0 -351
  40. package/src/lib/tokens/pos-tokens.scss +0 -437
  41. package/src/lib/tokens/pos-tokens.spec.ts +0 -164
  42. package/src/lib/tokens/pos-tokens.stories.ts +0 -22
  43. package/src/lib/tokens/pos-tokens.ts +0 -208
  44. package/src/public-api.ts +0 -37
  45. package/tsconfig.lib.json +0 -14
  46. package/tsconfig.lib.prod.json +0 -11
  47. package/tsconfig.spec.json +0 -11
  48. /package/{src/styles → styles}/styles.css +0 -0
  49. /package/{src/styles → styles}/tokens.css +0 -0
@@ -1,238 +0,0 @@
1
- :host {
2
- display: block;
3
- width: 100%;
4
- max-width: 26rem;
5
- color: var(--pos-color-text);
6
- font-family: var(--pos-font-family);
7
- font-size: var(--pos-font-size-md);
8
- }
9
-
10
- .pos-login__panel {
11
- position: relative;
12
- display: flex;
13
- flex-direction: column;
14
- min-height: 100%;
15
- padding: var(--pos-space-xl);
16
- border: 1px solid var(--pos-color-border);
17
- border-radius: var(--pos-radius-lg);
18
- background:
19
- radial-gradient(120% 80% at 100% 0%, rgb(0 194 178 / 0.08), transparent 55%),
20
- var(--pos-color-panel);
21
- }
22
-
23
- .pos-login__header {
24
- position: relative;
25
- display: flex;
26
- flex-direction: column;
27
- align-items: flex-start;
28
- gap: var(--pos-space-sm);
29
- padding-right: var(--pos-space-2xl);
30
- }
31
-
32
- .pos-login__kicker {
33
- margin: 0;
34
- color: var(--pos-color-primary);
35
- font-size: var(--pos-font-size-sm);
36
- font-weight: var(--pos-font-weight-bold);
37
- letter-spacing: 0.14em;
38
- text-transform: uppercase;
39
- }
40
-
41
- .pos-login__title {
42
- margin: 0;
43
- max-width: 14ch;
44
- font-family: var(--pos-font-family-display);
45
- font-size: var(--pos-font-size-xl);
46
- font-weight: var(--pos-font-weight-bold);
47
- letter-spacing: -0.03em;
48
- line-height: 1.15;
49
- }
50
-
51
- .pos-login__status {
52
- min-height: 1.5rem;
53
- margin: var(--pos-space-md) 0 var(--pos-space-sm);
54
- display: flex;
55
- flex-direction: column;
56
- gap: var(--pos-space-sm);
57
- }
58
-
59
- .pos-login__status p {
60
- margin: 0;
61
- font-size: var(--pos-font-size-sm);
62
- font-weight: var(--pos-font-weight-semibold);
63
- }
64
-
65
- .pos-login__error {
66
- padding: var(--pos-space-sm) var(--pos-space-md);
67
- border-left: 3px solid var(--pos-color-danger);
68
- background: rgb(214 69 69 / 0.08);
69
- color: var(--pos-color-danger);
70
- }
71
-
72
- .pos-login__key:disabled,
73
- .pos-login__back:disabled {
74
- cursor: not-allowed;
75
- opacity: 0.55;
76
- }
77
-
78
- .pos-login__body {
79
- display: flex;
80
- flex: 1 1 auto;
81
- flex-direction: column;
82
- gap: var(--pos-space-xl);
83
- width: 100%;
84
- }
85
-
86
- .pos-login__display {
87
- border: 1px solid var(--pos-color-border);
88
- border-radius: var(--pos-radius-lg);
89
- background: var(--pos-color-display);
90
- box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.06);
91
- padding: var(--pos-space-lg) var(--pos-space-xl);
92
- }
93
-
94
- .pos-login__input {
95
- display: block;
96
- width: 100%;
97
- margin: 0;
98
- border: 0;
99
- background: transparent;
100
- color: var(--pos-color-on-display);
101
- font-family: var(--pos-font-family-display);
102
- font-size: clamp(1.35rem, 3vw, 1.85rem);
103
- font-weight: var(--pos-font-weight-semibold);
104
- letter-spacing: var(--pos-letter-spacing-display);
105
- text-align: center;
106
- outline: none;
107
- }
108
-
109
- .pos-login__input::placeholder {
110
- color: rgb(232 237 242 / 0.35);
111
- letter-spacing: 0.45em;
112
- }
113
-
114
- .pos-login__keypad {
115
- display: flex;
116
- flex-direction: column;
117
- gap: var(--pos-space-md);
118
- }
119
-
120
- .pos-login__keypad-row {
121
- display: grid;
122
- grid-template-columns: repeat(3, minmax(0, 1fr));
123
- gap: var(--pos-space-md);
124
- }
125
-
126
- .pos-login__key {
127
- display: inline-flex;
128
- align-items: center;
129
- justify-content: center;
130
- min-height: var(--pos-keypad-size);
131
- border: 1px solid var(--pos-color-key-border);
132
- border-radius: var(--pos-radius-md);
133
- background: var(--pos-color-key);
134
- color: var(--pos-color-text);
135
- font-family: inherit;
136
- font-size: 1.15rem;
137
- font-weight: var(--pos-font-weight-bold);
138
- cursor: pointer;
139
- transition:
140
- background 120ms ease,
141
- color 120ms ease,
142
- border-color 120ms ease,
143
- transform 120ms ease;
144
- -webkit-tap-highlight-color: transparent;
145
- }
146
-
147
- .pos-login__key--action {
148
- color: var(--pos-color-text-muted);
149
- background: var(--pos-color-surface-muted);
150
- }
151
-
152
- .pos-login__key:active {
153
- background: var(--pos-color-key-active);
154
- border-color: var(--pos-color-key-active);
155
- color: var(--pos-color-on-key-active);
156
- transform: translateY(1px);
157
- }
158
-
159
- .pos-login__actions {
160
- display: flex;
161
- align-items: stretch;
162
- gap: var(--pos-space-md);
163
- margin-top: auto;
164
- }
165
-
166
- .pos-login__back {
167
- display: inline-flex;
168
- flex: 0 0 auto;
169
- align-items: center;
170
- gap: var(--pos-space-xs);
171
- min-height: var(--pos-button-min-height);
172
- padding: 0 var(--pos-space-lg);
173
- border: 1px solid var(--pos-color-border);
174
- border-radius: var(--pos-radius-md);
175
- background: var(--pos-color-panel-elevated);
176
- color: var(--pos-color-text);
177
- font: inherit;
178
- font-weight: var(--pos-font-weight-semibold);
179
- cursor: pointer;
180
- }
181
-
182
- .pos-login__primary {
183
- flex: 1 1 auto;
184
- min-height: var(--pos-button-min-height);
185
- border: 0;
186
- border-radius: var(--pos-radius-md);
187
- background: var(--pos-color-primary);
188
- color: var(--pos-color-on-primary);
189
- font: inherit;
190
- font-size: 1.05rem;
191
- font-weight: var(--pos-font-weight-bold);
192
- letter-spacing: 0.04em;
193
- text-transform: uppercase;
194
- cursor: pointer;
195
- transition: filter 120ms ease, transform 120ms ease;
196
- -webkit-tap-highlight-color: transparent;
197
- }
198
-
199
- .pos-login__primary:not(:disabled):hover {
200
- filter: brightness(0.97);
201
- }
202
-
203
- .pos-login__primary:not(:disabled):active {
204
- transform: translateY(1px);
205
- }
206
-
207
- .pos-login__primary--disabled,
208
- .pos-login__primary:disabled {
209
- background: var(--pos-color-disabled);
210
- color: var(--pos-color-on-disabled);
211
- cursor: not-allowed;
212
- filter: none;
213
- transform: none;
214
- }
215
-
216
- @media (min-width: 768px) {
217
- .pos-login__panel {
218
- padding: var(--pos-space-2xl);
219
- }
220
-
221
- .pos-login__key {
222
- min-height: var(--pos-keypad-size-md);
223
- }
224
- }
225
-
226
- @media (min-width: 1100px) {
227
- .pos-login__key {
228
- min-height: var(--pos-keypad-size-lg);
229
- font-size: 1.25rem;
230
- }
231
- }
232
-
233
- @media (prefers-reduced-motion: reduce) {
234
- .pos-login__key,
235
- .pos-login__primary {
236
- transition: none;
237
- }
238
- }
@@ -1,89 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
-
3
- import { PosLogin, PosLoginIntent } from './pos-login';
4
-
5
- describe('PosLogin', () => {
6
- let fixture: ComponentFixture<PosLogin>;
7
- let component: PosLogin;
8
-
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
11
- imports: [PosLogin],
12
- }).compileComponents();
13
-
14
- fixture = TestBed.createComponent(PosLogin);
15
- component = fixture.componentInstance;
16
- await fixture.whenStable();
17
- });
18
-
19
- it('should render the clerk number title by default', () => {
20
- const compiled = fixture.nativeElement as HTMLElement;
21
- expect(compiled.querySelector('.pos-login__title')?.textContent).toContain(
22
- 'Enter Clerk Number',
23
- );
24
- });
25
-
26
- it('should emit next with the clerk number after keypad entry', async () => {
27
- const intents: PosLoginIntent[] = [];
28
- component.action.subscribe((intent) => intents.push(intent));
29
-
30
- const keys = Array.from(
31
- (fixture.nativeElement as HTMLElement).querySelectorAll('.pos-login__key'),
32
- ) as HTMLButtonElement[];
33
- keys.find((button) => button.textContent?.trim() === '1')?.click();
34
- keys.find((button) => button.textContent?.trim() === '2')?.click();
35
- await fixture.whenStable();
36
-
37
- const primary = (fixture.nativeElement as HTMLElement).querySelector(
38
- '.pos-login__primary',
39
- ) as HTMLButtonElement;
40
- primary.click();
41
- await fixture.whenStable();
42
-
43
- expect(
44
- intents.some(
45
- (intent) => intent.type === 'login.next' && intent.clerkNumber === '12',
46
- ),
47
- ).toBe(true);
48
- expect(
49
- (fixture.nativeElement as HTMLElement).querySelector('.pos-login__title')
50
- ?.textContent,
51
- ).toContain('Enter Clerk Password');
52
- });
53
-
54
- it('should display session state from the host without calling a service', async () => {
55
- fixture.componentRef.setInput('session', {
56
- status: 'signedIn',
57
- displayName: 'Alex Rivera',
58
- });
59
- await fixture.whenStable();
60
-
61
- expect(
62
- (fixture.nativeElement as HTMLElement).querySelector('.pos-login__status')
63
- ?.textContent,
64
- ).toContain('Signed in as Alex Rivera');
65
- });
66
-
67
- it('should not emit submit while session is busy', async () => {
68
- const intents: PosLoginIntent[] = [];
69
- component.action.subscribe((intent) => intents.push(intent));
70
-
71
- fixture.componentRef.setInput('session', { status: 'busy' });
72
- await fixture.whenStable();
73
-
74
- const keys = Array.from(
75
- (fixture.nativeElement as HTMLElement).querySelectorAll('.pos-login__key'),
76
- ) as HTMLButtonElement[];
77
- keys.find((button) => button.textContent?.trim() === '1')?.click();
78
- const primary = (fixture.nativeElement as HTMLElement).querySelector(
79
- '.pos-login__primary',
80
- ) as HTMLButtonElement;
81
- primary.click();
82
- await fixture.whenStable();
83
-
84
- expect(intents.some((intent) => intent.type === 'login.submit')).toBe(
85
- false,
86
- );
87
- expect(primary.disabled).toBe(true);
88
- });
89
- });
@@ -1,36 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/angular-vite';
2
-
3
- import { PosLogin } from './pos-login';
4
-
5
- const meta: Meta<PosLogin> = {
6
- title: 'Login',
7
- component: PosLogin,
8
- args: {
9
- errorMessage: '',
10
- session: { status: 'signedOut' },
11
- },
12
- };
13
-
14
- export default meta;
15
-
16
- type Story = StoryObj<PosLogin>;
17
-
18
- export const Default: Story = {};
19
-
20
- export const Busy: Story = {
21
- args: {
22
- session: { status: 'busy' },
23
- },
24
- };
25
-
26
- export const SignedIn: Story = {
27
- args: {
28
- session: { status: 'signedIn', displayName: 'Alex Rivera' },
29
- },
30
- };
31
-
32
- export const Error: Story = {
33
- args: {
34
- errorMessage: 'Invalid clerk or password',
35
- },
36
- };
@@ -1,150 +0,0 @@
1
- import {
2
- ChangeDetectionStrategy,
3
- Component,
4
- computed,
5
- input,
6
- output,
7
- signal,
8
- } from '@angular/core';
9
-
10
- export type PosLoginIntent =
11
- | { readonly type: 'login.key'; readonly key: string }
12
- | { readonly type: 'login.backspace' }
13
- | { readonly type: 'login.next'; readonly clerkNumber: string }
14
- | { readonly type: 'login.submit'; readonly clerkNumber: string; readonly password: string }
15
- | { readonly type: 'login.back' };
16
-
17
- /** Display-only login/session state from the host envelope — not an API DTO. */
18
- export type PosLoginSessionStatus = 'signedOut' | 'busy' | 'signedIn';
19
-
20
- export interface PosLoginSessionState {
21
- readonly status: PosLoginSessionStatus;
22
- readonly displayName?: string;
23
- }
24
-
25
- const DEFAULT_SESSION: PosLoginSessionState = { status: 'signedOut' };
26
-
27
- type LoginStage = 'clerkNumber' | 'password';
28
-
29
- const KEYPAD_ROWS: readonly (readonly string[])[] = [
30
- ['1', '2', '3'],
31
- ['4', '5', '6'],
32
- ['7', '8', '9'],
33
- ['0', '.', '←'],
34
- ];
35
-
36
- @Component({
37
- selector: 'pos-login',
38
- changeDetection: ChangeDetectionStrategy.OnPush,
39
- templateUrl: './pos-login.html',
40
- styleUrl: './pos-login.scss',
41
- host: {
42
- class: 'pos-login',
43
- },
44
- })
45
- export class PosLogin {
46
- readonly errorMessage = input('');
47
- readonly session = input<PosLoginSessionState>(DEFAULT_SESSION);
48
-
49
- readonly titleClerkNumber = input('Enter Clerk Number');
50
- readonly titleClerkPassword = input('Enter Clerk Password');
51
- readonly nextLabel = input('Next');
52
- readonly loginLabel = input('Login');
53
-
54
- readonly action = output<PosLoginIntent>();
55
-
56
- protected readonly keypadRows = KEYPAD_ROWS;
57
- protected readonly inputValue = signal('');
58
- protected readonly stage = signal<LoginStage>('clerkNumber');
59
- private readonly clerkNumber = signal('');
60
-
61
- protected readonly title = computed(() =>
62
- this.stage() === 'clerkNumber'
63
- ? this.titleClerkNumber()
64
- : this.titleClerkPassword(),
65
- );
66
-
67
- protected readonly maskInput = computed(() => this.stage() === 'password');
68
-
69
- protected readonly showBack = computed(() => this.stage() === 'password');
70
-
71
- protected readonly primaryLabel = computed(() =>
72
- this.stage() === 'password' ? this.loginLabel() : this.nextLabel(),
73
- );
74
-
75
- protected readonly primaryEnabled = computed(
76
- () => this.inputValue().length > 0 && !this.interactionLocked(),
77
- );
78
-
79
- protected readonly interactionLocked = computed(() => {
80
- const status = this.session().status;
81
- return status === 'busy' || status === 'signedIn';
82
- });
83
-
84
- protected readonly sessionStatusText = computed(() => {
85
- const current = this.session();
86
- if (current.status === 'busy') {
87
- return 'Signing in…';
88
- }
89
- if (current.status === 'signedIn') {
90
- return current.displayName
91
- ? `Signed in as ${current.displayName}`
92
- : 'Signed in';
93
- }
94
- return '';
95
- });
96
-
97
- protected onKeypad(key: string): void {
98
- if (this.interactionLocked()) {
99
- return;
100
- }
101
-
102
- if (key === '←') {
103
- this.inputValue.update((value) => value.slice(0, -1));
104
- this.action.emit({ type: 'login.backspace' });
105
- return;
106
- }
107
-
108
- if (this.inputValue().length >= 12) {
109
- return;
110
- }
111
-
112
- this.inputValue.update((value) => value + key);
113
- this.action.emit({ type: 'login.key', key });
114
- }
115
-
116
- protected onPrimary(): void {
117
- if (this.interactionLocked()) {
118
- return;
119
- }
120
-
121
- const value = this.inputValue();
122
- if (!value) {
123
- return;
124
- }
125
-
126
- if (this.stage() === 'clerkNumber') {
127
- this.clerkNumber.set(value);
128
- this.inputValue.set('');
129
- this.stage.set('password');
130
- this.action.emit({ type: 'login.next', clerkNumber: value });
131
- return;
132
- }
133
-
134
- this.action.emit({
135
- type: 'login.submit',
136
- clerkNumber: this.clerkNumber(),
137
- password: value,
138
- });
139
- }
140
-
141
- protected onBack(): void {
142
- if (this.interactionLocked()) {
143
- return;
144
- }
145
-
146
- this.inputValue.set(this.clerkNumber());
147
- this.stage.set('clerkNumber');
148
- this.action.emit({ type: 'login.back' });
149
- }
150
- }
@@ -1,116 +0,0 @@
1
- <section
2
- class="pos-receipt__panel"
3
- [attr.aria-busy]="disabled() || !document().receiptReady ? true : null"
4
- >
5
- <header class="pos-receipt__header">
6
- <div class="pos-receipt__heading">
7
- <p class="pos-receipt__kicker">Adaptive POS</p>
8
- <h2 class="pos-receipt__title">{{ title() }}</h2>
9
- </div>
10
- @if (hasDocumentNumber()) {
11
- <p class="pos-receipt__number" aria-label="Document number">
12
- {{ document().number }}
13
- </p>
14
- }
15
- </header>
16
-
17
- <p class="pos-receipt__status" aria-live="polite">{{ statusText() }}</p>
18
-
19
- <ul class="pos-receipt__lines" aria-label="Receipt lines">
20
- @if (!hasLines()) {
21
- <li class="pos-receipt__empty">{{ emptyMessage() }}</li>
22
- } @else {
23
- @for (line of lines(); track line.lineId) {
24
- <li>
25
- <article
26
- class="pos-receipt__line"
27
- [class.pos-receipt__line--voided]="line.struckThrough"
28
- >
29
- <div class="pos-receipt__line-info">
30
- <h3 class="pos-receipt__line-name">{{ line.name }}</h3>
31
- <p class="pos-receipt__line-sku">{{ line.sku }}</p>
32
- @if (line.struckThrough) {
33
- <p class="pos-receipt__line-voided">Voided</p>
34
- }
35
- </div>
36
- <p class="pos-receipt__qty">{{ line.qty }}</p>
37
- <div class="pos-receipt__line-prices">
38
- <p class="pos-receipt__unit-price">
39
- {{ formatMoney(line.unitPrice) }}
40
- </p>
41
- <p class="pos-receipt__line-total">
42
- {{ formatMoney(line.lineTotal) }}
43
- </p>
44
- </div>
45
- </article>
46
- </li>
47
- }
48
- }
49
- </ul>
50
-
51
- <div class="pos-receipt__summary">
52
- <dl class="pos-receipt__totals" aria-live="polite">
53
- <div class="pos-receipt__row">
54
- <dt>Gross</dt>
55
- <dd>{{ formatMoney(totals().gross) }}</dd>
56
- </div>
57
- <div class="pos-receipt__row">
58
- <dt>Net</dt>
59
- <dd>{{ formatMoney(totals().net) }}</dd>
60
- </div>
61
- <div class="pos-receipt__row">
62
- <dt>VAT</dt>
63
- <dd>{{ formatMoney(totals().vat) }}</dd>
64
- </div>
65
- <div class="pos-receipt__row">
66
- <dt>Discount</dt>
67
- <dd>{{ formatMoney(totals().discount) }}</dd>
68
- </div>
69
- <div class="pos-receipt__row pos-receipt__row--due">
70
- <dt>Due</dt>
71
- <dd>{{ formatMoney(totals().due) }}</dd>
72
- </div>
73
- </dl>
74
-
75
- @if (hasPayments()) {
76
- <section class="pos-receipt__tender">
77
- <h3 class="pos-receipt__tender-title">{{ tenderLabel() }}</h3>
78
- <ul class="pos-receipt__payments">
79
- @for (payment of payments(); track $index) {
80
- <li class="pos-receipt__payment">
81
- <header class="pos-receipt__payment-header">
82
- <p class="pos-receipt__payment-tender">{{ payment.tender }}</p>
83
- <p class="pos-receipt__payment-state">{{ payment.state }}</p>
84
- </header>
85
- <dl class="pos-receipt__payment-amounts">
86
- <div class="pos-receipt__row">
87
- <dt>{{ amountLabel() }}</dt>
88
- <dd>{{ formatMoney(payment.amount) }}</dd>
89
- </div>
90
- <div class="pos-receipt__row">
91
- <dt>{{ tenderedLabel() }}</dt>
92
- <dd>{{ formatMoney(payment.tendered) }}</dd>
93
- </div>
94
- <div class="pos-receipt__row pos-receipt__row--change">
95
- <dt>{{ changeLabel() }}</dt>
96
- <dd>{{ formatMoney(payment.change) }}</dd>
97
- </div>
98
- </dl>
99
- </li>
100
- }
101
- </ul>
102
- </section>
103
- }
104
- </div>
105
-
106
- <div class="pos-receipt__actions">
107
- <button
108
- type="button"
109
- class="pos-receipt__new-sale"
110
- [disabled]="!canStartNewSale()"
111
- (click)="onNewSale()"
112
- >
113
- {{ newSaleLabel() }}
114
- </button>
115
- </div>
116
- </section>