@lyeve-labs/ui-kit 0.34.0 → 0.35.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.
package/README.md CHANGED
@@ -28,7 +28,7 @@ No config file, no theme provider, no setup ceremony.
28
28
 
29
29
  ## What's in the box
30
30
 
31
- - **71 components:** buttons, inputs, modals, drawers, tabs, tables, toasts, the works.
31
+ - **72 components:** buttons, inputs, modals, drawers, tabs, tables, toasts, the works.
32
32
  - **Two themes:** Soft Dark (default) and Soft Light, switched by a single `data-theme` attribute on `<html>`.
33
33
  - **One CSS file:** `@lyeve-labs/ui-kit/styles.css` declares every token. The rest is just Tailwind.
34
34
  - **Svelte 5 native:** built on runes and snippets, fully typed end-to-end.
@@ -37,13 +37,13 @@ No config file, no theme provider, no setup ceremony.
37
37
  ## Component list
38
38
 
39
39
  <details>
40
- <summary>71 components, organized by purpose</summary>
40
+ <summary>72 components, organized by purpose</summary>
41
41
 
42
42
  **Layout and structure**
43
43
  Card, Panel, AppShell, AuthShell, PageShell, PageHeader, SectionHeading, Divider, Accordion, AccordionItem, Collapsible, Table, DescriptionList, Toolbar, TreeView
44
44
 
45
45
  **Forms and inputs**
46
- Button, ButtonGroup, Input, PasswordInput, CopyField, Textarea, NumberInput, SearchInput, FileInput, Label, Field, FormMessage, SegmentedControl, Select, MultiSelect, Autocomplete, DatePicker, TimePicker, DateTimePicker, Checkbox, CheckboxGroup, Radio, RadioGroup, Toggle
46
+ Button, ButtonGroup, Input, PasswordInput, CopyField, Textarea, NumberInput, SearchInput, FileInput, Label, Field, FormMessage, SegmentedControl, FilterChips, Select, MultiSelect, Autocomplete, DatePicker, TimePicker, DateTimePicker, Checkbox, CheckboxGroup, Radio, RadioGroup, Toggle
47
47
 
48
48
  **Navigation**
49
49
  Breadcrumb, Tabs, Pagination, StepIndicator, Dropdown, SidebarNav, AccountMenu
