@lyeve-labs/ui-kit 0.41.1 → 0.42.1

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.
@@ -152,6 +152,7 @@
152
152
  class="{cls} {inert ? 'pointer-events-none opacity-50' : ''}"
153
153
  aria-disabled={inert ? 'true' : undefined}
154
154
  tabindex={inert ? -1 : undefined}
155
+ onclick={inert ? undefined : onclick}
155
156
  {...rest}
156
157
  >
157
158
  {#if loading}<Spinner size={spinnerSize} />{/if}
@@ -54,6 +54,12 @@
54
54
  /** The period that quote was priced for. A monthly quote says nothing about a year. */
55
55
  quotePeriod?: BillingPeriod | null;
56
56
  period?: BillingPeriod;
57
+ /**
58
+ * What the total is charged per, when it differs from `period`. A
59
+ * one-time payment is quoted monthly and charged once, so its host passes
60
+ * `'once'` and the total reads "$6.00 once" rather than "$6.00/mo".
61
+ */
62
+ per?: BillingPeriod | 'once';
57
63
  /** True while a fresh price is in flight. */
58
64
  quoting?: boolean;
59
65
  /** The count the bill will carry, when the host knows better than the cart. */
@@ -69,6 +75,21 @@
69
75
  actions?: Snippet<[{ skus: string[]; total: number | null }]>;
70
76
  /** Anything the host wants between the total and the actions. */
71
77
  notice?: Snippet;
78
+ /**
79
+ * The host's own summary, drawn in place of everything inside the aside:
80
+ * the title line, `error`, the list, the ladder, the total, `notice` and
81
+ * `actions`. For a host that prices more than the selection, such as a
82
+ * plan change or a discount code. The aside keeps its id, its stickiness
83
+ * and the bar that jumps to it on a narrow screen. `remove` takes one
84
+ * selected capability out and reports it through `onchange`, as unticking
85
+ * its card does. A capability that is not selected is left alone. `total`
86
+ * is null both while a price is in flight and before one is asked for, so
87
+ * a host that tells the two apart tracks its own request, as it does for
88
+ * `quoting`.
89
+ */
90
+ summary?: Snippet<[{ skus: string[]; total: number | null; remove: (sku: string) => void }]>;
91
+ /** The panel's name: the aside's accessible name, also printed atop the default summary. */
92
+ heading?: string;
72
93
  /**
73
94
  * How far below the top of the window the summary sticks on a wide
74
95
  * screen, as a CSS length. A page with a sticky header passes its height,
@@ -102,6 +123,7 @@
102
123
  quote = null,
103
124
  quotePeriod = null,
104
125
  period = 'monthly',
126
+ per,
105
127
  quoting = false,
106
128
  billedSkus = null,
107
129
  currency = 'USD',
@@ -111,6 +133,8 @@
111
133
  onchange,
112
134
  actions,
113
135
  notice,
136
+ summary,
137
+ heading = 'Your selection',
114
138
  stickyTop = '1rem',
115
139
  class: klass = '',
116
140
  }: Props = $props();
@@ -176,7 +200,10 @@
176
200
  ),
177
201
  );
178
202
 
179
- const perLabel = $derived(period === 'annual' ? '/yr' : '/mo');
203
+ const perLabel = $derived.by(() => {
204
+ const charged = per ?? period;
205
+ return charged === 'once' ? ' once' : charged === 'annual' ? '/yr' : '/mo';
206
+ });
180
207
 
181
208
  // The volume ladder drawn as a meter: the rungs that take something off,
182
209
  // placed along the count of the last one.
@@ -301,7 +328,7 @@
301
328
  >{plugin.name}</span
302
329
  >
303
330
  </span>
304
- <span id="{id}-price" class="shrink-0 font-mono text-sm font-semibold text-fg">
331
+ <span id="{id}-price" class="shrink-0 tabular-nums text-sm font-semibold text-fg">
305
332
  {formatCents(plugin.price_cents, currency)}<span
306
333
  class="text-xs font-normal text-muted">/mo</span
307
334
  >
@@ -333,13 +360,12 @@
333
360
  class="flex items-center justify-between gap-3 rounded-xl border border-brand bg-surface px-4 py-3 text-sm shadow-lg transition-colors hover:bg-surface-2"
334
361
  >
335
362
  <span class="text-fg">
336
- <span class="font-semibold">{count}</span> picked{#if total !== null}<span
337
- class="text-muted"
363
+ <span class="font-semibold">{count}</span> picked{#if total !== null}{' '}<span
364
+ class="text-muted">·</span
338
365
  >
339
- ·
340
- </span><span class="font-mono">{formatCents(total, currency)}{perLabel}</span>{/if}
366
+ <span class="tabular-nums">{formatCents(total, currency)}</span>{/if}
341
367
  </span>
342
- <span class="font-semibold text-brand">Review</span>
368
+ <span class="font-semibold text-brand">Review order</span>
343
369
  </a>
344
370
  </div>
345
371
  {/if}
@@ -349,129 +375,141 @@
349
375
  id={summaryId}
350
376
  class="scroll-mt-20 self-start rounded-xl border border-line bg-surface p-5 lg:sticky"
351
377
  style:top={stickyTop}
352
- aria-label="Your selection"
378
+ aria-label={heading}
353
379
  >
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>
380
+ {#if summary}
381
+ {@render summary({
382
+ skus: [...selected],
383
+ total,
384
+ remove: (sku) => {
385
+ if (selected.has(sku)) toggle(sku);
386
+ },
387
+ })}
388
+ {:else}
389
+ <div class="mb-4 flex items-center justify-between gap-2">
390
+ <p class="text-xs font-semibold uppercase tracking-widest text-faint">{heading}</p>
391
+ {#if count > 0}
392
+ <Badge tone="brand" size="sm">
393
+ {count}
394
+ {count === 1 ? 'capability' : 'capabilities'}
395
+ </Badge>
396
+ {/if}
397
+ </div>
363
398
 
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}
399
+ {#if error}
400
+ <p class="mb-3 rounded-lg border border-danger/30 bg-danger/10 p-3 text-xs text-danger">
401
+ {error}
402
+ </p>
403
+ {/if}
369
404
 
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>
405
+ {#if count === 0}
406
+ <p class="py-6 text-center text-sm text-muted">
407
+ Nothing picked yet. With none of these you are on the free tier, which needs no checkout.
408
+ </p>
409
+ {:else}
410
+ <ul class="mb-4 max-h-56 space-y-1 overflow-y-auto pe-1">
411
+ {#each [...selected] as sku (sku)}
412
+ {@const p = bySku.get(sku)}
413
+ <li class="flex items-center justify-between gap-2 text-xs">
414
+ <span class="truncate text-fg">{p?.name ?? sku}</span>
415
+ <span class="flex items-center gap-1">
416
+ <span class="tabular-nums text-muted"
417
+ >{formatCents(p?.price_cents ?? 0, currency)}</span
418
+ >
419
+ <Button
420
+ type="button"
421
+ variant="ghost"
422
+ size="sm"
423
+ onclick={() => toggle(sku)}
424
+ aria-label={`Take ${p?.name ?? sku} out of your selection`}
425
+ >
426
+ <X size={12} />
427
+ </Button>
428
+ </span>
429
+ </li>
430
+ {/each}
431
+ </ul>
395
432
 
396
- {#if rungs.length > 0}
397
- <!-- The ladder as a meter, so "two more" is something a reader can
433
+ {#if rungs.length > 0}
434
+ <!-- The ladder as a meter, so "two more" is something a reader can
398
435
  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)}
436
+ <div class="mb-3" aria-hidden="true">
437
+ <div class="relative h-1.5 rounded-full bg-surface-2">
406
438
  <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}%"
439
+ class="absolute inset-y-0 start-0 rounded-full bg-brand transition-[width] duration-progress"
440
+ style:width="{ladderFill}%"
414
441
  ></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}
442
+ {#each rungs as r (r.plugins)}
443
+ <span
444
+ class={cn(
445
+ 'absolute top-1/2 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 transition-colors',
446
+ ladder.count >= r.plugins
447
+ ? 'border-brand bg-brand'
448
+ : 'border-line-strong bg-surface',
449
+ )}
450
+ style:left="{(r.plugins / ladderTop) * 100}%"
451
+ ></span>
452
+ {/each}
453
+ </div>
454
+ <div class="relative mt-2 h-4 text-[10px] text-muted">
455
+ {#each rungs as r (r.plugins)}
456
+ <span
457
+ class="absolute -translate-x-1/2 whitespace-nowrap tabular-nums"
458
+ style:left="{Math.min((r.plugins / ladderTop) * 100, 92)}%"
459
+ >
460
+ {r.plugins}+ · {r.discount_pct}%
461
+ </span>
462
+ {/each}
463
+ </div>
426
464
  </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}
465
+ {/if}
440
466
 
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}
467
+ {#if ladder.reachedPct > 0}
468
+ <p class="mb-2 text-xs text-success">
469
+ Volume discount {ladder.reachedPct}% applied.
450
470
  </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>
471
+ {/if}
472
+ {#if ladder.next}
473
+ <p class="mb-3 text-xs text-muted">
474
+ {ladder.next.more} more and the discount is {ladder.next.discount_pct}%.
459
475
  </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.
476
+ {/if}
477
+
478
+ <div class="mb-4 border-t border-line pt-3">
479
+ {#if total === null}
480
+ <p class="flex items-center gap-2 text-sm text-muted">
481
+ {#if quoting}
482
+ <Spinner size={14} />
483
+ Pricing your selection
484
+ {:else}
485
+ Waiting for a price.
486
+ {/if}
463
487
  </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.
488
+ {:else}
489
+ <p class="flex items-baseline justify-between gap-2">
490
+ <span class="text-sm text-muted">Total</span>
491
+ <span class="tabular-nums text-xl font-semibold text-fg">
492
+ {formatCents(total, currency)}<span class="text-sm font-normal text-muted"
493
+ >{perLabel}</span
494
+ >
495
+ </span>
468
496
  </p>
497
+ {#if quoted && quoted.volume_discount_cents > 0}
498
+ <p class="mt-1 text-end text-xs text-muted">
499
+ {formatCents(quoted.volume_discount_cents, currency)} off the list price.
500
+ </p>
501
+ {/if}
502
+ {#if quoted && quoted.community_discount_cents > 0}
503
+ <p class="mt-1 text-end text-xs text-success">
504
+ Community discount {quoted.community_discount_pct}% applied.
505
+ </p>
506
+ {/if}
469
507
  {/if}
470
- {/if}
471
- </div>
472
- {/if}
508
+ </div>
509
+ {/if}
473
510
 
474
- {#if notice}{@render notice()}{/if}
475
- {#if actions}{@render actions({ skus: [...selected], total })}{/if}
511
+ {#if notice}{@render notice()}{/if}
512
+ {#if actions}{@render actions({ skus: [...selected], total })}{/if}
513
+ {/if}
476
514
  </aside>
477
515
  </div>
@@ -20,6 +20,12 @@ interface Props {
20
20
  /** The period that quote was priced for. A monthly quote says nothing about a year. */
21
21
  quotePeriod?: BillingPeriod | null;
22
22
  period?: BillingPeriod;
23
+ /**
24
+ * What the total is charged per, when it differs from `period`. A
25
+ * one-time payment is quoted monthly and charged once, so its host passes
26
+ * `'once'` and the total reads "$6.00 once" rather than "$6.00/mo".
27
+ */
28
+ per?: BillingPeriod | 'once';
23
29
  /** True while a fresh price is in flight. */
24
30
  quoting?: boolean;
25
31
  /** The count the bill will carry, when the host knows better than the cart. */
@@ -38,6 +44,25 @@ interface Props {
38
44
  }]>;
