@r2digisolutions/components 0.17.11 → 0.18.0

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.
@@ -1,75 +1,51 @@
1
1
  <script lang="ts">
2
- /**
3
- * Lightweight visual barcode (Code 39–style pattern from the value string).
4
- * For real scanning/printing use a dedicated encoder; this is UI-friendly.
5
- */
2
+ import { encodeBarcode, type BarcodeSymbology } from '../../../utils/barcode.js';
3
+
6
4
  interface BarcodeProps {
7
5
  value: string;
8
6
  height?: number;
9
7
  showValue?: boolean;
8
+ /** Force a symbology. `auto` picks EAN/UPC when the value is a valid GTIN. */
9
+ format?: 'auto' | BarcodeSymbology;
10
10
  class?: string;
11
11
  }
12
12
 
13
13
  const {
14
14
  value,
15
- height = 56,
15
+ height = 72,
16
16
  showValue = true,
17
+ format = 'auto',
17
18
  class: className = ''
18
19
  }: BarcodeProps = $props();
19
20
 
20
- /** Map chars to bar/space widths (narrow=1, wide=2) — simplified Code 39 subset */
21
- const WIDTHS: Record<string, number[]> = {
22
- '0': [1, 1, 1, 2, 2, 1, 2, 1, 1],
23
- '1': [2, 1, 1, 2, 1, 1, 1, 1, 2],
24
- '2': [1, 1, 2, 2, 1, 1, 1, 1, 2],
25
- '3': [2, 1, 2, 2, 1, 1, 1, 1, 1],
26
- '4': [1, 1, 1, 2, 2, 1, 1, 1, 2],
27
- '5': [2, 1, 1, 2, 2, 1, 1, 1, 1],
28
- '6': [1, 1, 2, 2, 2, 1, 1, 1, 1],
29
- '7': [1, 1, 1, 2, 1, 1, 2, 1, 2],
30
- '8': [2, 1, 1, 2, 1, 1, 2, 1, 1],
31
- '9': [1, 1, 2, 2, 1, 1, 2, 1, 1],
32
- A: [2, 1, 1, 1, 1, 2, 1, 1, 2],
33
- B: [1, 1, 2, 1, 1, 2, 1, 1, 2],
34
- C: [2, 1, 2, 1, 1, 2, 1, 1, 1],
35
- D: [1, 1, 1, 1, 2, 2, 1, 1, 2],
36
- E: [2, 1, 1, 1, 2, 2, 1, 1, 1],
37
- F: [1, 1, 2, 1, 2, 2, 1, 1, 1],
38
- '*': [1, 2, 1, 1, 2, 1, 2, 1, 1]
39
- };
40
-
41
- const bars = $derived.by(() => {
42
- const raw = value.toUpperCase().replace(/[^0-9A-Z]/g, '') || '0';
43
- const chars = ['*', ...raw.split(''), '*'];
44
- const out: { w: number; black: boolean }[] = [];
45
- for (const ch of chars) {
46
- const pattern = WIDTHS[ch] ?? WIDTHS['0'];
47
- pattern.forEach((w, i) => out.push({ w, black: i % 2 === 0 }));
48
- out.push({ w: 1, black: false }); // gap
49
- }
50
- return out;
51
- });
52
-
53
- const totalUnits = $derived(bars.reduce((s, b) => s + b.w, 0));
21
+ const encoded = $derived(encodeBarcode(value, format));
22
+ const label = $derived(encoded?.text || value);
54
23
  </script>
55
24
 
