@lyeve-labs/ui-kit 0.41.0 → 0.42.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.
package/README.md CHANGED
@@ -70,7 +70,7 @@ ThemeToggle, Toaster (plus the `toast` service)
70
70
  - **Svelte 5.0** or newer
71
71
  - **Tailwind CSS v4** with the `@tailwindcss/vite` plugin
72
72
  - **`@lucide/svelte`** 0.511 or newer, below 2.0
73
- - **Node 20** or newer
73
+ - **Node 24** or newer
74
74
 
75
75
  ## Install
76
76
 
@@ -385,6 +385,9 @@ const JOB_TONE: Record<JobState, StatusTone> = {
385
385
 
386
386
  ## Local development
387
387
 
388
+ The kit is developed on Node 26 and installs with pnpm only. Applications that
389
+ consume it need Node 24 or newer and may use any package manager.
390
+
388
391
  ```bash
389
392
  pnpm install # install dependencies
390
393
  pnpm test # run unit tests
@@ -69,6 +69,21 @@
69
69
  actions?: Snippet<[{ skus: string[]; total: number | null }]>;
70
70
  /** Anything the host wants between the total and the actions. */
71
71
  notice?: Snippet;
72
+ /**
73
+ * The host's own summary, drawn in place of everything inside the aside:
74
+ * the title line, `error`, the list, the ladder, the total, `notice` and
75
+ * `actions`. For a host that prices more than the selection, such as a
76
+ * plan change or a discount code. The aside keeps its id, its stickiness
77
+ * and the bar that jumps to it on a narrow screen. `remove` takes one
78
+ * selected capability out and reports it through `onchange`, as unticking
79
+ * its card does. A capability that is not selected is left alone. `total`
80
+ * is null both while a price is in flight and before one is asked for, so
81
+ * a host that tells the two apart tracks its own request, as it does for
82
+ * `quoting`.
83
+ */
84
+ summary?: Snippet<[{ skus: string[]; total: number | null; remove: (sku: string) => void }]>;
85
+ /** The panel's name: the aside's accessible name, also printed atop the default summary. */
86
+ heading?: string;
72
87
  /**
73
88
  * How far below the top of the window the summary sticks on a wide
74
89
  * screen, as a CSS length. A page with a sticky header passes its height,
@@ -111,6 +126,8 @@
111
126
  onchange,
112
127
  actions,
113
128
  notice,
129
+ summary,
130
+ heading = 'Your selection',
114
131
  stickyTop = '1rem',
115
132
  class: klass = '',
116
133
  }: Props = $props();
@@ -349,129 +366,141 @@
349
366
  id={summaryId}
350
367
  class="scroll-mt-20 self-start rounded-xl border border-line bg-surface p-5 lg:sticky"
351
368
  style:top={stickyTop}
352
- aria-label="Your selection"
369
+ aria-label={heading}
353
370
  >
354
- <div class="mb-4 flex items-center justify-between gap-2">
355
- <p class="text-xs font-semibold uppercase tracking-widest text-faint">Your selection</p>
356
- {#if count > 0}
357
- <Badge tone="brand" size="sm">
358
- {count}
359
- {count === 1 ? 'capability' : 'capabilities'}
360
- </Badge>
361
- {/if}
362
- </div>
371
+ {#if summary}
372
+ {@render summary({
373
+ skus: [...selected],
374
+ total,
375
+ remove: (sku) => {
376
+ if (selected.has(sku)) toggle(sku);
377
+ },
378
+ })}
379
+ {:else}
380
+ <div class="mb-4 flex items-center justify-between gap-2">
381
+ <p class="text-xs font-semibold uppercase tracking-widest text-faint">{heading}</p>
382
+ {#if count > 0}
383
+ <Badge tone="brand" size="sm">
384
+ {count}
385
+ {count === 1 ? 'capability' : 'capabilities'}
386
+ </Badge>
387
+ {/if}
388
+ </div>
363
389
 
364
- {#if error}
365
- <p class="mb-3 rounded-lg border border-danger/30 bg-danger/10 p-3 text-xs text-danger">
366
- {error}
367
- </p>
368
- {/if}
390
+ {#if error}
391
+ <p class="mb-3 rounded-lg border border-danger/30 bg-danger/10 p-3 text-xs text-danger">
392
+ {error}
393
+ </p>
394
+ {/if}
369
395
 
370
- {#if count === 0}
371
- <p class="py-6 text-center text-sm text-muted">
372
- Nothing picked yet. With none of these you are on the free tier, which needs no checkout.
373
- </p>
374
- {:else}
375
- <ul class="mb-4 max-h-56 space-y-1 overflow-y-auto pe-1">
376
- {#each [...selected] as sku (sku)}
377
- {@const p = bySku.get(sku)}
378
- <li class="flex items-center justify-between gap-2 text-xs">
379
- <span class="truncate text-fg">{p?.name ?? sku}</span>
380
- <span class="flex items-center gap-1">
381
- <span class="font-mono text-muted">{formatCents(p?.price_cents ?? 0, currency)}</span>
382
- <Button
383
- type="button"
384
- variant="ghost"
385
- size="sm"
386
- onclick={() => toggle(sku)}
387
- aria-label={`Take ${p?.name ?? sku} out of your selection`}
388
- >
389
- <X size={12} />
390
- </Button>
391
- </span>
392
- </li>
393
- {/each}
394
- </ul>
396
+ {#if count === 0}
397
+ <p class="py-6 text-center text-sm text-muted">
398
+ Nothing picked yet. With none of these you are on the free tier, which needs no checkout.
399
+ </p>
400
+ {:else}
401
+ <ul class="mb-4 max-h-56 space-y-1 overflow-y-auto pe-1">
402
+ {#each [...selected] as sku (sku)}
403
+ {@const p = bySku.get(sku)}
404
+ <li class="flex items-center justify-between gap-2 text-xs">
405
+ <span class="truncate text-fg">{p?.name ?? sku}</span>
406
+ <span class="flex items-center gap-1">
407
+ <span class="font-mono text-muted"
408
+ >{formatCents(p?.price_cents ?? 0, currency)}</span
409
+ >
410
+ <Button
411
+ type="button"
412
+ variant="ghost"
413
+ size="sm"
414
+ onclick={() => toggle(sku)}
415
+ aria-label={`Take ${p?.name ?? sku} out of your selection`}
416
+ >
417
+ <X size={12} />
418
+ </Button>
419
+ </span>
420
+ </li>
421
+ {/each}
422
+ </ul>
395
423
 
396
- {#if rungs.length > 0}
397
- <!-- The ladder as a meter, so "two more" is something a reader can
424
+ {#if rungs.length > 0}
425
+ <!-- The ladder as a meter, so "two more" is something a reader can
398
426
  see the distance to. -->
399
- <div class="mb-3" aria-hidden="true">
400
- <div class="relative h-1.5 rounded-full bg-surface-2">
401
- <span
402
- class="absolute inset-y-0 start-0 rounded-full bg-brand transition-[width] duration-progress"
403
- style:width="{ladderFill}%"
404
- ></span>
405
- {#each rungs as r (r.plugins)}
427
+ <div class="mb-3" aria-hidden="true">
428
+ <div class="relative h-1.5 rounded-full bg-surface-2">
406
429
  <span
407
- class={cn(
408
- 'absolute top-1/2 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 transition-colors',
409
- ladder.count >= r.plugins
410
- ? 'border-brand bg-brand'
411
- : 'border-line-strong bg-surface',
412
- )}
413
- style:left="{(r.plugins / ladderTop) * 100}%"
430
+ class="absolute inset-y-0 start-0 rounded-full bg-brand transition-[width] duration-progress"
431
+ style:width="{ladderFill}%"
414
432
  ></span>
415
- {/each}
416
- </div>
417
- <div class="relative mt-2 h-4 text-[10px] text-muted">
418
- {#each rungs as r (r.plugins)}
419
- <span
420
- class="absolute -translate-x-1/2 whitespace-nowrap font-mono"
421
- style:left="{Math.min((r.plugins / ladderTop) * 100, 92)}%"
422
- >
423
- {r.plugins}+ · {r.discount_pct}%
424
- </span>
425
- {/each}
433
+ {#each rungs as r (r.plugins)}
434
+ <span
435
+ class={cn(
436
+ 'absolute top-1/2 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 transition-colors',
437
+ ladder.count >= r.plugins
438
+ ? 'border-brand bg-brand'
439
+ : 'border-line-strong bg-surface',
440
+ )}
441
+ style:left="{(r.plugins / ladderTop) * 100}%"
442
+ ></span>
443
+ {/each}
444
+ </div>
445
+ <div class="relative mt-2 h-4 text-[10px] text-muted">
446
+ {#each rungs as r (r.plugins)}
447
+ <span
448
+ class="absolute -translate-x-1/2 whitespace-nowrap font-mono"
449
+ style:left="{Math.min((r.plugins / ladderTop) * 100, 92)}%"
450
+ >
451
+ {r.plugins}+ · {r.discount_pct}%
452
+ </span>
453
+ {/each}
454
+ </div>
426
455
  </div>
427
- </div>
428
- {/if}
429
-
430
- {#if ladder.reachedPct > 0}
431
- <p class="mb-2 text-xs text-success">
432
- Volume discount {ladder.reachedPct}% applied.
433
- </p>
434
- {/if}
435
- {#if ladder.next}
436
- <p class="mb-3 text-xs text-muted">
437
- {ladder.next.more} more and the discount is {ladder.next.discount_pct}%.
438
- </p>
439
- {/if}
456
+ {/if}
440
457
 
441
- <div class="mb-4 border-t border-line pt-3">
442
- {#if total === null}
443
- <p class="flex items-center gap-2 text-sm text-muted">
444
- {#if quoting}
445
- <Spinner size={14} />
446
- Pricing your selection
447
- {:else}
448
- Waiting for a price.
449
- {/if}
458
+ {#if ladder.reachedPct > 0}
459
+ <p class="mb-2 text-xs text-success">
460
+ Volume discount {ladder.reachedPct}% applied.
450
461
  </p>
451
- {:else}
452
- <p class="flex items-baseline justify-between gap-2">
453
- <span class="text-sm text-muted">Total</span>
454
- <span class="font-mono text-xl font-semibold text-fg">
455
- {formatCents(total, currency)}<span class="text-sm font-normal text-muted"
456
- >{perLabel}</span
457
- >
458
- </span>
462
+ {/if}
463
+ {#if ladder.next}
464
+ <p class="mb-3 text-xs text-muted">
465
+ {ladder.next.more} more and the discount is {ladder.next.discount_pct}%.
459
466
  </p>
460
- {#if quoted && quoted.volume_discount_cents > 0}
461
- <p class="mt-1 text-end text-xs text-muted">
462
- {formatCents(quoted.volume_discount_cents, currency)} off the list price.
467
+ {/if}
468
+
469
+ <div class="mb-4 border-t border-line pt-3">
470
+ {#if total === null}
471
+ <p class="flex items-center gap-2 text-sm text-muted">
472
+ {#if quoting}
473
+ <Spinner size={14} />
474
+ Pricing your selection
475
+ {:else}
476
+ Waiting for a price.
477
+ {/if}
463
478
  </p>
464
- {/if}
465
- {#if quoted && quoted.community_discount_cents > 0}
466
- <p class="mt-1 text-end text-xs text-success">
467
- Community discount {quoted.community_discount_pct}% applied.
479
+ {:else}
480
+ <p class="flex items-baseline justify-between gap-2">
481
+ <span class="text-sm text-muted">Total</span>
482
+ <span class="font-mono text-xl font-semibold text-fg">
483
+ {formatCents(total, currency)}<span class="text-sm font-normal text-muted"
484
+ >{perLabel}</span
485
+ >
486
+ </span>
468
487
  </p>
488
+ {#if quoted && quoted.volume_discount_cents > 0}
489
+ <p class="mt-1 text-end text-xs text-muted">
490
+ {formatCents(quoted.volume_discount_cents, currency)} off the list price.
491
+ </p>
492
+ {/if}
493
+ {#if quoted && quoted.community_discount_cents > 0}
494
+ <p class="mt-1 text-end text-xs text-success">
495
+ Community discount {quoted.community_discount_pct}% applied.
496
+ </p>
497
+ {/if}
469
498
  {/if}
470
- {/if}
471
- </div>
472
- {/if}
499
+ </div>
500
+ {/if}
473
501
 
474
- {#if notice}{@render notice()}{/if}
475
- {#if actions}{@render actions({ skus: [...selected], total })}{/if}
502
+ {#if notice}{@render notice()}{/if}
503
+ {#if actions}{@render actions({ skus: [...selected], total })}{/if}
504
+ {/if}
476
505
  </aside>
477
506
  </div>
@@ -38,6 +38,25 @@ interface Props {
38
38
  }]>;
39
39
  /** Anything the host wants between the total and the actions. */
40
40
  notice?: Snippet;
41
+ /**
42
+ * The host's own summary, drawn in place of everything inside the aside:
43
+ * the title line, `error`, the list, the ladder, the total, `notice` and
44
+ * `actions`. For a host that prices more than the selection, such as a
45
+ * plan change or a discount code. The aside keeps its id, its stickiness
46
+ * and the bar that jumps to it on a narrow screen. `remove` takes one
47
+ * selected capability out and reports it through `onchange`, as unticking
48
+ * its card does. A capability that is not selected is left alone. `total`
49
+ * is null both while a price is in flight and before one is asked for, so
50
+ * a host that tells the two apart tracks its own request, as it does for
51
+ * `quoting`.
52
+ */
53
+ summary?: Snippet<[{
54
+ skus: string[];
55
+ total: number | null;
56
+ remove: (sku: string) => void;
57
+ }]>;
58
+ /** The panel's name: the aside's accessible name, also printed atop the default summary. */
59
+ heading?: string;
41
60
  /**
42
61
  * How far below the top of the window the summary sticks on a wide
43
62
  * screen, as a CSS length. A page with a sticky header passes its height,
package/dist/index.d.ts CHANGED
@@ -107,4 +107,4 @@ export { cn, type ClassValue } from './utils/cn.js';
107
107
  export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, watchThemePreference, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
108
108
  export * as motion from './motion.js';
109
109
  export type { Rung as MotionRung, Curve as MotionCurve } from './motion.js';
110
- export declare const VERSION = "0.41.0";
110
+ export declare const VERSION = "0.42.0";
package/dist/index.js CHANGED
@@ -119,4 +119,4 @@ export * as motion from './motion.js';
119
119
  // ── Version ────────────────────────────────────────────────────────────────
120
120
  // Generated from package.json by `pnpm version:sync`. Bump package.json, never
121
121
  // this line. The build and the test suite fail when the two disagree.
122
- export const VERSION = '0.41.0';
122
+ export const VERSION = '0.42.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyeve-labs/ui-kit",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "A clean, accessible, palette-aware Svelte 5 component library. The design system behind LyEve.",
5
5
  "license": "MIT",
6
6
  "author": "LyEve Labs <hello@lyeve.com>",
@@ -58,18 +58,19 @@
58
58
  "@lucide/svelte": "^0.511.0",
59
59
  "@playwright/experimental-ct-svelte": "1.58.2",
60
60
  "@sveltejs/package": "^2.5.7",
61
- "@sveltejs/vite-plugin-svelte": "^4.0.4",
61
+ "@sveltejs/vite-plugin-svelte": "^5.1.1",
62
62
  "@testing-library/svelte": "^5.3.1",
63
- "@vitest/coverage-v8": "^2.1.9",
63
+ "@vitest/coverage-v8": "^4.1.12",
64
64
  "jsdom": "^25.0.1",
65
65
  "prettier": "^3.8.3",
66
66
  "prettier-plugin-svelte": "^4.0.1",
67
67
  "prettier-plugin-tailwindcss": "^0.8.0",
68
68
  "publint": "^0.2.12",
69
- "svelte": "^5.55.9",
69
+ "svelte": "^5.57.2",
70
70
  "svelte-check": "^4.4.8",
71
71
  "typescript": "^5.9.3",
72
- "vitest": "^2.1.9"
72
+ "vite": "^6.4.5",
73
+ "vitest": "^4.1.12"
73
74
  },
74
75
  "scripts": {
75
76
  "build": "pnpm version:check && svelte-package && publint && node scripts/check-dist.mjs",