@retalia/pos-components 0.0.4 → 0.0.5

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 (45) hide show
  1. package/fesm2022/retalia-pos-components.mjs +1089 -0
  2. package/fesm2022/retalia-pos-components.mjs.map +1 -0
  3. package/package.json +39 -17
  4. package/types/retalia-pos-components.d.ts +300 -0
  5. package/eslint.config.js +0 -32
  6. package/ng-package.json +0 -14
  7. package/src/lib/basket/pos-basket.html +0 -124
  8. package/src/lib/basket/pos-basket.scss +0 -285
  9. package/src/lib/basket/pos-basket.spec.ts +0 -198
  10. package/src/lib/basket/pos-basket.stories.ts +0 -114
  11. package/src/lib/basket/pos-basket.ts +0 -142
  12. package/src/lib/item-entry/pos-item-entry.html +0 -41
  13. package/src/lib/item-entry/pos-item-entry.scss +0 -145
  14. package/src/lib/item-entry/pos-item-entry.spec.ts +0 -123
  15. package/src/lib/item-entry/pos-item-entry.stories.ts +0 -31
  16. package/src/lib/item-entry/pos-item-entry.ts +0 -81
  17. package/src/lib/login/pos-login.html +0 -82
  18. package/src/lib/login/pos-login.scss +0 -238
  19. package/src/lib/login/pos-login.spec.ts +0 -89
  20. package/src/lib/login/pos-login.stories.ts +0 -36
  21. package/src/lib/login/pos-login.ts +0 -150
  22. package/src/lib/receipt/pos-receipt.html +0 -116
  23. package/src/lib/receipt/pos-receipt.scss +0 -367
  24. package/src/lib/receipt/pos-receipt.spec.ts +0 -188
  25. package/src/lib/receipt/pos-receipt.stories.ts +0 -163
  26. package/src/lib/receipt/pos-receipt.ts +0 -129
  27. package/src/lib/tender/pos-tender.html +0 -79
  28. package/src/lib/tender/pos-tender.scss +0 -318
  29. package/src/lib/tender/pos-tender.spec.ts +0 -150
  30. package/src/lib/tender/pos-tender.stories.ts +0 -47
  31. package/src/lib/tender/pos-tender.ts +0 -154
  32. package/src/lib/tokens/pos-theme-css.spec.ts +0 -75
  33. package/src/lib/tokens/pos-theme-css.ts +0 -123
  34. package/src/lib/tokens/pos-token-catalog.ts +0 -514
  35. package/src/lib/tokens/pos-tokens.html +0 -351
  36. package/src/lib/tokens/pos-tokens.scss +0 -437
  37. package/src/lib/tokens/pos-tokens.spec.ts +0 -164
  38. package/src/lib/tokens/pos-tokens.stories.ts +0 -22
  39. package/src/lib/tokens/pos-tokens.ts +0 -208
  40. package/src/public-api.ts +0 -37
  41. package/tsconfig.lib.json +0 -14
  42. package/tsconfig.lib.prod.json +0 -11
  43. package/tsconfig.spec.json +0 -11
  44. /package/{src/styles → styles}/styles.css +0 -0
  45. /package/{src/styles → styles}/tokens.css +0 -0