56
- <figure class={['inline-flex flex-col items-center gap-1.5', className]}>
57
- <svg
58
- role="img"
59
- aria-label={`Barcode ${value}`}
60
- height={height}
61
- viewBox={`0 0 ${totalUnits} ${height}`}
62
- class="w-full max-w-xs text-primary"
63
- preserveAspectRatio="none"
64
- >
65
- {#each bars as bar, i}
66
- {@const x = bars.slice(0, i).reduce((s, b) => s + b.w, 0)}
67
- {#if bar.black}
68
- <rect x={x} y="0" width={bar.w} height={height} fill="currentColor" />
69
- {/if}
70
- {/each}
71
- </svg>
72
- {#if showValue}
73
- <figcaption class="font-mono text-[11px] tracking-widest text-secondary">{value}</figcaption>
25
+ <figure class={['max-w-sm gap-2 inline-flex w-full flex-col items-center', className]}>
26
+ {#if encoded}
27
+ <div class="rounded-lg bg-white px-2 py-3 w-full">
28
+ <svg
29
+ role="img"
30
+ aria-label={`Barcode ${label}`}
31
+ {height}
32
+ viewBox="0 0 {encoded.modules} {height}"
33
+ class="text-black block w-full"
34
+ preserveAspectRatio="none"
35
+ >
36
+ <title>{label}</title>
37
+ <rect width={encoded.modules} {height} fill="#fff" />
38
+ {#each encoded.runs as run (run.x)}
39
+ {#if run.black}
40
+ <rect x={run.x} y="0" width={run.w} {height} fill="#000" />
41
+ {/if}
42
+ {/each}
43
+ </svg>
44
+ </div>
45
+ {#if showValue}
46
+ <figcaption class="font-mono tracking-widest text-secondary text-[11px]">{label}</figcaption>
47
+ {/if}
48
+ {:else}
49
+ <figcaption class="font-mono tracking-widest text-secondary text-[11px]">{value}</figcaption>
74
50
  {/if}
75
51
  </figure>
@@ -1,11 +1,10 @@
1
- /**
2
- * Lightweight visual barcode (Code 39–style pattern from the value string).
3
- * For real scanning/printing use a dedicated encoder; this is UI-friendly.
4
- */
1
+ import { type BarcodeSymbology } from '../../../utils/barcode.js';
5
2
  interface BarcodeProps {
6
3
  value: string;
7
4
  height?: number;
8
5
  showValue?: boolean;
6
+ /** Force a symbology. `auto` picks EAN/UPC when the value is a valid GTIN. */
7
+ format?: 'auto' | BarcodeSymbology;
9
8
  class?: string;
10
9
  }
11
10
  declare const Barcode: import("svelte").Component<BarcodeProps, {}, "">;
@@ -2,4 +2,17 @@
2
2
  import Barcode from './Barcode.svelte';
3
3
  </script>
4
4
 
5
- <Barcode value="R2DIGI2026" />
5
+ <div class="gap-8 bg-surface p-6 flex flex-col">
6
+ <div class="space-y-2">
7
+ <p class="text-xs font-medium text-muted">Code 128 (SKU)</p>
8
+ <Barcode value="DEMO-SKU-001" />
9
+ </div>
10
+ <div class="space-y-2">
11
+ <p class="text-xs font-medium text-muted">EAN-13</p>
12
+ <Barcode value="4006381333931" />
13
+ </div>
14
+ <div class="space-y-2">
15
+ <p class="text-xs font-medium text-muted">Alphanumeric</p>
16
+ <Barcode value="R2DIGI2026" />
17
+ </div>
18
+ </div>
@@ -2,7 +2,7 @@ import type { StoryObj } from '@storybook/svelte';
2
2
  declare const meta: {
3
3
  title: string;
4
4
  component: import("svelte").Component<{
5
- example?: "error" | "ended" | "manual" | "both" | "grid" | "list" | "chat" | "feed";
5
+ example?: "error" | "ended" | "manual" | "both" | "grid" | "feed" | "list" | "chat";
6
6
  mode?: import("./InfiniteScroll.svelte.ts").InfiniteScrollMode;
7
7
  rootMargin?: string;
8
8
  pageSize?: number;
@@ -2,7 +2,7 @@ import type { StoryObj } from '@storybook/svelte';
2
2
  declare const meta: {
3
3
  title: string;
4
4
  component: import("svelte").Component<{
5
- example?: "error" | "success" | "card" | "stacked" | "inline" | "minimal" | "banner" | "consent";
5
+ example?: "error" | "success" | "banner" | "card" | "stacked" | "inline" | "minimal" | "consent";
6
6
  title?: string;
7
7
  description?: string;
8
8
  variant?: import("./NewsletterSignup.svelte.ts").NewsletterVariant;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Standards-compliant 1D barcode encoder (Code 128, EAN-13, EAN-8, UPC-A).
3
+ * Generates module runs suitable for SVG rendering. No external deps.
4
+ */
5
+ export type BarcodeSymbology = 'code128' | 'ean13' | 'ean8' | 'upc_a';
6
+ export type BarcodeRun = {
7
+ x: number;
8
+ w: number;
9
+ black: boolean;
10
+ };
11
+ export type EncodedBarcode = {
12
+ format: BarcodeSymbology;
13
+ text: string;
14
+ runs: BarcodeRun[];
15
+ modules: number;
16
+ };
17
+ /** GS1 check digit for a payload without the check (odd positions from the right × 3). */
18
+ export declare function gs1CheckDigit(payload: string): number;
19
+ export declare function isValidGs1(digits: string): boolean;
20
+ export declare function encodeBarcode(value: string, format?: 'auto' | BarcodeSymbology): EncodedBarcode | null;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Standards-compliant 1D barcode encoder (Code 128, EAN-13, EAN-8, UPC-A).
3
+ * Generates module runs suitable for SVG rendering. No external deps.
4
+ */
5
+ const QUIET = 10;
6
+ /** Code 128 patterns 0–106 (11 modules). Stop is 106 + terminal bar `11`. */
7
+ const CODE128 = [
8
+ '11011001100',
9
+ '11001101100',
10
+ '11001100110',
11
+ '10010011000',
12
+ '10010001100',
13
+ '10001001100',
14
+ '10011001000',
15
+ '10011000100',
16
+ '10001100100',
17
+ '11001001000',
18
+ '11001000100',
19
+ '11000100100',
20
+ '10110011100',
21
+ '10011011100',
22
+ '10011001110',
23
+ '10111001100',
24
+ '10011101100',
25
+ '10011100110',
26
+ '11001110010',
27
+ '11001011100',
28
+ '11001001110',
29
+ '11011100100',
30
+ '11001110100',
31
+ '11101101110',
32
+ '11101001100',
33
+ '11100101100',
34
+ '11100100110',
35
+ '11101100100',
36
+ '11100110100',
37
+ '11100110010',
38
+ '11011011000',
39
+ '11011000110',
40
+ '11000110110',
41
+ '10100011000',
42
+ '10001011000',
43
+ '10001000110',
44
+ '10110001000',
45
+ '10001101000',
46
+ '10001100010',
47
+ '11010001000',
48
+ '11000101000',
49
+ '11000100010',
50
+ '10110111000',
51
+ '10110001110',
52
+ '10001101110',
53
+ '10111011000',
54
+ '10111000110',
55
+ '10001110110',
56
+ '11101110110',
57
+ '11010001110',
58
+ '11000101110',
59
+ '11011101000',
60
+ '11011100010',
61
+ '11011101110',
62
+ '11101011000',
63
+ '11101000110',
64
+ '11100010110',
65
+ '11101101000',
66
+ '11101100010',
67
+ '11100011010',
68
+ '11101111010',
69
+ '11001000010',
70
+ '11110001010',
71
+ '10100110000',
72
+ '10100001100',
73
+ '10010110000',
74
+ '10010000110',
75
+ '10000101100',
76
+ '10000100110',
77
+ '10110010000',
78
+ '10110000100',
79
+ '10011010000',
80
+ '10011000010',
81
+ '10000110100',
82
+ '10000110010',
83
+ '11000010010',
84
+ '11001010000',
85
+ '11110111010',
86
+ '11000010100',
87
+ '10001111010',
88
+ '10100111100',
89
+ '10010111100',
90
+ '10010011110',
91
+ '10111100100',
92
+ '10011110100',
93
+ '10011110010',
94
+ '11110100100',
95
+ '11110010100',
96
+ '11110010010',
97
+ '11011011110',
98
+ '11011110110',
99
+ '11110110110',
100
+ '10101111000',
101
+ '10100011110',
102
+ '10001011110',
103
+ '10111101000',
104
+ '10111100010',
105
+ '11110101000',
106
+ '11110100010',
107
+ '10111011110',
108
+ '10111101110',
109
+ '11101011110',
110
+ '11110101110',
111
+ '11010000100',
112
+ '11010010000',
113
+ '11010011100',
114
+ '11000111010'
115
+ ];
116
+ const EAN_L = [
117
+ '0001101',
118
+ '0011001',
119
+ '0010011',
120
+ '0111101',
121
+ '0100011',
122
+ '0110001',
123
+ '0101111',
124
+ '0111011',
125
+ '0110111',
126
+ '0001011'
127
+ ];
128
+ const EAN_G = [
129
+ '0100111',
130
+ '0110011',
131
+ '0011011',
132
+ '0100001',
133
+ '0011101',
134
+ '0111001',
135
+ '0000101',
136
+ '0010001',
137
+ '0001001',
138
+ '0010111'
139
+ ];
140
+ const EAN_R = [
141
+ '1110010',
142
+ '1100110',
143
+ '1101100',
144
+ '1000010',
145
+ '1011100',
146
+ '1001110',
147
+ '1010000',
148
+ '1000100',
149
+ '1001000',
150
+ '1110100'
151
+ ];
152
+ /** First-digit parity for the left half of EAN-13 (L/G). */
153
+ const EAN13_PARITY = [
154
+ 'LLLLLL',
155
+ 'LLGLGG',
156
+ 'LLGGLG',
157
+ 'LLGGGL',
158
+ 'LGLLGG',
159
+ 'LGGLLG',
160
+ 'LGGGLL',
161
+ 'LGLGLG',
162
+ 'LGLGGL',
163
+ 'LGGLGL'
164
+ ];
165
+ function bitsToRuns(bits) {
166
+ const runs = [];
167
+ for (const ch of bits) {
168
+ const black = ch === '1';
169
+ const last = runs.at(-1);
170
+ if (last && last.black === black)
171
+ last.w += 1;
172
+ else
173
+ runs.push({ x: last ? last.x + last.w : 0, w: 1, black });
174
+ }
175
+ return runs;
176
+ }
177
+ function withQuiet(bits, left = QUIET, right = QUIET) {
178
+ return `${'0'.repeat(left)}${bits}${'0'.repeat(right)}`;
179
+ }
180
+ function encoded(format, text, bits) {
181
+ const padded = withQuiet(bits);
182
+ return { format, text, runs: bitsToRuns(padded), modules: padded.length };
183
+ }
184
+ /** GS1 check digit for a payload without the check (odd positions from the right × 3). */
185
+ export function gs1CheckDigit(payload) {
186
+ let sum = 0;
187
+ for (let i = 0; i < payload.length; i++) {
188
+ const n = Number(payload[payload.length - 1 - i]);
189
+ sum += n * (i % 2 === 0 ? 3 : 1);
190
+ }
191
+ return (10 - (sum % 10)) % 10;
192
+ }
193
+ export function isValidGs1(digits) {
194
+ if (!/^\d{8}$|^\d{12}$|^\d{13}$/.test(digits))
195
+ return false;
196
+ return gs1CheckDigit(digits.slice(0, -1)) === Number(digits.at(-1));
197
+ }
198
+ function encodeCode128(text) {
199
+ if (!text)
200
+ return null;
201
+ const codes = [104];
202
+ for (const ch of text) {
203
+ const code = ch.charCodeAt(0) - 32;
204
+ if (code < 0 || code > 94)
205
+ return null;
206
+ codes.push(code);
207
+ }
208
+ let sum = codes[0];
209
+ for (let i = 1; i < codes.length; i++)
210
+ sum += codes[i] * i;
211
+ codes.push(sum % 103);
212
+ let bits = '';
213
+ for (const code of codes)
214
+ bits += CODE128[code];
215
+ bits += `${CODE128[106]}11`;
216
+ return encoded('code128', text, bits);
217
+ }
218
+ function encodeEan13(digits) {
219
+ if (!/^\d{13}$/.test(digits) || !isValidGs1(digits))
220
+ return null;
221
+ const first = Number(digits[0]);
222
+ const parity = EAN13_PARITY[first];
223
+ let bits = '101';
224
+ for (let i = 0; i < 6; i++) {
225
+ const d = Number(digits[i + 1]);
226
+ bits += parity[i] === 'G' ? EAN_G[d] : EAN_L[d];
227
+ }
228
+ bits += '01010';
229
+ for (let i = 7; i < 13; i++)
230
+ bits += EAN_R[Number(digits[i])];
231
+ bits += '101';
232
+ return encoded('ean13', digits, bits);
233
+ }
234
+ function encodeEan8(digits) {
235
+ if (!/^\d{8}$/.test(digits) || !isValidGs1(digits))
236
+ return null;
237
+ let bits = '101';
238
+ for (let i = 0; i < 4; i++)
239
+ bits += EAN_L[Number(digits[i])];
240
+ bits += '01010';
241
+ for (let i = 4; i < 8; i++)
242
+ bits += EAN_R[Number(digits[i])];
243
+ bits += '101';
244
+ return encoded('ean8', digits, bits);
245
+ }
246
+ function encodeUpcA(digits) {
247
+ if (!/^\d{12}$/.test(digits) || !isValidGs1(digits))
248
+ return null;
249
+ const ean = encodeEan13(`0${digits}`);
250
+ if (!ean)
251
+ return null;
252
+ return { ...ean, format: 'upc_a', text: digits };
253
+ }
254
+ export function encodeBarcode(value, format = 'auto') {
255
+ const text = value.trim();
256
+ if (!text)
257
+ return null;
258
+ if (format === 'code128')
259
+ return encodeCode128(text);
260
+ if (format === 'ean13')
261
+ return encodeEan13(text);
262
+ if (format === 'ean8')
263
+ return encodeEan8(text);
264
+ if (format === 'upc_a')
265
+ return encodeUpcA(text);
266
+ if (/^\d{13}$/.test(text) && isValidGs1(text))
267
+ return encodeEan13(text);
268
+ if (/^\d{12}$/.test(text) && isValidGs1(text))
269
+ return encodeUpcA(text);
270
+ if (/^\d{8}$/.test(text) && isValidGs1(text))
271
+ return encodeEan8(text);
272
+ return encodeCode128(text);
273
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.17.11",
3
+ "version": "0.18.0",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",
@@ -66,8 +66,8 @@
66
66
  },
67
67
  "devDependencies": {
68
68
  "@eslint/js": "^10.0.1",
69
- "@lucide/svelte": "^1.39.0",
70
- "@playwright/test": "^1.62.1",
69
+ "@lucide/svelte": "^1.44.0",
70
+ "@playwright/test": "^1.63.0",
71
71
  "@storybook/addon-a11y": "^10.6.0",
72
72
  "@storybook/addon-docs": "^10.6.0",
73
73
  "@storybook/addon-svelte-csf": "^5.1.3",
@@ -80,14 +80,14 @@
80
80
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
81
81
  "@tailwindcss/vite": "^4.3.3",
82
82
  "@types/node": "^26.4.1",
83
- "@vitest/browser-playwright": "^4.1.11",
83
+ "@vitest/browser-playwright": "^5.0.0",
84
84
  "bumpp": "~12.3.0",
85
- "eslint": "^10.9.1",
85
+ "eslint": "^10.10.0",
86
86
  "eslint-config-prettier": "^10.1.8",
87
87
  "eslint-plugin-svelte": "^3.23.0",
88
88
  "globals": "^17.12.0",
89
89
  "mdsvex": "^0.12.8",
90
- "playwright": "^1.62.1",
90
+ "playwright": "^1.63.0",
91
91
  "prettier": "^3.9.6",
92
92
  "prettier-plugin-svelte": "^4.1.1",
93
93
  "prettier-plugin-tailwindcss": "^0.8.1",
@@ -97,10 +97,10 @@
97
97
  "svelte-check": "^4.7.6",
98
98
  "tailwindcss": "^4.3.3",
99
99
  "typescript": "6.0.3",
100
- "typescript-eslint": "^8.69.0",
100
+ "typescript-eslint": "^8.70.0",
101
101
  "vite": "^8.2.2",
102
- "vitest": "^4.1.11",
103
- "vitest-browser-svelte": "^3.0.0"
102
+ "vitest": "^5.0.0",
103
+ "vitest-browser-svelte": "^3.1.0"
104
104
  },
105
105
  "keywords": [
106
106
  "svelte",