@@ -253,7 +253,7 @@ This repo is a single-purpose component library. Nothing but `src/lib/`.
253
253
  ```
254
254
  src/
255
255
  └── lib/ # → published as @lyeve-labs/ui-kit
256
- ├── components/ # 71 .svelte files
256
+ ├── components/ # 72 .svelte files
257
257
  ├── stores/ # toast.svelte.ts
258
258
  ├── styles/ # theme.css (the one stylesheet)
259
259
  ├── utils/ # cn.ts, theme.ts
@@ -93,7 +93,13 @@
93
93
  // is what stops a real click, and it is the only thing that does, so a
94
94
  // change arriving any other way (a synthetic event, a script driving the
95
95
  // node) would still move the bound value with nothing on screen to say so.
96
- if (disabled) return;
96
+ // bind:checked has already taken the input's new state, so a disabled box
97
+ // puts the input and the value back.
98
+ if (disabled) {
99
+ e.currentTarget.checked = !e.currentTarget.checked;
100
+ checked = e.currentTarget.checked;
101
+ return;
102
+ }
97
103
  // The DOM clears indeterminate on the first click. Leaving the prop set
98
104
  // would repaint the mixed bar over a box the user has just ticked.
99
105
  indeterminate = false;
@@ -102,6 +108,10 @@
102
108
  }
103
109
  </script>
104
110
 
111
+ <!-- bind:checked and not {checked}: a visitor can tick the box before the
112
+ page hydrates. A one-way prop is written over the input when the page
113
+ hydrates and clears that tick. The binding reads the input's live state
114
+ instead, the same way the browser kept it. -->
105
115
  {#snippet control()}
106
116
  <input
107
117
  type="checkbox"
@@ -110,7 +120,7 @@
110
120
  {value}
111
121
  {required}
112
122
  {disabled}
113
- {checked}
123
+ bind:checked
114
124
  {indeterminate}
115
125
  aria-checked={indeterminate ? 'mixed' : undefined}
116
126
  aria-invalid={error ? 'true' : undefined}
@@ -0,0 +1,86 @@
1
+ <script lang="ts" module>
2
+ import type { Component } from 'svelte';
3
+
4
+ /** One chip. */
5
+ export interface FilterChip<T extends string = string> {
6
+ /** The chosen value, and the key the chip is rendered under. */
7
+ value: T;
8
+ /** What the reader sees. */
9
+ label: string;
10
+ /** How many rows this chip leaves on screen, drawn small beside the label. */
11
+ count?: number;
12
+ /** A lucide component before the label. */
13
+ icon?: Component<{ size?: number; class?: string }>;
14
+ }
15
+ </script>
16
+
17
+ <script lang="ts" generics="T extends string">
18
+ /**
19
+ * One filter out of many: a row of chips that wraps.
20
+ *
21
+ * A SegmentedControl holds two to four short options in one strip. A
22
+ * catalog filtered by a dozen categories does not fit one: the strip either
23
+ * scrolls, hiding categories past the edge, or squeezes each label onto two
24
+ * lines inside its segment. Chips wrap instead, so every category is in
25
+ * sight at every width. One is always pressed.
26
+ */
27
+ import { cn } from '../utils/cn.js';
28
+ import { HIT_AREA } from '../internal/touch.js';
29
+
30
+ interface Props {
31
+ /** The pressed chip's value. */
32
+ value: T;
33
+ options: FilterChip<T>[];
34
+ /** Names the group for assistive technology. The chips name themselves. */
35
+ label: string;
36
+ /** Read after the count, so "3" is heard as "3 on sale". */
37
+ countLabel?: string;
38
+ class?: string;
39
+ onchange?: (value: T) => void;
40
+ }
41
+
42
+ let {
43
+ value = $bindable(),
44
+ options,
45
+ label,
46
+ countLabel = '',
47
+ class: klass = '',
48
+ onchange = undefined,
49
+ }: Props = $props();
50
+
51
+ function pick(next: T): void {
52
+ if (next === value) return;
53
+ value = next;
54
+ onchange?.(next);
55
+ }
56
+ </script>
57
+
58
+ <div class={cn('flex flex-wrap gap-1.5', klass)} role="group" aria-label={label}>
59
+ {#each options as chip (chip.value)}
60
+ {@const pressed = chip.value === value}
61
+ <button
62
+ type="button"
63
+ aria-pressed={pressed}
64
+ class={cn(
65
+ HIT_AREA,
66
+ 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors',
67
+ 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand',
68
+ pressed
69
+ ? 'border-brand bg-brand/10 text-fg'
70
+ : 'border-line text-muted hover:border-line-strong hover:text-fg',
71
+ )}
72
+ onclick={() => pick(chip.value)}
73
+ >
74
+ {#if chip.icon}
75
+ {@const Icon = chip.icon}
76
+ <Icon size={12} class="shrink-0" />
77
+ {/if}
78
+ {chip.label}
79
+ {#if chip.count !== undefined}
80
+ <span class="font-mono text-[10px] text-faint"
81
+ >{chip.count}{#if countLabel}<span class="sr-only"> {countLabel}</span>{/if}</span
82
+ >
83
+ {/if}
84
+ </button>
85
+ {/each}
86
+ </div>
@@ -0,0 +1,49 @@
1
+ import type { Component } from 'svelte';
2
+ /** One chip. */
3
+ export interface FilterChip<T extends string = string> {
4
+ /** The chosen value, and the key the chip is rendered under. */
5
+ value: T;
6
+ /** What the reader sees. */
7
+ label: string;
8
+ /** How many rows this chip leaves on screen, drawn small beside the label. */
9
+ count?: number;
10
+ /** A lucide component before the label. */
11
+ icon?: Component<{
12
+ size?: number;
13
+ class?: string;
14
+ }>;
15
+ }
16
+ declare function $$render<T extends string>(): {
17
+ props: {
18
+ /** The pressed chip's value. */
19
+ value: T;
20
+ options: FilterChip<T>[];
21
+ /** Names the group for assistive technology. The chips name themselves. */
22
+ label: string;
23
+ /** Read after the count, so "3" is heard as "3 on sale". */
24
+ countLabel?: string;
25
+ class?: string;
26
+ onchange?: (value: T) => void;
27
+ };
28
+ exports: {};
29
+ bindings: "value";
30
+ slots: {};
31
+ events: {};
32
+ };
33
+ declare class __sveltets_Render<T extends string> {
34
+ props(): ReturnType<typeof $$render<T>>['props'];
35
+ events(): ReturnType<typeof $$render<T>>['events'];
36
+ slots(): ReturnType<typeof $$render<T>>['slots'];
37
+ bindings(): "value";
38
+ exports(): {};
39
+ }
40
+ interface $$IsomorphicComponent {
41
+ new <T extends string>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
42
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
43
+ } & ReturnType<__sveltets_Render<T>['exports']>;
44
+ <T extends string>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
45
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
46
+ }
47
+ declare const FilterChips: $$IsomorphicComponent;
48
+ type FilterChips<T extends string> = InstanceType<typeof FilterChips<T>>;
49
+ export default FilterChips;
@@ -15,6 +15,7 @@
15
15
  import { X } from '@lucide/svelte';
16
16
  import Badge from './Badge.svelte';
17
17
  import Button from './Button.svelte';
18
+ import FilterChips from './FilterChips.svelte';
18
19
  import SearchInput from './SearchInput.svelte';
19
20
  import Spinner from './Spinner.svelte';
20
21
  import * as motion from '../motion.js';
@@ -161,7 +162,11 @@
161
162
  );
162
163
 
163
164
  const total = $derived(
164
- quoted ? (period === 'annual' ? (quoted.annual_total_cents ?? null) : quoted.total_cents) : null,
165
+ quoted
166
+ ? period === 'annual'
167
+ ? (quoted.annual_total_cents ?? null)
168
+ : quoted.total_cents
169
+ : null,
165
170
  );
166
171
 
167
172
  const ladder = $derived(
@@ -229,26 +234,12 @@
229
234
  {#if categories.length > 2}
230
235
  <!-- Chips that wrap rather than a strip that scrolls: every category is
231
236
  in sight at every width, and none hides past the edge. -->
232
- <div class="flex flex-wrap gap-1.5" role="group" aria-label="Category">
233
- {#each categories as c (c.value)}
234
- <button
235
- type="button"
236
- aria-pressed={category === c.value}
237
- class={cn(
238
- HIT_AREA,
239
- 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors',
240
- 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand',
241
- category === c.value
242
- ? 'border-brand bg-brand/10 text-fg'
243
- : 'border-line text-muted hover:border-line-strong hover:text-fg',
244
- )}
245
- onclick={() => (category = c.value)}
246
- >
247
- {c.label}
248
- <span class="font-mono text-[10px] text-faint">{c.count}<span class="sr-only"> on sale</span></span>
249
- </button>
250
- {/each}
251
- </div>
237
+ <FilterChips
238
+ label="Category"
239
+ countLabel="on sale"
240
+ options={categories}
241
+ bind:value={category}
242
+ />
252
243
  {/if}
253
244
 
254
245
  {#if shown.length === 0}
@@ -278,7 +269,9 @@
278
269
  aria-describedby="{id}-price {id}-blurb{tagged ? ` ${id}-tags` : ''}"
279
270
  onchange={() => toggle(plugin.sku)}
280
271
  />
281
- <span class={cn(choiceWrap('card', on, owned), CHOICE_FOCUS, 'h-full flex-col gap-2 p-4')}>
272
+ <span
273
+ class={cn(choiceWrap('card', on, owned), CHOICE_FOCUS, 'h-full flex-col gap-2 p-4')}
274
+ >
282
275
  <span class="flex w-full items-start justify-between gap-3">
283
276
  <span class="flex min-w-0 items-start gap-2.5">
284
277
  <span
@@ -289,15 +282,29 @@
289
282
  aria-hidden="true"
290
283
  >
291
284
  {#if on}
292
- <svg viewBox="0 0 10 8" class="size-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d={CHOICE_MARK.check} /></svg>
285
+ <svg
286
+ viewBox="0 0 10 8"
287
+ class="size-2.5"
288
+ fill="none"
289
+ stroke="currentColor"
290
+ stroke-width="1.5"
291
+ stroke-linecap="round"
292
+ stroke-linejoin="round"><path d={CHOICE_MARK.check} /></svg
293
+ >
293
294
  {/if}
294
295
  </span>
295
296
  <!-- A one-word name longer than the space beside the price breaks
296
297
  with a hyphen rather than running under it. -->
297
- <span id="{id}-name" class="min-w-0 text-sm font-semibold leading-snug text-fg hyphens-auto [overflow-wrap:anywhere]">{plugin.name}</span>
298
+ <span
299
+ id="{id}-name"
300
+ class="min-w-0 text-sm font-semibold leading-snug text-fg hyphens-auto [overflow-wrap:anywhere]"
301
+ >{plugin.name}</span
302
+ >
298
303
  </span>
299
304
  <span id="{id}-price" class="shrink-0 font-mono text-sm font-semibold text-fg">
300
- {formatCents(plugin.price_cents, currency)}<span class="text-xs font-normal text-muted">/mo</span>
305
+ {formatCents(plugin.price_cents, currency)}<span
306
+ class="text-xs font-normal text-muted">/mo</span
307
+ >
301
308
  </span>
302
309
  </span>
303
310
  <span id="{id}-blurb" class="text-xs leading-relaxed text-muted">
@@ -305,7 +312,8 @@
305
312
  </span>
306
313
  {#if tagged}
307
314
  <span id="{id}-tags" class="mt-auto flex flex-wrap gap-1.5 pt-1">
308
- {#if plugin.category}<Badge tone="neutral" size="sm">{plugin.category}</Badge>{/if}
315
+ {#if plugin.category}<Badge tone="neutral" size="sm">{plugin.category}</Badge
316
+ >{/if}
309
317
  {#if blurb.beta}<Badge tone="violet" size="sm">Beta</Badge>{/if}
310
318
  {#if owned}<Badge tone="success" size="sm">Owned</Badge>{/if}
311
319
  </span>
@@ -325,8 +333,11 @@
325
333
  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"
326
334
  >
327
335
  <span class="text-fg">
328
- <span class="font-semibold">{count}</span> picked{#if total !== null}<span class="text-muted">
329
- · </span><span class="font-mono">{formatCents(total, currency)}{perLabel}</span>{/if}
336
+ <span class="font-semibold">{count}</span> picked{#if total !== null}<span
337
+ class="text-muted"
338
+ >
339
+ ·
340
+ </span><span class="font-mono">{formatCents(total, currency)}{perLabel}</span>{/if}
330
341
  </span>
331
342
  <span class="font-semibold text-brand">Review</span>
332
343
  </a>
@@ -395,7 +406,9 @@
395
406
  <span
396
407
  class={cn(
397
408
  'absolute top-1/2 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 transition-colors',
398
- ladder.count >= r.plugins ? 'border-brand bg-brand' : 'border-line-strong bg-surface',
409
+ ladder.count >= r.plugins
410
+ ? 'border-brand bg-brand'
411
+ : 'border-line-strong bg-surface',
399
412
  )}
400
413
  style:left="{(r.plugins / ladderTop) * 100}%"
401
414
  ></span>
@@ -403,7 +416,10 @@
403
416
  </div>
404
417
  <div class="relative mt-2 h-4 text-[10px] text-muted">
405
418
  {#each rungs as r (r.plugins)}
406
- <span class="absolute -translate-x-1/2 whitespace-nowrap font-mono" style:left="{Math.min((r.plugins / ladderTop) * 100, 92)}%">
419
+ <span
420
+ class="absolute -translate-x-1/2 whitespace-nowrap font-mono"
421
+ style:left="{Math.min((r.plugins / ladderTop) * 100, 92)}%"
422
+ >
407
423
  {r.plugins}+ · {r.discount_pct}%
408
424
  </span>
409
425
  {/each}
package/dist/index.d.ts CHANGED
@@ -37,6 +37,8 @@ export { default as Label } from './components/Label.svelte';
37
37
  export { default as Field } from './components/Field.svelte';
38
38
  export { default as FormMessage } from './components/FormMessage.svelte';
39
39
  export { default as SegmentedControl } from './components/SegmentedControl.svelte';
40
+ export { default as FilterChips } from './components/FilterChips.svelte';
41
+ export type { FilterChip } from './components/FilterChips.svelte';
40
42
  export { default as Select } from './components/Select.svelte';
41
43
  export { default as MultiSelect } from './components/MultiSelect.svelte';
42
44
  export { default as Autocomplete } from './components/Autocomplete.svelte';
@@ -98,4 +100,4 @@ export { cn, type ClassValue } from './utils/cn.js';
98
100
  export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, watchThemePreference, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
99
101
  export * as motion from './motion.js';
100
102
  export type { Rung as MotionRung, Curve as MotionCurve } from './motion.js';
101
- export declare const VERSION = "0.34.0";
103
+ export declare const VERSION = "0.35.1";
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ export { default as Label } from './components/Label.svelte';
38
38
  export { default as Field } from './components/Field.svelte';
39
39
  export { default as FormMessage } from './components/FormMessage.svelte';
40
40
  export { default as SegmentedControl } from './components/SegmentedControl.svelte';
41
+ export { default as FilterChips } from './components/FilterChips.svelte';
41
42
  export { default as Select } from './components/Select.svelte';
42
43
  export { default as MultiSelect } from './components/MultiSelect.svelte';
43
44
  export { default as Autocomplete } from './components/Autocomplete.svelte';
@@ -107,4 +108,4 @@ export * as motion from './motion.js';
107
108
  // ── Version ────────────────────────────────────────────────────────────────
108
109
  // Generated from package.json by `pnpm version:sync`. Bump package.json, never
109
110
  // this line. The build and the test suite fail when the two disagree.
110
- export const VERSION = '0.34.0';
111
+ export const VERSION = '0.35.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyeve-labs/ui-kit",
3
- "version": "0.34.0",
3
+ "version": "0.35.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>",