@r2digisolutions/components 0.17.11 → 0.19.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,28 @@
1
+ import type { StoryObj } from '@storybook/svelte';
2
+ declare const meta: {
3
+ title: string;
4
+ component: import("svelte").Component<{
5
+ featured?: boolean;
6
+ installed?: boolean;
7
+ tags?: ("new" | "featured" | "popular")[];
8
+ }, {}, "">;
9
+ tags: string[];
10
+ argTypes: {
11
+ featured: {
12
+ control: "boolean";
13
+ };
14
+ installed: {
15
+ control: "boolean";
16
+ };
17
+ };
18
+ args: {
19
+ featured: false;
20
+ installed: false;
21
+ tags: "new"[];
22
+ };
23
+ };
24
+ export default meta;
25
+ type Story = StoryObj<typeof meta>;
26
+ export declare const Default: Story;
27
+ export declare const Featured: Story;
28
+ export declare const Installed: Story;
@@ -0,0 +1,23 @@
1
+ import StoreProductTileStory from './StoreProductTileStory.svelte';
2
+ const meta = {
3
+ title: 'Molecules/StoreProductTile',
4
+ component: StoreProductTileStory,
5
+ tags: ['autodocs'],
6
+ argTypes: {
7
+ featured: { control: 'boolean' },
8
+ installed: { control: 'boolean' }
9
+ },
10
+ args: {
11
+ featured: false,
12
+ installed: false,
13
+ tags: ['new']
14
+ }
15
+ };
16
+ export default meta;
17
+ export const Default = {};
18
+ export const Featured = {
19
+ args: { featured: true, tags: ['featured', 'new'] }
20
+ };
21
+ export const Installed = {
22
+ args: { installed: true, tags: ['popular'] }
23
+ };
@@ -0,0 +1,339 @@
1
+ <script lang="ts">
2
+ import type { Component, Snippet } from 'svelte';
3
+ import Badge from '../../atoms/Badge/Badge.svelte';
4
+
5
+ export type StoreProductTag = 'new' | 'featured' | 'popular';
6
+
7
+ const TAG_LABELS: Record<StoreProductTag, string> = {
8
+ new: 'Nuevo',
9
+ featured: 'Destacado',
10
+ popular: 'Popular'
11
+ };
12
+
13
+ type Props = {
14
+ name: string;
15
+ description?: string;
16
+ priceLabel: string;
17
+ categoryLabel?: string;
18
+ installed?: boolean;
19
+ /** landscape promotional card */
20
+ featured?: boolean;
21
+ tags?: StoreProductTag[];
22
+ icon: Component;
23
+ accentClass?: string;
24
+ onclick?: () => void;
25
+ /** Optional custom badge area */
26
+ badge?: Snippet;
27
+ };
28
+
29
+ let {
30
+ name,
31
+ description = '',
32
+ priceLabel,
33
+ categoryLabel = '',
34
+ installed = false,
35
+ featured = false,
36
+ tags = [],
37
+ icon: Icon,
38
+ accentClass = 'from-neutral-800 to-neutral-600',
39
+ onclick,
40
+ badge
41
+ }: Props = $props();
42
+
43
+ const visibleTags = $derived(
44
+ tags.filter((t): t is StoreProductTag => t === 'new' || t === 'featured' || t === 'popular')
45
+ );
46
+ </script>
47
+
48
+ <button
49
+ type="button"
50
+ class="group ms-tile"
51
+ class:ms-tile--featured={featured}
52
+ {onclick}
53
+ >
54
+ {#if featured}
55
+ <div class="ms-tile__promo bg-gradient-to-br {accentClass}">
56
+ <div class="ms-tile__promo-mesh" aria-hidden="true"></div>
57
+ {#if visibleTags.length || installed || badge}
58
+ <div class="ms-tile__tags">
59
+ {#if badge}
60
+ {@render badge()}
61
+ {:else}
62
+ {#each visibleTags as tag (tag)}
63
+ <Badge
64
+ variant={tag === 'new' ? 'info' : tag === 'featured' ? 'primary' : 'secondary'}
65
+ size="sm"
66
+ >
67
+ {TAG_LABELS[tag]}
68
+ </Badge>
69
+ {/each}
70
+ {#if installed}
71
+ <Badge variant="success" size="sm">Instalado</Badge>
72
+ {/if}
73
+ {/if}
74
+ </div>
75
+ {/if}
76
+ <div class="ms-tile__promo-copy">
77
+ {#if categoryLabel}
78
+ <span class="ms-tile__eyebrow">{categoryLabel}</span>
79
+ {/if}
80
+ <span class="ms-tile__promo-title">{name}</span>
81
+ {#if description}
82
+ <span class="ms-tile__promo-desc">{description}</span>
83
+ {/if}
84
+ <span class="ms-tile__promo-price">{priceLabel}</span>
85
+ </div>
86
+ <div class="ms-tile__promo-icon bg-gradient-to-br {accentClass}">
87
+ <Icon class="size-10 text-white drop-shadow" />
88
+ </div>
89
+ </div>
90
+ {:else}
91
+ <div class="ms-tile__icon-shell">
92
+ <div class="ms-tile__icon bg-gradient-to-br {accentClass}">
93
+ <div class="ms-tile__icon-shine" aria-hidden="true"></div>
94
+ <Icon class="relative size-9 text-white drop-shadow-sm sm:size-10" />
95
+ </div>
96
+ {#if visibleTags[0]}
97
+ <span class="ms-tile__corner">
98
+ <Badge
99
+ variant={visibleTags[0] === 'new'
100
+ ? 'info'
101
+ : visibleTags[0] === 'featured'
102
+ ? 'primary'
103
+ : 'secondary'}
104
+ size="sm"
105
+ >
106
+ {TAG_LABELS[visibleTags[0]]}
107
+ </Badge>
108
+ </span>
109
+ {:else if installed}
110
+ <span class="ms-tile__dot" title="Instalado" aria-label="Instalado"></span>
111
+ {/if}
112
+ </div>
113
+ <div class="ms-tile__meta">
114
+ <span class="ms-tile__name">{name}</span>
115
+ <span class="ms-tile__publisher">Evoteg</span>
116
+ <span class="ms-tile__price">{priceLabel}</span>
117
+ </div>
118
+ {/if}
119
+ </button>
120
+
121
+ <style>
122
+ .ms-tile {
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: 0.7rem;
126
+ width: 100%;
127
+ min-width: 0;
128
+ text-align: left;
129
+ border: 0;
130
+ background: transparent;
131
+ padding: 0;
132
+ cursor: pointer;
133
+ border-radius: 1rem;
134
+ transition:
135
+ transform 200ms cubic-bezier(0.22, 1, 0.36, 1),
136
+ filter 200ms ease;
137
+ }
138
+ .ms-tile:hover {
139
+ transform: translateY(-3px);
140
+ }
141
+ .ms-tile:focus-visible {
142
+ outline: 2px solid var(--color-brand-500, #2563eb);
143
+ outline-offset: 4px;
144
+ }
145
+ .ms-tile__icon-shell {
146
+ position: relative;
147
+ width: 100%;
148
+ aspect-ratio: 1;
149
+ display: grid;
150
+ place-items: center;
151
+ border-radius: 1.35rem;
152
+ background: linear-gradient(180deg, rgb(255 255 255 / 0.9), rgb(245 245 245 / 0.95));
153
+ box-shadow:
154
+ 0 1px 0 rgb(255 255 255 / 0.8) inset,
155
+ 0 1px 2px rgb(0 0 0 / 0.04),
156
+ 0 10px 28px rgb(0 0 0 / 0.07);
157
+ }
158
+ :global(.dark) .ms-tile__icon-shell {
159
+ background: linear-gradient(180deg, rgb(38 38 38), rgb(28 28 28));
160
+ box-shadow:
161
+ 0 1px 0 rgb(255 255 255 / 0.04) inset,
162
+ 0 10px 28px rgb(0 0 0 / 0.35);
163
+ }
164
+ .ms-tile__icon {
165
+ position: relative;
166
+ display: grid;
167
+ place-items: center;
168
+ width: 58%;
169
+ aspect-ratio: 1;
170
+ border-radius: 22%;
171
+ overflow: hidden;
172
+ box-shadow:
173
+ 0 8px 20px rgb(0 0 0 / 0.22),
174
+ 0 1px 0 rgb(255 255 255 / 0.2) inset;
175
+ transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
176
+ }
177
+ .ms-tile:hover .ms-tile__icon {
178
+ transform: scale(1.04);
179
+ }
180
+ .ms-tile__icon-shine {
181
+ position: absolute;
182
+ inset: 0;
183
+ background:
184
+ linear-gradient(135deg, rgb(255 255 255 / 0.35), transparent 42%),
185
+ radial-gradient(circle at 70% 80%, rgb(0 0 0 / 0.2), transparent 55%);
186
+ }
187
+ .ms-tile__corner {
188
+ position: absolute;
189
+ top: 0.45rem;
190
+ left: 0.45rem;
191
+ z-index: 2;
192
+ }
193
+ .ms-tile__dot {
194
+ position: absolute;
195
+ top: 0.55rem;
196
+ right: 0.55rem;
197
+ width: 0.55rem;
198
+ height: 0.55rem;
199
+ border-radius: 999px;
200
+ background: #16a34a;
201
+ box-shadow: 0 0 0 3px rgb(255 255 255 / 0.9);
202
+ }
203
+ :global(.dark) .ms-tile__dot {
204
+ box-shadow: 0 0 0 3px rgb(28 28 28);
205
+ }
206
+ .ms-tile__meta {
207
+ display: flex;
208
+ flex-direction: column;
209
+ gap: 0.1rem;
210
+ padding-inline: 0.2rem;
211
+ }
212
+ .ms-tile__name {
213
+ color: var(--color-neutral-900, #171717);
214
+ font-size: 0.875rem;
215
+ font-weight: 650;
216
+ letter-spacing: -0.01em;
217
+ overflow: hidden;
218
+ text-overflow: ellipsis;
219
+ white-space: nowrap;
220
+ }
221
+ :global(.dark) .ms-tile__name {
222
+ color: var(--color-neutral-50, #fafafa);
223
+ }
224
+ .ms-tile__publisher,
225
+ .ms-tile__price {
226
+ color: var(--color-neutral-500, #737373);
227
+ font-size: 0.75rem;
228
+ overflow: hidden;
229
+ text-overflow: ellipsis;
230
+ white-space: nowrap;
231
+ }
232
+ .ms-tile__price {
233
+ font-weight: 550;
234
+ color: var(--color-neutral-700, #404040);
235
+ }
236
+ :global(.dark) .ms-tile__price {
237
+ color: var(--color-neutral-300, #d4d4d4);
238
+ }
239
+
240
+ .ms-tile--featured {
241
+ height: 100%;
242
+ }
243
+ .ms-tile__promo {
244
+ position: relative;
245
+ display: flex;
246
+ align-items: flex-end;
247
+ justify-content: space-between;
248
+ gap: 1rem;
249
+ min-height: 11.5rem;
250
+ padding: 1.15rem 1.15rem 1.2rem;
251
+ border-radius: 1.35rem;
252
+ overflow: hidden;
253
+ color: white;
254
+ box-shadow:
255
+ 0 1px 0 rgb(255 255 255 / 0.12) inset,
256
+ 0 16px 40px rgb(0 0 0 / 0.18);
257
+ transition: box-shadow 220ms ease;
258
+ }
259
+ .ms-tile:hover .ms-tile__promo {
260
+ box-shadow:
261
+ 0 1px 0 rgb(255 255 255 / 0.16) inset,
262
+ 0 22px 48px rgb(0 0 0 / 0.24);
263
+ }
264
+ .ms-tile__promo-mesh {
265
+ position: absolute;
266
+ inset: 0;
267
+ background:
268
+ radial-gradient(circle at 12% 18%, rgb(255 255 255 / 0.28), transparent 40%),
269
+ radial-gradient(circle at 88% 78%, rgb(0 0 0 / 0.28), transparent 48%),
270
+ linear-gradient(120deg, transparent 40%, rgb(255 255 255 / 0.06) 100%);
271
+ }
272
+ .ms-tile__tags {
273
+ position: absolute;
274
+ top: 0.75rem;
275
+ left: 0.75rem;
276
+ z-index: 2;
277
+ display: flex;
278
+ flex-wrap: wrap;
279
+ gap: 0.35rem;
280
+ max-width: calc(100% - 5.5rem);
281
+ }
282
+ .ms-tile__promo-copy {
283
+ position: relative;
284
+ z-index: 1;
285
+ display: flex;
286
+ flex-direction: column;
287
+ gap: 0.35rem;
288
+ min-width: 0;
289
+ max-width: 70%;
290
+ }
291
+ .ms-tile__eyebrow {
292
+ font-size: 0.65rem;
293
+ font-weight: 700;
294
+ letter-spacing: 0.16em;
295
+ text-transform: uppercase;
296
+ color: rgb(255 255 255 / 0.72);
297
+ }
298
+ .ms-tile__promo-title {
299
+ font-size: 1.15rem;
300
+ font-weight: 700;
301
+ letter-spacing: -0.02em;
302
+ line-height: 1.15;
303
+ }
304
+ .ms-tile__promo-desc {
305
+ font-size: 0.78rem;
306
+ line-height: 1.35;
307
+ color: rgb(255 255 255 / 0.82);
308
+ display: -webkit-box;
309
+ -webkit-line-clamp: 2;
310
+ line-clamp: 2;
311
+ -webkit-box-orient: vertical;
312
+ overflow: hidden;
313
+ }
314
+ .ms-tile__promo-price {
315
+ margin-top: 0.25rem;
316
+ font-size: 0.78rem;
317
+ font-weight: 600;
318
+ color: rgb(255 255 255 / 0.92);
319
+ }
320
+ .ms-tile__promo-icon {
321
+ position: relative;
322
+ z-index: 1;
323
+ display: grid;
324
+ place-items: center;
325
+ width: 4.25rem;
326
+ height: 4.25rem;
327
+ border-radius: 22%;
328
+ flex-shrink: 0;
329
+ box-shadow:
330
+ 0 12px 28px rgb(0 0 0 / 0.28),
331
+ 0 1px 0 rgb(255 255 255 / 0.25) inset;
332
+ border: 1px solid rgb(255 255 255 / 0.18);
333
+ transform: translateY(-0.15rem);
334
+ transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
335
+ }
336
+ .ms-tile:hover .ms-tile__promo-icon {
337
+ transform: translateY(-0.35rem) scale(1.03);
338
+ }
339
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { Component, Snippet } from 'svelte';
2
+ export type StoreProductTag = 'new' | 'featured' | 'popular';
3
+ type Props = {
4
+ name: string;
5
+ description?: string;
6
+ priceLabel: string;
7
+ categoryLabel?: string;
8
+ installed?: boolean;
9
+ /** landscape promotional card */
10
+ featured?: boolean;
11
+ tags?: StoreProductTag[];
12
+ icon: Component;
13
+ accentClass?: string;
14
+ onclick?: () => void;
15
+ /** Optional custom badge area */
16
+ badge?: Snippet;
17
+ };
18
+ declare const StoreProductTile: Component<Props, {}, "">;
19
+ type StoreProductTile = ReturnType<typeof StoreProductTile>;
20
+ export default StoreProductTile;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import StoreProductTile from './StoreProductTile.svelte';
3
+ import Package from '@lucide/svelte/icons/package';
4
+ import Utensils from '@lucide/svelte/icons/utensils';
5
+
6
+ let {
7
+ featured = false,
8
+ installed = false,
9
+ tags = ['new'] as ('new' | 'featured' | 'popular')[]
10
+ } = $props();
11
+ </script>
12
+
13
+ <div class="max-w-sm p-6">
14
+ <StoreProductTile
15
+ name={featured ? 'TPV / POS' : 'Inventario'}
16
+ description="Punto de venta con mesas, KDS y caja."
17
+ priceLabel={featured ? '29 €/mes' : 'Gratis'}
18
+ categoryLabel="Comercial"
19
+ {featured}
20
+ {installed}
21
+ {tags}
22
+ icon={featured ? Utensils : Package}
23
+ accentClass={featured ? 'from-sky-700 via-sky-600 to-cyan-500' : 'from-emerald-800 to-teal-600'}
24
+ />
25
+ </div>
@@ -0,0 +1,7 @@
1
+ declare const StoreProductTileStory: import("svelte").Component<{
2
+ featured?: boolean;
3
+ installed?: boolean;
4
+ tags?: ("new" | "featured" | "popular")[];
5
+ }, {}, "">;
6
+ type StoreProductTileStory = ReturnType<typeof StoreProductTileStory>;
7
+ export default StoreProductTileStory;
@@ -0,0 +1,8 @@
1
+ import StoreRailStory from './StoreRailStory.svelte';
2
+ const meta = {
3
+ title: 'Molecules/StoreRail',
4
+ component: StoreRailStory,
5
+ tags: ['autodocs']
6
+ };
7
+ export default meta;
8
+ export const Default = {};
@@ -0,0 +1,152 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Button from '../../atoms/Button/Button.svelte';
4
+ import ChevronLeft from '@lucide/svelte/icons/chevron-left';
5
+ import ChevronRight from '@lucide/svelte/icons/chevron-right';
6
+
7
+ type Props = {
8
+ title: string;
9
+ subtitle?: string;
10
+ children: Snippet;
11
+ wide?: boolean;
12
+ /** Soft edge fades (narrow by default). */
13
+ fade?: boolean;
14
+ class?: string;
15
+ };
16
+
17
+ let {
18
+ title,
19
+ subtitle = '',
20
+ children,
21
+ wide = false,
22
+ fade = true,
23
+ class: className = ''
24
+ }: Props = $props();
25
+
26
+ let track = $state<HTMLDivElement | null>(null);
27
+
28
+ function scrollByDir(dir: -1 | 1) {
29
+ if (!track) return;
30
+ const amount = Math.max(240, track.clientWidth * 0.72) * dir;
31
+ track.scrollBy({ left: amount, behavior: 'smooth' });
32
+ }
33
+ </script>
34
+
35
+ <section class="ms-rail {className}">
36
+ <div class="ms-rail__head">
37
+ <div class="min-w-0">
38
+ <h2 class="ms-rail__title">{title}</h2>
39
+ {#if subtitle}
40
+ <p class="ms-rail__subtitle">{subtitle}</p>
41
+ {/if}
42
+ </div>
43
+ <div class="ms-rail__controls">
44
+ <Button
45
+ variant="secondary"
46
+ size="sm"
47
+ onclick={() => scrollByDir(-1)}
48
+ aria-label="Desplazar a la izquierda"
49
+ >
50
+ <ChevronLeft class="size-4" />
51
+ </Button>
52
+ <Button
53
+ variant="secondary"
54
+ size="sm"
55
+ onclick={() => scrollByDir(1)}
56
+ aria-label="Desplazar a la derecha"
57
+ >
58
+ <ChevronRight class="size-4" />
59
+ </Button>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="ms-rail__viewport">
64
+ <div class="ms-rail__track" class:ms-rail__track--wide={wide} bind:this={track}>
65
+ {@render children()}
66
+ </div>
67
+ {#if fade}
68
+ <div class="ms-rail__fade ms-rail__fade--left" aria-hidden="true"></div>
69
+ <div class="ms-rail__fade ms-rail__fade--right" aria-hidden="true"></div>
70
+ {/if}
71
+ </div>
72
+ </section>
73
+
74
+ <style>
75
+ .ms-rail {
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 0.9rem;
79
+ }
80
+ .ms-rail__head {
81
+ display: flex;
82
+ align-items: flex-end;
83
+ justify-content: space-between;
84
+ gap: 1rem;
85
+ padding-inline: 0.1rem;
86
+ }
87
+ .ms-rail__title {
88
+ margin: 0;
89
+ font-size: 1.35rem;
90
+ font-weight: 700;
91
+ letter-spacing: -0.03em;
92
+ color: var(--color-neutral-900, #171717);
93
+ }
94
+ :global(.dark) .ms-rail__title {
95
+ color: var(--color-neutral-50, #fafafa);
96
+ }
97
+ .ms-rail__subtitle {
98
+ margin: 0.15rem 0 0;
99
+ font-size: 0.8125rem;
100
+ color: var(--color-neutral-500, #737373);
101
+ }
102
+ .ms-rail__controls {
103
+ display: flex;
104
+ gap: 0.4rem;
105
+ flex-shrink: 0;
106
+ }
107
+ .ms-rail__viewport {
108
+ position: relative;
109
+ }
110
+ .ms-rail__track {
111
+ display: grid;
112
+ grid-auto-flow: column;
113
+ grid-auto-columns: minmax(8.75rem, 9.75rem);
114
+ gap: 1rem;
115
+ overflow-x: auto;
116
+ padding: 0.35rem 0.15rem 0.85rem;
117
+ scroll-snap-type: x mandatory;
118
+ scrollbar-width: none;
119
+ }
120
+ .ms-rail__track::-webkit-scrollbar {
121
+ display: none;
122
+ }
123
+ .ms-rail__track--wide {
124
+ grid-auto-columns: minmax(17rem, 19rem);
125
+ }
126
+ .ms-rail__track > :global(*) {
127
+ scroll-snap-align: start;
128
+ }
129
+ .ms-rail__fade {
130
+ pointer-events: none;
131
+ position: absolute;
132
+ top: 0;
133
+ bottom: 0.85rem;
134
+ width: 0.85rem;
135
+ z-index: 1;
136
+ opacity: 0.55;
137
+ }
138
+ .ms-rail__fade--left {
139
+ left: 0;
140
+ background: linear-gradient(90deg, var(--store-bg, #fafafa), transparent);
141
+ }
142
+ .ms-rail__fade--right {
143
+ right: 0;
144
+ background: linear-gradient(270deg, var(--store-bg, #fafafa), transparent);
145
+ }
146
+ :global(.dark) .ms-rail__fade--left {
147
+ background: linear-gradient(90deg, var(--store-bg-dark, #0a0a0a), transparent);
148
+ }
149
+ :global(.dark) .ms-rail__fade--right {
150
+ background: linear-gradient(270deg, var(--store-bg-dark, #0a0a0a), transparent);
151
+ }
152
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ title: string;
4
+ subtitle?: string;
5
+ children: Snippet;
6
+ wide?: boolean;
7
+ /** Soft edge fades (narrow by default). */
8
+ fade?: boolean;
9
+ class?: string;
10
+ };
11
+ declare const StoreRail: import("svelte").Component<Props, {}, "">;
12
+ type StoreRail = ReturnType<typeof StoreRail>;
13
+ export default StoreRail;
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import StoreRail from './StoreRail.svelte';
3
+ import StoreProductTile from '../StoreProductTile/StoreProductTile.svelte';
4
+ import Package from '@lucide/svelte/icons/package';
5
+ import Utensils from '@lucide/svelte/icons/utensils';
6
+ import Wallet from '@lucide/svelte/icons/wallet';
7
+ import Users from '@lucide/svelte/icons/users';
8
+ import Briefcase from '@lucide/svelte/icons/briefcase';
9
+
10
+ const items = [
11
+ { name: 'Inventario', price: '15 €/mes', icon: Package, accent: 'from-emerald-800 to-teal-600', tags: ['new'] as const },
12
+ { name: 'TPV', price: '29 €/mes', icon: Utensils, accent: 'from-sky-700 to-cyan-500', tags: ['featured'] as const },
13
+ { name: 'VeriFactu', price: '9 €/mes', icon: Wallet, accent: 'from-amber-800 to-orange-500', tags: [] as const },
14
+ { name: 'Portal', price: 'Gratis', icon: Users, accent: 'from-stone-700 to-neutral-500', tags: ['popular'] as const },
15
+ { name: 'Proyectos', price: '19 €/mes', icon: Briefcase, accent: 'from-teal-800 to-emerald-500', tags: ['new'] as const }
16
+ ];
17
+ </script>
18
+
19
+ <div class="bg-neutral-50 p-6 dark:bg-neutral-950" style="--store-bg: #fafafa; --store-bg-dark: #0a0a0a">
20
+ <StoreRail title="Destacados" subtitle="Ejemplo de rail" wide={false}>
21
+ {#each items as item (item.name)}
22
+ <StoreProductTile
23
+ name={item.name}
24
+ priceLabel={item.price}
25
+ icon={item.icon}
26
+ accentClass={item.accent}
27
+ tags={[...item.tags]}
28
+ />
29
+ {/each}
30
+ </StoreRail>
31
+ </div>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const StoreRailStory: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type StoreRailStory = InstanceType<typeof StoreRailStory>;
18
+ export default StoreRailStory;
package/dist/index.d.ts CHANGED
@@ -599,6 +599,9 @@ export { default as DateGroupedList } from './components/molecules/DateGroupedLi
599
599
  export type { DateGroupedListItem, DateGroupedListGroup, DateGroupedAccent } from './components/molecules/DateGroupedList/DateGroupedList.svelte';
600
600
  export { default as BentoGrid } from './components/molecules/BentoGrid/BentoGrid.svelte';
601
601
  export type { BentoItem, BentoGridCols, BentoGridGap } from './components/molecules/BentoGrid/BentoGrid.svelte';
602
+ export { default as StoreProductTile } from './components/molecules/StoreProductTile/StoreProductTile.svelte';
603
+ export type { StoreProductTag } from './components/molecules/StoreProductTile/StoreProductTile.svelte';
604
+ export { default as StoreRail } from './components/molecules/StoreRail/StoreRail.svelte';
602
605
  export { default as WidgetFrame } from './components/molecules/WidgetFrame/WidgetFrame.svelte';
603
606
  export type { WidgetResizeEdge, WidgetRect, WidgetHandleStyle } from './components/molecules/WidgetFrame/WidgetFrame.svelte';
604
607
  export { default as DashboardGridToolbar } from './components/molecules/DashboardGridToolbar/DashboardGridToolbar.svelte';
package/dist/index.js CHANGED
@@ -412,6 +412,8 @@ export { default as ShippingEstimate } from './components/molecules/ShippingEsti
412
412
  export { default as SmartSuggestion } from './components/molecules/SmartSuggestion/SmartSuggestion.svelte';
413
413
  export { default as DateGroupedList } from './components/molecules/DateGroupedList/DateGroupedList.svelte';
414
414
  export { default as BentoGrid } from './components/molecules/BentoGrid/BentoGrid.svelte';
415
+ export { default as StoreProductTile } from './components/molecules/StoreProductTile/StoreProductTile.svelte';
416
+ export { default as StoreRail } from './components/molecules/StoreRail/StoreRail.svelte';
415
417
  export { default as WidgetFrame } from './components/molecules/WidgetFrame/WidgetFrame.svelte';
416
418
  export { default as DashboardGridToolbar } from './components/molecules/DashboardGridToolbar/DashboardGridToolbar.svelte';
417
419
  export { default as WidgetCanvas } from './components/molecules/WidgetCanvas/WidgetCanvas.svelte';
@@ -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.19.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",