@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,285 +0,0 @@
1
- :host {
2
- display: flex;
3
- flex-direction: column;
4
- min-height: 0;
5
- width: 100%;
6
- height: 100%;
7
- color: var(--pos-color-text);
8
- font-family: var(--pos-font-family);
9
- font-size: var(--pos-font-size-md);
10
- }
11
-
12
- .pos-basket__panel {
13
- display: flex;
14
- flex-direction: column;
15
- min-height: 0;
16
- height: 100%;
17
- padding: var(--pos-space-lg);
18
- border: 1px solid var(--pos-color-border);
19
- border-radius: var(--pos-radius-lg);
20
- background: var(--pos-color-panel);
21
- }
22
-
23
- .pos-basket__header {
24
- display: flex;
25
- align-items: baseline;
26
- justify-content: space-between;
27
- gap: var(--pos-space-md);
28
- margin-bottom: var(--pos-space-md);
29
- }
30
-
31
- .pos-basket__title {
32
- margin: 0;
33
- font-family: var(--pos-font-family-display);
34
- font-size: var(--pos-font-size-xl);
35
- font-weight: var(--pos-font-weight-bold);
36
- letter-spacing: -0.03em;
37
- }
38
-
39
- .pos-basket__count {
40
- margin: 0;
41
- color: var(--pos-color-text-muted);
42
- font-size: var(--pos-font-size-sm);
43
- font-weight: var(--pos-font-weight-semibold);
44
- }
45
-
46
- .pos-basket__lines {
47
- display: flex;
48
- flex: 1 1 auto;
49
- flex-direction: column;
50
- gap: var(--pos-space-sm);
51
- min-height: 8rem;
52
- margin: 0;
53
- padding: 0;
54
- overflow-y: auto;
55
- list-style: none;
56
- }
57
-
58
- .pos-basket__empty {
59
- margin: auto 0;
60
- padding: var(--pos-space-2xl) var(--pos-space-md);
61
- color: var(--pos-color-text-muted);
62
- text-align: center;
63
- }
64
-
65
- .pos-basket__line {
66
- display: grid;
67
- grid-template-columns: minmax(0, 1fr) auto;
68
- grid-template-areas:
69
- 'info void'
70
- 'stepper prices';
71
- align-items: center;
72
- gap: var(--pos-space-sm) var(--pos-space-md);
73
- padding: var(--pos-space-md);
74
- border: 1px solid var(--pos-color-border);
75
- border-radius: var(--pos-radius-md);
76
- background: var(--pos-color-panel-elevated);
77
- }
78
-
79
- .pos-basket__line-info {
80
- grid-area: info;
81
- min-width: 0;
82
- }
83
-
84
- .pos-basket__line-name {
85
- margin: 0;
86
- font-size: var(--pos-font-size-md);
87
- font-weight: var(--pos-font-weight-semibold);
88
- overflow: hidden;
89
- text-overflow: ellipsis;
90
- white-space: nowrap;
91
- }
92
-
93
- .pos-basket__line-sku,
94
- .pos-basket__line-voided {
95
- margin: var(--pos-space-xs) 0 0;
96
- font-size: var(--pos-font-size-sm);
97
- }
98
-
99
- .pos-basket__line-sku {
100
- color: var(--pos-color-text-muted);
101
- }
102
-
103
- .pos-basket__line-voided {
104
- color: var(--pos-color-danger);
105
- font-weight: var(--pos-font-weight-semibold);
106
- }
107
-
108
- .pos-basket__stepper {
109
- grid-area: stepper;
110
- display: inline-flex;
111
- align-items: center;
112
- gap: var(--pos-space-xs);
113
- width: fit-content;
114
- padding: var(--pos-space-xs);
115
- border: 1px solid var(--pos-color-border);
116
- border-radius: var(--pos-radius-md);
117
- background: var(--pos-color-surface);
118
- }
119
-
120
- .pos-basket__qty-btn,
121
- .pos-basket__void {
122
- display: inline-flex;
123
- align-items: center;
124
- justify-content: center;
125
- margin: 0;
126
- border: 0;
127
- background: transparent;
128
- color: var(--pos-color-text);
129
- font: inherit;
130
- cursor: pointer;
131
- -webkit-tap-highlight-color: transparent;
132
- }
133
-
134
- .pos-basket__qty-btn {
135
- min-width: 2.75rem;
136
- min-height: 2.75rem;
137
- border-radius: var(--pos-radius-sm);
138
- font-size: var(--pos-font-size-lg);
139
- font-weight: var(--pos-font-weight-bold);
140
- }
141
-
142
- .pos-basket__qty-btn:not(:disabled):active,
143
- .pos-basket__void:not(:disabled):active {
144
- background: var(--pos-color-key-active);
145
- color: var(--pos-color-on-key-active);
146
- }
147
-
148
- .pos-basket__qty {
149
- min-width: 1.75rem;
150
- font-family: var(--pos-font-family-display);
151
- font-weight: var(--pos-font-weight-bold);
152
- text-align: center;
153
- }
154
-
155
- .pos-basket__line-prices {
156
- grid-area: prices;
157
- display: flex;
158
- flex-direction: column;
159
- align-items: flex-end;
160
- gap: var(--pos-space-xs);
161
- }
162
-
163
- .pos-basket__unit-price {
164
- margin: 0;
165
- color: var(--pos-color-text-muted);
166
- font-size: var(--pos-font-size-sm);
167
- }
168
-
169
- .pos-basket__line-total {
170
- margin: 0;
171
- font-family: var(--pos-font-family-display);
172
- font-weight: var(--pos-font-weight-bold);
173
- }
174
-
175
- .pos-basket__void {
176
- grid-area: void;
177
- justify-self: end;
178
- min-width: 2.75rem;
179
- min-height: 2.75rem;
180
- border: 1px solid var(--pos-color-border);
181
- border-radius: var(--pos-radius-md);
182
- color: var(--pos-color-text-muted);
183
- }
184
-
185
- .pos-basket__qty-btn:disabled,
186
- .pos-basket__void:disabled,
187
- .pos-basket__clear:disabled {
188
- cursor: not-allowed;
189
- opacity: 0.55;
190
- }
191
-
192
- .pos-basket__qty-btn:focus-visible,
193
- .pos-basket__void:focus-visible,
194
- .pos-basket__clear:focus-visible {
195
- outline: 2px solid var(--pos-color-primary);
196
- outline-offset: 2px;
197
- }
198
-
199
- .pos-basket__line--voided .pos-basket__line-name,
200
- .pos-basket__line--voided .pos-basket__line-sku,
201
- .pos-basket__line--voided .pos-basket__line-prices {
202
- color: var(--pos-color-text-muted);
203
- text-decoration: line-through;
204
- }
205
-
206
- .pos-basket__footer {
207
- display: flex;
208
- flex-direction: column;
209
- gap: var(--pos-space-md);
210
- margin-top: var(--pos-space-md);
211
- padding-top: var(--pos-space-md);
212
- border-top: 1px solid var(--pos-color-border);
213
- }
214
-
215
- .pos-basket__totals {
216
- display: flex;
217
- flex-direction: column;
218
- gap: var(--pos-space-sm);
219
- margin: 0;
220
- }
221
-
222
- .pos-basket__total-row {
223
- display: flex;
224
- justify-content: space-between;
225
- gap: var(--pos-space-md);
226
- margin: 0;
227
- color: var(--pos-color-text-muted);
228
- font-size: var(--pos-font-size-sm);
229
- }
230
-
231
- .pos-basket__total-row dt,
232
- .pos-basket__total-row dd {
233
- margin: 0;
234
- }
235
-
236
- .pos-basket__total-row--due {
237
- padding-top: var(--pos-space-sm);
238
- border-top: 1px solid var(--pos-color-border);
239
- color: var(--pos-color-text);
240
- font-family: var(--pos-font-family-display);
241
- font-size: var(--pos-font-size-lg);
242
- font-weight: var(--pos-font-weight-bold);
243
- }
244
-
245
- .pos-basket__clear {
246
- min-height: var(--pos-button-min-height);
247
- border: 0;
248
- border-radius: var(--pos-radius-md);
249
- background: var(--pos-color-danger);
250
- color: var(--pos-color-on-danger);
251
- font: inherit;
252
- font-weight: var(--pos-font-weight-bold);
253
- letter-spacing: 0.04em;
254
- text-transform: uppercase;
255
- cursor: pointer;
256
- -webkit-tap-highlight-color: transparent;
257
- }
258
-
259
- .pos-basket__clear:not(:disabled):active {
260
- transform: translateY(1px);
261
- }
262
-
263
- .pos-basket__clear:disabled {
264
- background: var(--pos-color-disabled);
265
- color: var(--pos-color-on-disabled);
266
- transform: none;
267
- }
268
-
269
- @media (min-width: 768px) {
270
- .pos-basket__panel {
271
- padding: var(--pos-space-xl);
272
- }
273
-
274
- .pos-basket__line {
275
- grid-template-columns: minmax(0, 1fr) auto auto auto;
276
- grid-template-areas: 'info stepper prices void';
277
- gap: var(--pos-space-md);
278
- }
279
- }
280
-
281
- @media (prefers-reduced-motion: reduce) {
282
- .pos-basket__clear {
283
- transform: none;
284
- }
285
- }
@@ -1,198 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
-
3
- import {
4
- PosBasket,
5
- PosBasketIntent,
6
- PosBasketLine,
7
- PosBasketTotals,
8
- } from './pos-basket';
9
-
10
- const COFFEE_LINE: PosBasketLine = {
11
- lineId: 1,
12
- sku: 'SKU-001',
13
- name: 'Coffee 250g',
14
- qty: 2,
15
- unitPrice: 6,
16
- lineTotal: 12,
17
- vat: { rate: 0.25, amount: 2.4, included: true },
18
- struckThrough: false,
19
- };
20
-
21
- const MID_SALE_TOTALS: PosBasketTotals = {
22
- gross: 12,
23
- net: 9.6,
24
- vat: 2.4,
25
- discount: 0,
26
- due: 12,
27
- currency: 'EUR',
28
- };
29
-
30
- describe('PosBasket', () => {
31
- let fixture: ComponentFixture<PosBasket>;
32
- let component: PosBasket;
33
-
34
- beforeEach(async () => {
35
- await TestBed.configureTestingModule({
36
- imports: [PosBasket],
37
- }).compileComponents();
38
-
39
- fixture = TestBed.createComponent(PosBasket);
40
- component = fixture.componentInstance;
41
- await fixture.whenStable();
42
- });
43
-
44
- it('should render the empty basket state by default', () => {
45
- const compiled = fixture.nativeElement as HTMLElement;
46
- expect(compiled.querySelector('.pos-basket__title')?.textContent).toContain(
47
- 'Basket',
48
- );
49
- expect(compiled.querySelector('.pos-basket__empty')?.textContent).toContain(
50
- 'Basket is empty',
51
- );
52
- });
53
-
54
- it('should render lines and totals exactly as received without calculating', async () => {
55
- const mismatched: PosBasketLine = {
56
- ...COFFEE_LINE,
57
- qty: 2,
58
- unitPrice: 6,
59
- lineTotal: 99,
60
- };
61
- const totals: PosBasketTotals = {
62
- ...MID_SALE_TOTALS,
63
- gross: 50,
64
- due: 40,
65
- };
66
-
67
- fixture.componentRef.setInput('lines', [mismatched]);
68
- fixture.componentRef.setInput('totals', totals);
69
- await fixture.whenStable();
70
-
71
- const compiled = fixture.nativeElement as HTMLElement;
72
- expect(
73
- compiled.querySelector('.pos-basket__line-name')?.textContent,
74
- ).toContain('Coffee 250g');
75
- expect(
76
- compiled.querySelector('.pos-basket__line-sku')?.textContent,
77
- ).toContain('SKU-001');
78
- expect(compiled.querySelector('.pos-basket__qty')?.textContent?.trim()).toBe(
79
- '2',
80
- );
81
- expect(
82
- compiled.querySelector('.pos-basket__line-total')?.textContent,
83
- ).toContain('99');
84
- expect(
85
- compiled.querySelector('.pos-basket__line-total')?.textContent,
86
- ).not.toContain('12');
87
-
88
- const due = compiled.querySelector('.pos-basket__total-row--due dd');
89
- expect(due?.textContent).toContain('40');
90
- expect(due?.textContent).not.toContain('50');
91
- });
92
-
93
- it('should emit change-quantity when the cashier increments a line', async () => {
94
- const intents: PosBasketIntent[] = [];
95
- component.action.subscribe((intent) => intents.push(intent));
96
-
97
- fixture.componentRef.setInput('lines', [COFFEE_LINE]);
98
- fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
99
- await fixture.whenStable();
100
-
101
- const increase = (
102
- fixture.nativeElement as HTMLElement
103
- ).querySelectorAll('.pos-basket__qty-btn')[1] as HTMLButtonElement;
104
- increase.click();
105
- await fixture.whenStable();
106
-
107
- expect(intents).toEqual([
108
- { type: 'basket.changeQuantity', lineId: 1, qty: 3 },
109
- ]);
110
- });
111
-
112
- it('should emit change-quantity when the cashier decrements a line', async () => {
113
- const intents: PosBasketIntent[] = [];
114
- component.action.subscribe((intent) => intents.push(intent));
115
-
116
- fixture.componentRef.setInput('lines', [COFFEE_LINE]);
117
- fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
118
- await fixture.whenStable();
119
-
120
- const decrease = (
121
- fixture.nativeElement as HTMLElement
122
- ).querySelector('.pos-basket__qty-btn') as HTMLButtonElement;
123
- decrease.click();
124
- await fixture.whenStable();
125
-
126
- expect(intents).toEqual([
127
- { type: 'basket.changeQuantity', lineId: 1, qty: 1 },
128
- ]);
129
- });
130
-
131
- it('should emit void-line for the selected line', async () => {
132
- const intents: PosBasketIntent[] = [];
133
- component.action.subscribe((intent) => intents.push(intent));
134
-
135
- fixture.componentRef.setInput('lines', [COFFEE_LINE]);
136
- fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
137
- await fixture.whenStable();
138
-
139
- const voidBtn = (fixture.nativeElement as HTMLElement).querySelector(
140
- '.pos-basket__void',
141
- ) as HTMLButtonElement;
142
- voidBtn.click();
143
- await fixture.whenStable();
144
-
145
- expect(intents).toEqual([{ type: 'basket.voidLine', lineId: 1 }]);
146
- });
147
-
148
- it('should emit clear-basket when the cashier clears', async () => {
149
- const intents: PosBasketIntent[] = [];
150
- component.action.subscribe((intent) => intents.push(intent));
151
-
152
- fixture.componentRef.setInput('lines', [COFFEE_LINE]);
153
- fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
154
- await fixture.whenStable();
155
-
156
- const clear = (fixture.nativeElement as HTMLElement).querySelector(
157
- '.pos-basket__clear',
158
- ) as HTMLButtonElement;
159
- clear.click();
160
- await fixture.whenStable();
161
-
162
- expect(intents).toEqual([{ type: 'basket.clear' }]);
163
- });
164
-
165
- it('should not emit while disabled, and should disable void on struck-through lines', async () => {
166
- const intents: PosBasketIntent[] = [];
167
- component.action.subscribe((intent) => intents.push(intent));
168
-
169
- fixture.componentRef.setInput('lines', [
170
- { ...COFFEE_LINE, struckThrough: true },
171
- ]);
172
- fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
173
- fixture.componentRef.setInput('disabled', true);
174
- await fixture.whenStable();
175
-
176
- const compiled = fixture.nativeElement as HTMLElement;
177
- const increase = compiled.querySelectorAll(
178
- '.pos-basket__qty-btn',
179
- )[1] as HTMLButtonElement;
180
- const voidBtn = compiled.querySelector(
181
- '.pos-basket__void',
182
- ) as HTMLButtonElement;
183
- const clear = compiled.querySelector(
184
- '.pos-basket__clear',
185
- ) as HTMLButtonElement;
186
-
187
- increase.click();
188
- voidBtn.click();
189
- clear.click();
190
- await fixture.whenStable();
191
-
192
- expect(increase.disabled).toBe(true);
193
- expect(voidBtn.disabled).toBe(true);
194
- expect(clear.disabled).toBe(true);
195
- expect(intents).toEqual([]);
196
- expect(compiled.querySelector('.pos-basket__line--voided')).toBeTruthy();
197
- });
198
- });
@@ -1,114 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/angular-vite';
2
-
3
- import { PosBasket, PosBasketLine, PosBasketTotals } from './pos-basket';
4
-
5
- const coffee: PosBasketLine = {
6
- lineId: 1,
7
- sku: 'SKU-001',
8
- name: 'Coffee 250g',
9
- qty: 2,
10
- unitPrice: 6,
11
- lineTotal: 12,
12
- vat: { rate: 0.25, amount: 2.4, included: true },
13
- struckThrough: false,
14
- };
15
-
16
- const pastry: PosBasketLine = {
17
- lineId: 2,
18
- sku: 'SKU-014',
19
- name: 'Almond croissant',
20
- qty: 1,
21
- unitPrice: 3.5,
22
- lineTotal: 3.5,
23
- vat: { rate: 0.2, amount: 0.58, included: true },
24
- struckThrough: false,
25
- };
26
-
27
- const midSaleTotals: PosBasketTotals = {
28
- gross: 12,
29
- net: 9.6,
30
- vat: 2.4,
31
- discount: 0,
32
- due: 12,
33
- currency: 'EUR',
34
- };
35
-
36
- const meta: Meta<PosBasket> = {
37
- title: 'Basket',
38
- component: PosBasket,
39
- args: {
40
- lines: [],
41
- totals: {
42
- gross: 0,
43
- net: 0,
44
- vat: 0,
45
- discount: 0,
46
- due: 0,
47
- currency: 'EUR',
48
- },
49
- disabled: false,
50
- },
51
- };
52
-
53
- export default meta;
54
-
55
- type Story = StoryObj<PosBasket>;
56
-
57
- export const Empty: Story = {};
58
-
59
- export const MidSale: Story = {
60
- args: {
61
- lines: [coffee],
62
- totals: midSaleTotals,
63
- },
64
- };
65
-
66
- export const MultipleLines: Story = {
67
- args: {
68
- lines: [coffee, pastry],
69
- totals: {
70
- gross: 15.5,
71
- net: 12.52,
72
- vat: 2.98,
73
- discount: 0,
74
- due: 15.5,
75
- currency: 'EUR',
76
- },
77
- },
78
- };
79
-
80
- export const VoidedLine: Story = {
81
- args: {
82
- lines: [{ ...coffee, struckThrough: true }, pastry],
83
- totals: {
84
- gross: 3.5,
85
- net: 2.92,
86
- vat: 0.58,
87
- discount: 0,
88
- due: 3.5,
89
- currency: 'EUR',
90
- },
91
- },
92
- };
93
-
94
- export const AsReceivedNotCalculated: Story = {
95
- args: {
96
- lines: [{ ...coffee, qty: 2, unitPrice: 6, lineTotal: 99 }],
97
- totals: {
98
- gross: 50,
99
- net: 40,
100
- vat: 10,
101
- discount: 5,
102
- due: 45,
103
- currency: 'EUR',
104
- },
105
- },
106
- };
107
-
108
- export const Disabled: Story = {
109
- args: {
110
- lines: [coffee],
111
- totals: midSaleTotals,
112
- disabled: true,
113
- },
114
- };