39
45
  /** Anything the host wants between the total and the actions. */
40
46
  notice?: Snippet;
47
+ /**
48
+ * The host's own summary, drawn in place of everything inside the aside:
49
+ * the title line, `error`, the list, the ladder, the total, `notice` and
50
+ * `actions`. For a host that prices more than the selection, such as a
51
+ * plan change or a discount code. The aside keeps its id, its stickiness
52
+ * and the bar that jumps to it on a narrow screen. `remove` takes one
53
+ * selected capability out and reports it through `onchange`, as unticking
54
+ * its card does. A capability that is not selected is left alone. `total`
55
+ * is null both while a price is in flight and before one is asked for, so
56
+ * a host that tells the two apart tracks its own request, as it does for
57
+ * `quoting`.
58
+ */
59
+ summary?: Snippet<[{
60
+ skus: string[];
61
+ total: number | null;
62
+ remove: (sku: string) => void;
63
+ }]>;
64
+ /** The panel's name: the aside's accessible name, also printed atop the default summary. */
65
+ heading?: string;
41
66
  /**
42
67
  * How far below the top of the window the summary sticks on a wide
43
68
  * 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.1";
110
+ export declare const VERSION = "0.42.1";
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.1';
122
+ export const VERSION = '0.42.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyeve-labs/ui-kit",
3
- "version": "0.41.1",
3
+ "version": "0.42.1",
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>",