@retalia/pos-components 0.0.1 → 0.0.3
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/README.md +303 -5
- package/fesm2022/retalia-pos-components.mjs +1074 -82
- package/fesm2022/retalia-pos-components.mjs.map +1 -1
- package/package.json +5 -2
- package/styles/styles.css +1 -0
- package/styles/tokens.css +57 -0
- package/types/retalia-pos-components.d.ts +294 -10
package/README.md
CHANGED
|
@@ -1,5 +1,303 @@
|
|
|
1
|
-
# @retalia/pos-components
|
|
2
|
-
|
|
3
|
-
Presentational Adaptive POS components.
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# @retalia/pos-components
|
|
2
|
+
|
|
3
|
+
Presentational Adaptive POS Angular components. State in, typed intents out — no service calls.
|
|
4
|
+
|
|
5
|
+
Requires **Angular 21** (`@angular/core` and `@angular/common` `^21.2.0`).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @retalia/pos-components
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Styles
|
|
14
|
+
|
|
15
|
+
Add the package stylesheet once in the host app (e.g. `angular.json` `styles`):
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
node_modules/@retalia/pos-components/styles/styles.css
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Theme via CSS custom properties (`--pos-*`) defined in that file. Override tokens in the host to rebrand; values not exposed as tokens are not overridable without a library change.
|
|
22
|
+
|
|
23
|
+
Use `<pos-tokens>` (Storybook: **Theme / Tokens**, or an internal theme lab page) to see every token in action. Click a sample or token to edit values as **live overrides** — this does not change the package defaults in `styles.css`. Export a complete `:root` CSS file for the consuming POS, and import that file back into the lab later.
|
|
24
|
+
|
|
25
|
+
## Tokens gallery
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { Component } from '@angular/core';
|
|
29
|
+
import { PosTokens } from '@retalia/pos-components';
|
|
30
|
+
|
|
31
|
+
@Component({
|
|
32
|
+
selector: 'app-theme-lab',
|
|
33
|
+
imports: [PosTokens],
|
|
34
|
+
template: `<pos-tokens />`,
|
|
35
|
+
})
|
|
36
|
+
export class ThemeLab {}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Host the exported file **after** the package stylesheet so `--pos-*` overrides win:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
node_modules/@retalia/pos-components/styles/styles.css
|
|
43
|
+
src/styles/pos-theme.css
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| | |
|
|
47
|
+
|---|---|
|
|
48
|
+
| Selector | `pos-tokens` |
|
|
49
|
+
| Purpose | Documentation / theme lab — not for production POS screens |
|
|
50
|
+
| Export | Complete `:root { --pos-*: … }` CSS for the host POS |
|
|
51
|
+
| Import | Previously exported theme CSS, applied as lab overrides only |
|
|
52
|
+
|
|
53
|
+
## Login
|
|
54
|
+
|
|
55
|
+
Keypad login UI — presentational only. It does not call auth services. Layout around it (e.g. split with an image) belongs in the host app.
|
|
56
|
+
|
|
57
|
+
The host envelope holds login/session state and passes it in. If the host uses a package `AuthService`, it calls that on `login.submit`, then updates `[session]` / `[errorMessage]`. The component never injects the service.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { Component, signal } from '@angular/core';
|
|
61
|
+
import {
|
|
62
|
+
PosLogin,
|
|
63
|
+
PosLoginIntent,
|
|
64
|
+
PosLoginSessionState,
|
|
65
|
+
} from '@retalia/pos-components';
|
|
66
|
+
|
|
67
|
+
@Component({
|
|
68
|
+
selector: 'app-root',
|
|
69
|
+
imports: [PosLogin],
|
|
70
|
+
template: `
|
|
71
|
+
<pos-login
|
|
72
|
+
[session]="session()"
|
|
73
|
+
[errorMessage]="errorMessage()"
|
|
74
|
+
(action)="onLoginAction($event)"
|
|
75
|
+
/>
|
|
76
|
+
`,
|
|
77
|
+
})
|
|
78
|
+
export class App {
|
|
79
|
+
readonly session = signal<PosLoginSessionState>({ status: 'signedOut' });
|
|
80
|
+
readonly errorMessage = signal('');
|
|
81
|
+
|
|
82
|
+
onLoginAction(intent: PosLoginIntent): void {
|
|
83
|
+
if (intent.type !== 'login.submit') {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Host (or optional package AuthService) calls the API, then:
|
|
87
|
+
// this.session.set({ status: 'busy' });
|
|
88
|
+
// this.session.set({ status: 'signedIn', displayName: clerk.name });
|
|
89
|
+
// this.errorMessage.set('Invalid clerk or password');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| | |
|
|
95
|
+
|---|---|
|
|
96
|
+
| Selector | `pos-login` |
|
|
97
|
+
| Input `session` | Display-only login/session state: `signedOut` \| `busy` \| `signedIn`, optional `displayName` |
|
|
98
|
+
| Input `errorMessage` | Optional error text |
|
|
99
|
+
| Output `action` | Typed intents: `login.key`, `login.next`, `login.submit`, `login.back`, … |
|
|
100
|
+
|
|
101
|
+
## Basket
|
|
102
|
+
|
|
103
|
+
Presentational sale basket — renders `basket.lines[]` and `totals` exactly as the host envelope provides them. It does not price, tax, or discount. Quantity, void, and clear only emit intents; the host (or server) updates the envelope.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { Component, signal } from '@angular/core';
|
|
107
|
+
import {
|
|
108
|
+
PosBasket,
|
|
109
|
+
PosBasketIntent,
|
|
110
|
+
PosBasketLine,
|
|
111
|
+
PosBasketTotals,
|
|
112
|
+
} from '@retalia/pos-components';
|
|
113
|
+
|
|
114
|
+
@Component({
|
|
115
|
+
selector: 'app-sale',
|
|
116
|
+
imports: [PosBasket],
|
|
117
|
+
template: `
|
|
118
|
+
<pos-basket
|
|
119
|
+
[lines]="lines()"
|
|
120
|
+
[totals]="totals()"
|
|
121
|
+
(action)="onBasketAction($event)"
|
|
122
|
+
/>
|
|
123
|
+
`,
|
|
124
|
+
})
|
|
125
|
+
export class Sale {
|
|
126
|
+
readonly lines = signal<readonly PosBasketLine[]>([]);
|
|
127
|
+
readonly totals = signal<PosBasketTotals>({
|
|
128
|
+
gross: 0,
|
|
129
|
+
net: 0,
|
|
130
|
+
vat: 0,
|
|
131
|
+
discount: 0,
|
|
132
|
+
due: 0,
|
|
133
|
+
currency: 'EUR',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
onBasketAction(intent: PosBasketIntent): void {
|
|
137
|
+
// Host envelope binding maps:
|
|
138
|
+
// basket.changeQuantity → change-quantity
|
|
139
|
+
// basket.voidLine → void-line
|
|
140
|
+
// basket.clear → clear-basket
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
| | |
|
|
146
|
+
|---|---|
|
|
147
|
+
| Selector | `pos-basket` |
|
|
148
|
+
| Input `lines` | Envelope `basket.lines[]` as given (`lineId`, `sku`, `name`, `qty`, `unitPrice`, `lineTotal`, `vat`, `struckThrough`) |
|
|
149
|
+
| Input `totals` | Envelope `totals` as given (`gross`, `net`, `vat`, `discount`, `due`, `currency`) |
|
|
150
|
+
| Input `disabled` | Optional lock while the host is busy |
|
|
151
|
+
| Output `action` | Typed intents: `basket.changeQuantity`, `basket.voidLine`, `basket.clear` |
|
|
152
|
+
|
|
153
|
+
## Item entry
|
|
154
|
+
|
|
155
|
+
Presentational scan-or-type field for the sale screen. It captures an item reference (barcode scan or typed SKU) and emits an item-entry intent. It does not look up, validate, or price the item — the host envelope maps the intent to `item-entry`, and the server handles catalog lookup.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { Component, signal } from '@angular/core';
|
|
159
|
+
import { PosItemEntry, PosItemEntryIntent } from '@retalia/pos-components';
|
|
160
|
+
|
|
161
|
+
@Component({
|
|
162
|
+
selector: 'app-sale',
|
|
163
|
+
imports: [PosItemEntry],
|
|
164
|
+
template: `
|
|
165
|
+
<pos-item-entry
|
|
166
|
+
[disabled]="busy()"
|
|
167
|
+
[errorMessage]="errorMessage()"
|
|
168
|
+
(action)="onItemEntryAction($event)"
|
|
169
|
+
/>
|
|
170
|
+
`,
|
|
171
|
+
})
|
|
172
|
+
export class Sale {
|
|
173
|
+
readonly busy = signal(false);
|
|
174
|
+
readonly errorMessage = signal('');
|
|
175
|
+
|
|
176
|
+
onItemEntryAction(intent: PosItemEntryIntent): void {
|
|
177
|
+
// Host envelope binding maps:
|
|
178
|
+
// itemEntry.submit → item-entry (payload: reference)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| | |
|
|
184
|
+
|---|---|
|
|
185
|
+
| Selector | `pos-item-entry` |
|
|
186
|
+
| Input `disabled` | Optional lock while the host is busy (e.g. AddItem in flight) |
|
|
187
|
+
| Input `errorMessage` | Optional host-provided error text (lookup failures belong to the host) |
|
|
188
|
+
| Input `autofocus` | Focus the field on render so a scanner can type immediately (default `true`) |
|
|
189
|
+
| Output `action` | Typed intent: `itemEntry.submit` with `reference` |
|
|
190
|
+
|
|
191
|
+
## Tender
|
|
192
|
+
|
|
193
|
+
Presentational cash tender — the cashier enters the amount tendered; the host envelope sends that intent and passes back `changeDue`. The component never calculates change or takes payment. Card / electronic tender is out of scope.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { Component, signal } from '@angular/core';
|
|
197
|
+
import { PosTender, PosTenderIntent } from '@retalia/pos-components';
|
|
198
|
+
|
|
199
|
+
@Component({
|
|
200
|
+
selector: 'app-tender',
|
|
201
|
+
imports: [PosTender],
|
|
202
|
+
template: `
|
|
203
|
+
<pos-tender
|
|
204
|
+
[amountDue]="amountDue()"
|
|
205
|
+
[changeDue]="changeDue()"
|
|
206
|
+
[currency]="currency()"
|
|
207
|
+
[disabled]="busy()"
|
|
208
|
+
[errorMessage]="errorMessage()"
|
|
209
|
+
(action)="onTenderAction($event)"
|
|
210
|
+
/>
|
|
211
|
+
`,
|
|
212
|
+
})
|
|
213
|
+
export class Tender {
|
|
214
|
+
readonly amountDue = signal(12);
|
|
215
|
+
readonly changeDue = signal<number | null>(null);
|
|
216
|
+
readonly currency = signal('EUR');
|
|
217
|
+
readonly busy = signal(false);
|
|
218
|
+
readonly errorMessage = signal('');
|
|
219
|
+
|
|
220
|
+
onTenderAction(intent: PosTenderIntent): void {
|
|
221
|
+
if (intent.type !== 'tender.cash') {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// Host envelope binding maps tender.cash → tender intent,
|
|
225
|
+
// then sets changeDue from the envelope response.
|
|
226
|
+
// this.busy.set(true);
|
|
227
|
+
// this.changeDue.set(envelope.changeDue);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| | |
|
|
233
|
+
|---|---|
|
|
234
|
+
| Selector | `pos-tender` |
|
|
235
|
+
| Input `amountDue` | Envelope amount due as given — display only |
|
|
236
|
+
| Input `changeDue` | Envelope change due as given (`null` until the host responds). Shown as received, never calculated |
|
|
237
|
+
| Input `currency` | ISO currency code used only for display formatting |
|
|
238
|
+
| Input `disabled` | Optional lock while the host is busy |
|
|
239
|
+
| Input `errorMessage` | Optional error text |
|
|
240
|
+
| Output `action` | Typed intent: `tender.cash` with the cashier-entered `amount` |
|
|
241
|
+
|
|
242
|
+
## Receipt
|
|
243
|
+
|
|
244
|
+
Presentational completed-sale receipt — renders `basket.lines[]`, `totals`, `payments[]`, and `document` exactly as the host envelope provides them. It does not format, tax, or calculate change. Printing is out of scope.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { Component, signal } from '@angular/core';
|
|
248
|
+
import {
|
|
249
|
+
PosReceipt,
|
|
250
|
+
PosReceiptIntent,
|
|
251
|
+
PosReceiptLine,
|
|
252
|
+
PosReceiptPayment,
|
|
253
|
+
PosReceiptTotals,
|
|
254
|
+
PosReceiptDocument,
|
|
255
|
+
} from '@retalia/pos-components';
|
|
256
|
+
|
|
257
|
+
@Component({
|
|
258
|
+
selector: 'app-receipt',
|
|
259
|
+
imports: [PosReceipt],
|
|
260
|
+
template: `
|
|
261
|
+
<pos-receipt
|
|
262
|
+
[lines]="lines()"
|
|
263
|
+
[totals]="totals()"
|
|
264
|
+
[payments]="payments()"
|
|
265
|
+
[document]="document()"
|
|
266
|
+
(action)="onReceiptAction($event)"
|
|
267
|
+
/>
|
|
268
|
+
`,
|
|
269
|
+
})
|
|
270
|
+
export class Receipt {
|
|
271
|
+
readonly lines = signal<readonly PosReceiptLine[]>([]);
|
|
272
|
+
readonly totals = signal<PosReceiptTotals>({
|
|
273
|
+
gross: 0,
|
|
274
|
+
net: 0,
|
|
275
|
+
vat: 0,
|
|
276
|
+
discount: 0,
|
|
277
|
+
due: 0,
|
|
278
|
+
currency: 'EUR',
|
|
279
|
+
});
|
|
280
|
+
readonly payments = signal<readonly PosReceiptPayment[]>([]);
|
|
281
|
+
readonly document = signal<PosReceiptDocument>({
|
|
282
|
+
number: '',
|
|
283
|
+
receiptReady: false,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
onReceiptAction(intent: PosReceiptIntent): void {
|
|
287
|
+
if (intent.type !== 'receipt.newSale') {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Host envelope binding maps receipt.newSale → newSale.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
| | |
|
|
296
|
+
|---|---|
|
|
297
|
+
| Selector | `pos-receipt` |
|
|
298
|
+
| Input `lines` | Envelope `basket.lines[]` as given (`lineId`, `sku`, `name`, `qty`, `unitPrice`, `lineTotal`, `vat`, `struckThrough`) |
|
|
299
|
+
| Input `totals` | Envelope `totals` as given (`gross`, `net`, `vat`, `discount`, `due`, `currency`) |
|
|
300
|
+
| Input `payments` | Envelope `payments[]` as given (`tender`, `amount`, `tendered`, `change`, `state`) |
|
|
301
|
+
| Input `document` | Envelope `document` as given (`number`, `receiptReady`) |
|
|
302
|
+
| Input `disabled` | Optional lock while the host is busy |
|
|
303
|
+
| Output `action` | Typed intent: `receipt.newSale` |
|