@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.
- package/fesm2022/retalia-pos-components.mjs +1089 -0
- package/fesm2022/retalia-pos-components.mjs.map +1 -0
- package/package.json +39 -17
- package/types/retalia-pos-components.d.ts +300 -0
- package/eslint.config.js +0 -32
- package/ng-package.json +0 -14
- package/src/lib/basket/pos-basket.html +0 -124
- package/src/lib/basket/pos-basket.scss +0 -285
- package/src/lib/basket/pos-basket.spec.ts +0 -198
- package/src/lib/basket/pos-basket.stories.ts +0 -114
- package/src/lib/basket/pos-basket.ts +0 -142
- package/src/lib/item-entry/pos-item-entry.html +0 -41
- package/src/lib/item-entry/pos-item-entry.scss +0 -145
- package/src/lib/item-entry/pos-item-entry.spec.ts +0 -123
- package/src/lib/item-entry/pos-item-entry.stories.ts +0 -31
- package/src/lib/item-entry/pos-item-entry.ts +0 -81
- package/src/lib/login/pos-login.html +0 -82
- package/src/lib/login/pos-login.scss +0 -238
- package/src/lib/login/pos-login.spec.ts +0 -89
- package/src/lib/login/pos-login.stories.ts +0 -36
- package/src/lib/login/pos-login.ts +0 -150
- package/src/lib/receipt/pos-receipt.html +0 -116
- package/src/lib/receipt/pos-receipt.scss +0 -367
- package/src/lib/receipt/pos-receipt.spec.ts +0 -188
- package/src/lib/receipt/pos-receipt.stories.ts +0 -163
- package/src/lib/receipt/pos-receipt.ts +0 -129
- package/src/lib/tender/pos-tender.html +0 -79
- package/src/lib/tender/pos-tender.scss +0 -318
- package/src/lib/tender/pos-tender.spec.ts +0 -150
- package/src/lib/tender/pos-tender.stories.ts +0 -47
- package/src/lib/tender/pos-tender.ts +0 -154
- package/src/lib/tokens/pos-theme-css.spec.ts +0 -75
- package/src/lib/tokens/pos-theme-css.ts +0 -123
- package/src/lib/tokens/pos-token-catalog.ts +0 -514
- package/src/lib/tokens/pos-tokens.html +0 -351
- package/src/lib/tokens/pos-tokens.scss +0 -437
- package/src/lib/tokens/pos-tokens.spec.ts +0 -164
- package/src/lib/tokens/pos-tokens.stories.ts +0 -22
- package/src/lib/tokens/pos-tokens.ts +0 -208
- package/src/public-api.ts +0 -37
- package/tsconfig.lib.json +0 -14
- package/tsconfig.lib.prod.json +0 -11
- package/tsconfig.spec.json +0 -11
- /package/{src/styles → styles}/styles.css +0 -0
- /package/{src/styles → styles}/tokens.css +0 -0
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/angular-vite';
|
|
2
|
-
|
|
3
|
-
import { PosTender } from './pos-tender';
|
|
4
|
-
|
|
5
|
-
const meta: Meta<PosTender> = {
|
|
6
|
-
title: 'Tender',
|
|
7
|
-
component: PosTender,
|
|
8
|
-
args: {
|
|
9
|
-
amountDue: 12,
|
|
10
|
-
changeDue: null,
|
|
11
|
-
currency: 'EUR',
|
|
12
|
-
disabled: false,
|
|
13
|
-
errorMessage: '',
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export default meta;
|
|
18
|
-
|
|
19
|
-
type Story = StoryObj<PosTender>;
|
|
20
|
-
|
|
21
|
-
export const EnterCash: Story = {};
|
|
22
|
-
|
|
23
|
-
export const Busy: Story = {
|
|
24
|
-
args: {
|
|
25
|
-
disabled: true,
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const ChangeDueFromEnvelope: Story = {
|
|
30
|
-
args: {
|
|
31
|
-
amountDue: 12,
|
|
32
|
-
changeDue: 99,
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const ExactTenderZeroChange: Story = {
|
|
37
|
-
args: {
|
|
38
|
-
amountDue: 12,
|
|
39
|
-
changeDue: 0,
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export const Error: Story = {
|
|
44
|
-
args: {
|
|
45
|
-
errorMessage: 'Tender was declined',
|
|
46
|
-
},
|
|
47
|
-
};
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
input,
|
|
6
|
-
output,
|
|
7
|
-
signal,
|
|
8
|
-
} from '@angular/core';
|
|
9
|
-
|
|
10
|
-
export type PosTenderIntent =
|
|
11
|
-
| {
|
|
12
|
-
readonly type: 'tender.cash';
|
|
13
|
-
readonly amount: number;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const KEYPAD_ROWS: readonly (readonly string[])[] = [
|
|
17
|
-
['1', '2', '3'],
|
|
18
|
-
['4', '5', '6'],
|
|
19
|
-
['7', '8', '9'],
|
|
20
|
-
['0', '.', '←'],
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
const MAX_WHOLE_DIGITS = 8;
|
|
24
|
-
|
|
25
|
-
function parseCashAmount(raw: string): number | null {
|
|
26
|
-
if (!raw) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const amount = Number(raw);
|
|
31
|
-
if (!Number.isFinite(amount) || amount <= 0) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return Math.round(amount * 100) / 100;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function appendCashKey(current: string, key: string): string {
|
|
39
|
-
if (key === '←') {
|
|
40
|
-
return current.slice(0, -1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (key === '.') {
|
|
44
|
-
if (current.includes('.')) {
|
|
45
|
-
return current;
|
|
46
|
-
}
|
|
47
|
-
return current === '' ? '0.' : `${current}.`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!/^\d$/.test(key)) {
|
|
51
|
-
return current;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (current === '0') {
|
|
55
|
-
return key;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const [whole = '', fraction] = current.split('.');
|
|
59
|
-
if (fraction !== undefined) {
|
|
60
|
-
if (fraction.length >= 2) {
|
|
61
|
-
return current;
|
|
62
|
-
}
|
|
63
|
-
return current + key;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const significant = whole.replace(/^0+/, '');
|
|
67
|
-
if (significant.length >= MAX_WHOLE_DIGITS) {
|
|
68
|
-
return current;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return current + key;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
@Component({
|
|
75
|
-
selector: 'pos-tender',
|
|
76
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
77
|
-
templateUrl: './pos-tender.html',
|
|
78
|
-
styleUrl: './pos-tender.scss',
|
|
79
|
-
host: {
|
|
80
|
-
class: 'pos-tender',
|
|
81
|
-
},
|
|
82
|
-
})
|
|
83
|
-
export class PosTender {
|
|
84
|
-
/** Display-only amount due from the host envelope — not calculated here. */
|
|
85
|
-
readonly amountDue = input(0);
|
|
86
|
-
/** Display-only change due from the host envelope — not calculated here. */
|
|
87
|
-
readonly changeDue = input<number | null>(null);
|
|
88
|
-
readonly currency = input('EUR');
|
|
89
|
-
readonly disabled = input(false);
|
|
90
|
-
readonly errorMessage = input('');
|
|
91
|
-
|
|
92
|
-
readonly title = input('Cash tender');
|
|
93
|
-
readonly amountDueLabel = input('Amount due');
|
|
94
|
-
readonly tenderedLabel = input('Amount tendered');
|
|
95
|
-
readonly changeDueLabel = input('Change due');
|
|
96
|
-
readonly tenderLabel = input('Tender cash');
|
|
97
|
-
readonly processingLabel = input('Processing tender…');
|
|
98
|
-
|
|
99
|
-
readonly action = output<PosTenderIntent>();
|
|
100
|
-
|
|
101
|
-
protected readonly keypadRows = KEYPAD_ROWS;
|
|
102
|
-
protected readonly amountInput = signal('');
|
|
103
|
-
|
|
104
|
-
protected readonly parsedAmount = computed(() =>
|
|
105
|
-
parseCashAmount(this.amountInput()),
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
protected readonly canTender = computed(
|
|
109
|
-
() => this.parsedAmount() !== null && !this.disabled(),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
protected readonly hasChangeDue = computed(() => this.changeDue() !== null);
|
|
113
|
-
|
|
114
|
-
protected readonly displayTendered = computed(
|
|
115
|
-
() => this.amountInput() || '0',
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
protected readonly statusText = computed(() => {
|
|
119
|
-
if (this.disabled() && !this.hasChangeDue()) {
|
|
120
|
-
return this.processingLabel();
|
|
121
|
-
}
|
|
122
|
-
return '';
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
protected formatMoney(amount: number): string {
|
|
126
|
-
return new Intl.NumberFormat(undefined, {
|
|
127
|
-
style: 'currency',
|
|
128
|
-
currency: this.currency() || 'EUR',
|
|
129
|
-
minimumFractionDigits: 2,
|
|
130
|
-
maximumFractionDigits: 2,
|
|
131
|
-
}).format(amount);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
protected onKeypad(key: string): void {
|
|
135
|
-
if (this.disabled()) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
this.amountInput.update((value) => appendCashKey(value, key));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
protected onTender(): void {
|
|
143
|
-
if (!this.canTender()) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const amount = this.parsedAmount();
|
|
148
|
-
if (amount === null) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
this.action.emit({ type: 'tender.cash', amount });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
POS_TOKEN_CATALOG,
|
|
3
|
-
POS_TOKEN_DEFAULTS,
|
|
4
|
-
} from './pos-token-catalog';
|
|
5
|
-
import {
|
|
6
|
-
normalizePosTokenValue,
|
|
7
|
-
overridesFromThemeValues,
|
|
8
|
-
parseCssHex,
|
|
9
|
-
parsePosThemeCss,
|
|
10
|
-
resolvedPosTheme,
|
|
11
|
-
serializePosThemeCss,
|
|
12
|
-
} from './pos-theme-css';
|
|
13
|
-
|
|
14
|
-
describe('pos-theme-css', () => {
|
|
15
|
-
it('parses known --pos-* declarations from a theme file', () => {
|
|
16
|
-
const parsed = parsePosThemeCss(`
|
|
17
|
-
:root {
|
|
18
|
-
--pos-color-primary: #111111;
|
|
19
|
-
--pos-font-size-md: 1rem;
|
|
20
|
-
--unknown-token: red;
|
|
21
|
-
}
|
|
22
|
-
`);
|
|
23
|
-
|
|
24
|
-
expect(parsed).toEqual({
|
|
25
|
-
'--pos-color-primary': '#111111',
|
|
26
|
-
'--pos-font-size-md': '1rem',
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('serializes a complete :root theme without mutating defaults', () => {
|
|
31
|
-
const css = serializePosThemeCss({
|
|
32
|
-
...POS_TOKEN_DEFAULTS,
|
|
33
|
-
'--pos-color-primary': '#111111',
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(css).toContain(':root {');
|
|
37
|
-
expect(css).toContain('--pos-color-primary: #111111;');
|
|
38
|
-
expect(css).toContain('--pos-color-success: #16dbcc;');
|
|
39
|
-
expect(POS_TOKEN_DEFAULTS['--pos-color-primary']).toBe('#c9b5f0');
|
|
40
|
-
expect(POS_TOKEN_CATALOG.find((token) => token.variable === '--pos-color-primary')?.defaultValue).toBe(
|
|
41
|
-
'#c9b5f0',
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('round-trips every catalog token through serialize and parse', () => {
|
|
46
|
-
const css = serializePosThemeCss(POS_TOKEN_DEFAULTS);
|
|
47
|
-
const parsed = parsePosThemeCss(css);
|
|
48
|
-
|
|
49
|
-
for (const token of POS_TOKEN_CATALOG) {
|
|
50
|
-
expect(parsed[token.variable]).toBe(token.defaultValue);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('keeps only values that differ from package defaults as overrides', () => {
|
|
55
|
-
expect(
|
|
56
|
-
overridesFromThemeValues({
|
|
57
|
-
'--pos-color-primary': '#111111',
|
|
58
|
-
'--pos-color-success': '#16dbcc',
|
|
59
|
-
}),
|
|
60
|
-
).toEqual({
|
|
61
|
-
'--pos-color-primary': '#111111',
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('resolves overrides on top of package defaults', () => {
|
|
66
|
-
const resolved = resolvedPosTheme({ '--pos-radius-md': '20px' });
|
|
67
|
-
expect(resolved['--pos-radius-md']).toBe('20px');
|
|
68
|
-
expect(resolved['--pos-radius-sm']).toBe('4px');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('normalizes short hex colors', () => {
|
|
72
|
-
expect(parseCssHex('#abc')).toBe('#aabbcc');
|
|
73
|
-
expect(normalizePosTokenValue('color', '#ABC')).toBe('#aabbcc');
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
POS_TOKEN_BY_VARIABLE,
|
|
3
|
-
POS_TOKEN_CATALOG,
|
|
4
|
-
POS_TOKEN_DEFAULTS,
|
|
5
|
-
type PosTokenKind,
|
|
6
|
-
} from './pos-token-catalog';
|
|
7
|
-
|
|
8
|
-
const TOKEN_DECL = /(--pos-[a-z0-9-]+)\s*:\s*([^;}{]+)/gi;
|
|
9
|
-
|
|
10
|
-
export const POS_THEME_CSS_FILENAME = 'pos-theme.css';
|
|
11
|
-
|
|
12
|
-
const THEME_CSS_HEADER = `/**
|
|
13
|
-
* Adaptive POS theme
|
|
14
|
-
* Generated by <pos-tokens>. Load after @retalia/pos-components styles.
|
|
15
|
-
* Package defaults are unchanged; this file only overrides --pos-* on :root.
|
|
16
|
-
*/`;
|
|
17
|
-
|
|
18
|
-
export function normalizePosTokenValue(
|
|
19
|
-
kind: PosTokenKind,
|
|
20
|
-
value: string,
|
|
21
|
-
): string {
|
|
22
|
-
const trimmed = value.trim().replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
|
23
|
-
if (!trimmed) {
|
|
24
|
-
return '';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (kind === 'color') {
|
|
28
|
-
const hex = parseCssHex(trimmed);
|
|
29
|
-
if (hex) {
|
|
30
|
-
return hex;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return trimmed.replace(/\s*!important$/i, '').trim();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function parseCssHex(value: string): string | null {
|
|
38
|
-
const trimmed = value.trim();
|
|
39
|
-
const short = /^#([0-9a-f]{3})$/i.exec(trimmed)?.[1];
|
|
40
|
-
if (short && short.length === 3) {
|
|
41
|
-
const [r, g, b] = short;
|
|
42
|
-
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (/^#[0-9a-f]{6}$/i.test(trimmed)) {
|
|
46
|
-
return trimmed.toLowerCase();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function parsePosThemeCss(css: string): Readonly<Record<string, string>> {
|
|
53
|
-
const result: Record<string, string> = {};
|
|
54
|
-
|
|
55
|
-
for (const match of css.matchAll(TOKEN_DECL)) {
|
|
56
|
-
const variable = match[1];
|
|
57
|
-
const token = POS_TOKEN_BY_VARIABLE.get(variable);
|
|
58
|
-
if (!token) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const value = normalizePosTokenValue(token.kind, match[2] ?? '');
|
|
63
|
-
if (value) {
|
|
64
|
-
result[variable] = value;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function resolvedPosTheme(
|
|
72
|
-
overrides: Readonly<Record<string, string>>,
|
|
73
|
-
): Readonly<Record<string, string>> {
|
|
74
|
-
const resolved: Record<string, string> = { ...POS_TOKEN_DEFAULTS };
|
|
75
|
-
for (const [variable, value] of Object.entries(overrides)) {
|
|
76
|
-
if (variable in POS_TOKEN_DEFAULTS && value.trim()) {
|
|
77
|
-
resolved[variable] = value;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return resolved;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function overridesFromThemeValues(
|
|
84
|
-
values: Readonly<Record<string, string>>,
|
|
85
|
-
): Record<string, string> {
|
|
86
|
-
const overrides: Record<string, string> = {};
|
|
87
|
-
for (const [variable, value] of Object.entries(values)) {
|
|
88
|
-
const token = POS_TOKEN_BY_VARIABLE.get(variable);
|
|
89
|
-
if (!token) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const normalized = normalizePosTokenValue(token.kind, value);
|
|
94
|
-
if (normalized && normalized !== token.defaultValue) {
|
|
95
|
-
overrides[variable] = normalized;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return overrides;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function serializePosThemeCss(
|
|
102
|
-
values: Readonly<Record<string, string>>,
|
|
103
|
-
): string {
|
|
104
|
-
const lines = POS_TOKEN_CATALOG.map((token) => {
|
|
105
|
-
const value = values[token.variable] ?? token.defaultValue;
|
|
106
|
-
return ` ${token.variable}: ${value};`;
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return `${THEME_CSS_HEADER}\n:root {\n${lines.join('\n')}\n}\n`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function downloadTextFile(filename: string, contents: string): void {
|
|
113
|
-
const blob = new Blob([contents], { type: 'text/css;charset=utf-8' });
|
|
114
|
-
const url = URL.createObjectURL(blob);
|
|
115
|
-
const anchor = document.createElement('a');
|
|
116
|
-
anchor.href = url;
|
|
117
|
-
anchor.download = filename;
|
|
118
|
-
anchor.rel = 'noopener';
|
|
119
|
-
document.body.append(anchor);
|
|
120
|
-
anchor.click();
|
|
121
|
-
anchor.remove();
|
|
122
|
-
URL.revokeObjectURL(url);
|
|
123
|
-
}
|