@@ -1,142 +0,0 @@
1
- import {
2
- ChangeDetectionStrategy,
3
- Component,
4
- computed,
5
- input,
6
- output,
7
- } from '@angular/core';
8
-
9
- export type PosBasketIntent =
10
- | {
11
- readonly type: 'basket.changeQuantity';
12
- readonly lineId: number;
13
- readonly qty: number;
14
- }
15
- | { readonly type: 'basket.voidLine'; readonly lineId: number }
16
- | { readonly type: 'basket.clear' };
17
-
18
- /** Display-only VAT from the host envelope — not calculated here. */
19
- export interface PosBasketVat {
20
- readonly rate: number;
21
- readonly amount: number;
22
- readonly included: boolean;
23
- }
24
-
25
- /** Display-only basket line from the host envelope — not an API DTO. */
26
- export interface PosBasketLine {
27
- readonly lineId: number;
28
- readonly sku: string;
29
- readonly name: string;
30
- readonly qty: number;
31
- readonly unitPrice: number;
32
- readonly lineTotal: number;
33
- readonly vat: PosBasketVat;
34
- readonly struckThrough: boolean;
35
- }
36
-
37
- /** Display-only totals from the host envelope — not calculated here. */
38
- export interface PosBasketTotals {
39
- readonly gross: number;
40
- readonly net: number;
41
- readonly vat: number;
42
- readonly discount: number;
43
- readonly due: number;
44
- readonly currency: string;
45
- }
46
-
47
- const EMPTY_TOTALS: PosBasketTotals = {
48
- gross: 0,
49
- net: 0,
50
- vat: 0,
51
- discount: 0,
52
- due: 0,
53
- currency: 'EUR',
54
- };
55
-
56
- @Component({
57
- selector: 'pos-basket',
58
- changeDetection: ChangeDetectionStrategy.OnPush,
59
- templateUrl: './pos-basket.html',
60
- styleUrl: './pos-basket.scss',
61
- host: {
62
- class: 'pos-basket',
63
- },
64
- })
65
- export class PosBasket {
66
- readonly lines = input<readonly PosBasketLine[]>([]);
67
- readonly totals = input<PosBasketTotals>(EMPTY_TOTALS);
68
- readonly disabled = input(false);
69
-
70
- readonly title = input('Basket');
71
- readonly emptyMessage = input('Basket is empty');
72
- readonly clearLabel = input('Clear basket');
73
-
74
- readonly action = output<PosBasketIntent>();
75
-
76
- protected readonly hasLines = computed(() => this.lines().length > 0);
77
-
78
- protected readonly canClear = computed(
79
- () => this.hasLines() && !this.disabled(),
80
- );
81
-
82
- protected formatMoney(amount: number): string {
83
- return new Intl.NumberFormat(undefined, {
84
- style: 'currency',
85
- currency: this.totals().currency || 'EUR',
86
- minimumFractionDigits: 2,
87
- maximumFractionDigits: 2,
88
- }).format(amount);
89
- }
90
-
91
- protected canChangeQuantity(line: PosBasketLine): boolean {
92
- return !this.disabled() && !line.struckThrough;
93
- }
94
-
95
- protected canDecrement(line: PosBasketLine): boolean {
96
- return this.canChangeQuantity(line) && line.qty > 1;
97
- }
98
-
99
- protected canVoid(line: PosBasketLine): boolean {
100
- return this.canChangeQuantity(line);
101
- }
102
-
103
- protected onIncrement(line: PosBasketLine): void {
104
- if (!this.canChangeQuantity(line)) {
105
- return;
106
- }
107
-
108
- this.action.emit({
109
- type: 'basket.changeQuantity',
110
- lineId: line.lineId,
111
- qty: line.qty + 1,
112
- });
113
- }
114
-
115
- protected onDecrement(line: PosBasketLine): void {
116
- if (!this.canDecrement(line)) {
117
- return;
118
- }
119
-
120
- this.action.emit({
121
- type: 'basket.changeQuantity',
122
- lineId: line.lineId,
123
- qty: line.qty - 1,
124
- });
125
- }
126
-
127
- protected onVoid(line: PosBasketLine): void {
128
- if (!this.canVoid(line)) {
129
- return;
130
- }
131
-
132
- this.action.emit({ type: 'basket.voidLine', lineId: line.lineId });
133
- }
134
-
135
- protected onClear(): void {
136
- if (!this.canClear()) {
137
- return;
138
- }
139
-
140
- this.action.emit({ type: 'basket.clear' });
141
- }
142
- }
@@ -1,41 +0,0 @@
1
- <section
2
- class="pos-item-entry__panel"
3
- [attr.aria-busy]="disabled() ? true : null"
4
- >
5
- <form class="pos-item-entry__form" (submit)="onSubmit($event)">
6
- <label class="pos-item-entry__field">
7
- <span class="pos-item-entry__label">{{ label() }}</span>
8
- <input
9
- #itemInput
10
- class="pos-item-entry__input"
11
- type="text"
12
- name="itemReference"
13
- autocomplete="off"
14
- spellcheck="false"
15
- [attr.autocorrect]="'off'"
16
- [attr.autocapitalize]="'none'"
17
- enterkeyhint="done"
18
- [placeholder]="placeholder()"
19
- [value]="reference()"
20
- [disabled]="disabled()"
21
- [attr.aria-invalid]="errorMessage() ? true : null"
22
- (input)="onInput($event)"
23
- />
24
- </label>
25
-
26
- <button
27
- type="submit"
28
- class="pos-item-entry__add"
29
- [class.pos-item-entry__add--disabled]="!canSubmit()"
30
- [disabled]="!canSubmit()"
31
- >
32
- {{ addLabel() }}
33
- </button>
34
- </form>
35
-
36
- <div class="pos-item-entry__status" aria-live="polite">
37
- @if (errorMessage()) {
38
- <p class="pos-item-entry__error">{{ errorMessage() }}</p>
39
- }
40
- </div>
41
- </section>
@@ -1,145 +0,0 @@
1
- :host {
2
- display: block;
3
- width: 100%;
4
- color: var(--pos-color-text);
5
- font-family: var(--pos-font-family);
6
- font-size: var(--pos-font-size-md);
7
- }
8
-
9
- .pos-item-entry__panel {
10
- display: flex;
11
- flex-direction: column;
12
- gap: var(--pos-space-sm);
13
- padding: var(--pos-space-lg);
14
- border: 1px solid var(--pos-color-border);
15
- border-radius: var(--pos-radius-lg);
16
- background: var(--pos-color-panel);
17
- }
18
-
19
- .pos-item-entry__form {
20
- display: flex;
21
- flex-direction: column;
22
- gap: var(--pos-space-sm);
23
- }
24
-
25
- .pos-item-entry__field {
26
- display: flex;
27
- flex: 1 1 auto;
28
- flex-direction: column;
29
- gap: var(--pos-space-xs);
30
- min-width: 0;
31
- }
32
-
33
- .pos-item-entry__label {
34
- color: var(--pos-color-text-muted);
35
- font-size: var(--pos-font-size-sm);
36
- font-weight: var(--pos-font-weight-semibold);
37
- }
38
-
39
- .pos-item-entry__input {
40
- box-sizing: border-box;
41
- width: 100%;
42
- min-height: var(--pos-button-min-height);
43
- margin: 0;
44
- padding: 0 var(--pos-space-md);
45
- border: 1px solid var(--pos-color-border);
46
- border-radius: var(--pos-radius-md);
47
- background: var(--pos-color-panel-elevated);
48
- color: var(--pos-color-text);
49
- font: inherit;
50
- font-family: var(--pos-font-family-display);
51
- font-size: var(--pos-font-size-lg);
52
- font-weight: var(--pos-font-weight-semibold);
53
- outline: none;
54
- }
55
-
56
- .pos-item-entry__input::placeholder {
57
- color: var(--pos-color-text-muted);
58
- font-weight: var(--pos-font-weight-semibold);
59
- opacity: 0.8;
60
- }
61
-
62
- .pos-item-entry__input:disabled {
63
- cursor: not-allowed;
64
- background: var(--pos-color-disabled);
65
- color: var(--pos-color-on-disabled);
66
- }
67
-
68
- .pos-item-entry__input[aria-invalid='true'] {
69
- border-color: var(--pos-color-danger);
70
- }
71
-
72
- .pos-item-entry__add {
73
- flex: 0 0 auto;
74
- min-height: var(--pos-button-min-height);
75
- padding: 0 var(--pos-space-xl);
76
- border: 0;
77
- border-radius: var(--pos-radius-md);
78
- background: var(--pos-color-primary);
79
- color: var(--pos-color-on-primary);
80
- font: inherit;
81
- font-weight: var(--pos-font-weight-bold);
82
- letter-spacing: 0.04em;
83
- text-transform: uppercase;
84
- cursor: pointer;
85
- -webkit-tap-highlight-color: transparent;
86
- }
87
-
88
- .pos-item-entry__add:not(:disabled):active {
89
- transform: translateY(1px);
90
- }
91
-
92
- .pos-item-entry__add--disabled,
93
- .pos-item-entry__add:disabled {
94
- background: var(--pos-color-disabled);
95
- color: var(--pos-color-on-disabled);
96
- cursor: not-allowed;
97
- transform: none;
98
- }
99
-
100
- .pos-item-entry__input:focus-visible {
101
- border-color: var(--pos-color-primary);
102
- outline: 2px solid var(--pos-color-primary);
103
- outline-offset: 2px;
104
- }
105
-
106
- .pos-item-entry__add:focus-visible {
107
- outline: 2px solid var(--pos-color-primary);
108
- outline-offset: 2px;
109
- }
110
-
111
- .pos-item-entry__status {
112
- min-height: 0;
113
- }
114
-
115
- .pos-item-entry__error {
116
- margin: 0;
117
- padding: var(--pos-space-sm) var(--pos-space-md);
118
- border-left: 3px solid var(--pos-color-danger);
119
- background: color-mix(in srgb, var(--pos-color-danger) 12%, transparent);
120
- color: var(--pos-color-danger);
121
- font-size: var(--pos-font-size-sm);
122
- font-weight: var(--pos-font-weight-semibold);
123
- }
124
-
125
- @media (min-width: 768px) {
126
- .pos-item-entry__panel {
127
- padding: var(--pos-space-xl);
128
- }
129
-
130
- .pos-item-entry__form {
131
- flex-direction: row;
132
- align-items: flex-end;
133
- gap: var(--pos-space-md);
134
- }
135
-
136
- .pos-item-entry__add {
137
- min-width: 7.5rem;
138
- }
139
- }
140
-
141
- @media (prefers-reduced-motion: reduce) {
142
- .pos-item-entry__add {
143
- transform: none;
144
- }
145
- }
@@ -1,123 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
-
3
- import { PosItemEntry, PosItemEntryIntent } from './pos-item-entry';
4
-
5
- describe('PosItemEntry', () => {
6
- let fixture: ComponentFixture<PosItemEntry>;
7
- let component: PosItemEntry;
8
-
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
11
- imports: [PosItemEntry],
12
- }).compileComponents();
13
-
14
- fixture = TestBed.createComponent(PosItemEntry);
15
- component = fixture.componentInstance;
16
- await fixture.whenStable();
17
- });
18
-
19
- function typeReference(value: string): void {
20
- const input = (fixture.nativeElement as HTMLElement).querySelector(
21
- '.pos-item-entry__input',
22
- ) as HTMLInputElement;
23
- input.value = value;
24
- input.dispatchEvent(new Event('input', { bubbles: true }));
25
- }
26
-
27
- it('should render a scan-or-type field by default', () => {
28
- const compiled = fixture.nativeElement as HTMLElement;
29
- expect(compiled.querySelector('.pos-item-entry__label')?.textContent).toContain('Item');
30
- expect(
31
- (compiled.querySelector('.pos-item-entry__input') as HTMLInputElement).placeholder,
32
- ).toBe('Scan or enter item');
33
- expect(
34
- (compiled.querySelector('.pos-item-entry__add') as HTMLButtonElement).disabled,
35
- ).toBe(true);
36
- });
37
-
38
- it('should emit item-entry with the typed reference and then clear the field', async () => {
39
- const intents: PosItemEntryIntent[] = [];
40
- component.action.subscribe((intent) => intents.push(intent));
41
-
42
- typeReference('SKU-001');
43
- await fixture.whenStable();
44
-
45
- const add = (fixture.nativeElement as HTMLElement).querySelector(
46
- '.pos-item-entry__add',
47
- ) as HTMLButtonElement;
48
- add.click();
49
- await fixture.whenStable();
50
-
51
- expect(intents).toEqual([{ type: 'itemEntry.submit', reference: 'SKU-001' }]);
52
- expect(
53
- (fixture.nativeElement as HTMLElement).querySelector(
54
- '.pos-item-entry__input',
55
- ) as HTMLInputElement,
56
- ).toHaveProperty('value', '');
57
- });
58
-
59
- it('should emit item-entry when a scanner (or cashier) submits the form with Enter', async () => {
60
- const intents: PosItemEntryIntent[] = [];
61
- component.action.subscribe((intent) => intents.push(intent));
62
-
63
- typeReference('5012345678900');
64
- await fixture.whenStable();
65
-
66
- const form = (fixture.nativeElement as HTMLElement).querySelector(
67
- '.pos-item-entry__form',
68
- ) as HTMLFormElement;
69
- form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
70
- await fixture.whenStable();
71
-
72
- expect(intents).toEqual([{ type: 'itemEntry.submit', reference: '5012345678900' }]);
73
- });
74
-
75
- it('should trim the reference and ignore empty or whitespace-only input', async () => {
76
- const intents: PosItemEntryIntent[] = [];
77
- component.action.subscribe((intent) => intents.push(intent));
78
-
79
- typeReference(' ');
80
- await fixture.whenStable();
81
-
82
- const form = (fixture.nativeElement as HTMLElement).querySelector(
83
- '.pos-item-entry__form',
84
- ) as HTMLFormElement;
85
- form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
86
- await fixture.whenStable();
87
-
88
- expect(intents).toEqual([]);
89
-
90
- typeReference(' SKU-014 ');
91
- await fixture.whenStable();
92
- form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
93
- await fixture.whenStable();
94
-
95
- expect(intents).toEqual([{ type: 'itemEntry.submit', reference: 'SKU-014' }]);
96
- });
97
-
98
- it('should not emit while disabled, and should show host error text as given', async () => {
99
- const intents: PosItemEntryIntent[] = [];
100
- component.action.subscribe((intent) => intents.push(intent));
101
-
102
- fixture.componentRef.setInput('disabled', true);
103
- fixture.componentRef.setInput('errorMessage', 'Item not found');
104
- await fixture.whenStable();
105
-
106
- typeReference('SKU-001');
107
- const add = (fixture.nativeElement as HTMLElement).querySelector(
108
- '.pos-item-entry__add',
109
- ) as HTMLButtonElement;
110
- add.click();
111
- await fixture.whenStable();
112
-
113
- const compiled = fixture.nativeElement as HTMLElement;
114
- expect(add.disabled).toBe(true);
115
- expect(
116
- (compiled.querySelector('.pos-item-entry__input') as HTMLInputElement).disabled,
117
- ).toBe(true);
118
- expect(compiled.querySelector('.pos-item-entry__error')?.textContent).toContain(
119
- 'Item not found',
120
- );
121
- expect(intents).toEqual([]);
122
- });
123
- });
@@ -1,31 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/angular-vite';
2
-
3
- import { PosItemEntry } from './pos-item-entry';
4
-
5
- const meta: Meta<PosItemEntry> = {
6
- title: 'Item entry',
7
- component: PosItemEntry,
8
- args: {
9
- disabled: false,
10
- errorMessage: '',
11
- autofocus: false,
12
- },
13
- };
14
-
15
- export default meta;
16
-
17
- type Story = StoryObj<PosItemEntry>;
18
-
19
- export const Default: Story = {};
20
-
21
- export const Disabled: Story = {
22
- args: {
23
- disabled: true,
24
- },
25
- };
26
-
27
- export const Error: Story = {
28
- args: {
29
- errorMessage: 'Item not found',
30
- },
31
- };
@@ -1,81 +0,0 @@
1
- import {
2
- afterNextRender,
3
- ChangeDetectionStrategy,
4
- Component,
5
- computed,
6
- ElementRef,
7
- input,
8
- output,
9
- signal,
10
- viewChild,
11
- } from '@angular/core';
12
-
13
- export type PosItemEntryIntent =
14
- | { readonly type: 'itemEntry.submit'; readonly reference: string };
15
-
16
- @Component({
17
- selector: 'pos-item-entry',
18
- changeDetection: ChangeDetectionStrategy.OnPush,
19
- templateUrl: './pos-item-entry.html',
20
- styleUrl: './pos-item-entry.scss',
21
- host: {
22
- class: 'pos-item-entry',
23
- },
24
- })
25
- export class PosItemEntry {
26
- readonly disabled = input(false);
27
- readonly errorMessage = input('');
28
- readonly autofocus = input(true);
29
-
30
- readonly label = input('Item');
31
- readonly placeholder = input('Scan or enter item');
32
- readonly addLabel = input('Add');
33
-
34
- readonly action = output<PosItemEntryIntent>();
35
-
36
- private readonly itemInput = viewChild<ElementRef<HTMLInputElement>>('itemInput');
37
-
38
- protected readonly reference = signal('');
39
-
40
- protected readonly canSubmit = computed(
41
- () => this.reference().trim().length > 0 && !this.disabled(),
42
- );
43
-
44
- constructor() {
45
- afterNextRender(() => {
46
- if (this.autofocus() && !this.disabled()) {
47
- this.focusInput();
48
- }
49
- });
50
- }
51
-
52
- protected onInput(event: Event): void {
53
- const target = event.target;
54
- if (!(target instanceof HTMLInputElement)) {
55
- return;
56
- }
57
-
58
- this.reference.set(target.value);
59
- }
60
-
61
- protected onSubmit(event: Event): void {
62
- event.preventDefault();
63
-
64
- if (this.disabled()) {
65
- return;
66
- }
67
-
68
- const reference = this.reference().trim();
69
- if (!reference) {
70
- return;
71
- }
72
-
73
- this.action.emit({ type: 'itemEntry.submit', reference });
74
- this.reference.set('');
75
- this.focusInput();
76
- }
77
-
78
- private focusInput(): void {
79
- this.itemInput()?.nativeElement.focus();
80
- }
81
- }
@@ -1,82 +0,0 @@
1
- <section class="pos-login__panel" [attr.aria-busy]="session().status === 'busy' ? true : null">
2
- <header class="pos-login__header">
3
- <p class="pos-login__kicker">Adaptive POS</p>
4
- <h1 class="pos-login__title">{{ title() }}</h1>
5
- </header>
6
-
7
- <div class="pos-login__status" aria-live="polite">
8
- @if (sessionStatusText()) {
9
- <p>{{ sessionStatusText() }}</p>
10
- }
11
- @if (errorMessage()) {
12
- <p class="pos-login__error">{{ errorMessage() }}</p>
13
- }
14
- </div>
15
-
16
- <div class="pos-login__body">
17
- <div class="pos-login__display">
18
- <input
19
- class="pos-login__input"
20
- [attr.type]="maskInput() ? 'password' : 'text'"
21
- [value]="inputValue()"
22
- maxlength="12"
23
- readonly
24
- tabindex="-1"
25
- placeholder="· · · ·"
26
- (keydown)="$event.preventDefault()"
27
- aria-label="Login code"
28
- />
29
- </div>
30
-
31
- <div class="pos-login__keypad" role="group" aria-label="Keypad">
32
- @for (row of keypadRows; track $index) {
33
- <div class="pos-login__keypad-row">
34
- @for (key of row; track key) {
35
- <button
36
- type="button"
37
- class="pos-login__key"
38
- [class.pos-login__key--action]="key === '←' || key === '.'"
39
- [disabled]="interactionLocked()"
40
- (click)="onKeypad(key)"
41
- >
42
- {{ key }}
43
- </button>
44
- }
45
- </div>
46
- }
47
- </div>
48
-
49
- <div class="pos-login__actions">
50
- @if (showBack()) {
51
- <button
52
- type="button"
53
- class="pos-login__back"
54
- aria-label="Back"
55
- [disabled]="interactionLocked()"
56
- (click)="onBack()"
57
- >
58
- <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
59
- <path
60
- d="M15 6 9 12l6 6"
61
- stroke="currentColor"
62
- stroke-width="2"
63
- stroke-linecap="round"
64
- stroke-linejoin="round"
65
- />
66
- </svg>
67
- <span>Back</span>
68
- </button>
69
- }
70
-
71
- <button
72
- type="button"
73
- class="pos-login__primary"
74
- [class.pos-login__primary--disabled]="!primaryEnabled()"
75
- [disabled]="!primaryEnabled()"
76
- (click)="onPrimary()"
77
- >
78
- {{ primaryLabel() }}
79
- </button>
80
- </div>
81
- </div>
82
- </section>