Skip to content

Commit b85513e

Browse files
authored
chore: small retouches (#10)
* fix: update value type in InputProps to ClassValue and improve class binding * fix: ensure proper binding in toClassValue calls for class handling * fix: refine class binding in dropdown query component and remove unused imports * fix: update dropdown story with new data and improve trigger functionality * fix: add missing line breaks for improved readability in dropdown index file * fix: simplify selected items rendering in dropdown trigger * docs: enhance README with composability section and advanced usage example * bump version to 1.0.0-alpha.20
1 parent 54098da commit b85513e

File tree

7 files changed

+135
-46
lines changed

7 files changed

+135
-46
lines changed

README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ Leverages Svelte's fine-grained reactivity system for optimal performance and sm
4040

4141
Components are headless by default, giving you complete control over styling while providing sensible defaults.
4242

43+
### 🎨 **Composable**
44+
45+
Build complex UIs by combining simple, reusable components. Each component is designed to work seamlessly with others through the Bond pattern and context API. Create sophisticated features like multi-level dropdowns, nested accordions, or custom form controls by composing atomic components together.
46+
4347
---
4448

4549
## 📦 Available Components
@@ -268,6 +272,85 @@ For more control, you can use the Bond system directly:
268272
</div>
269273
```
270274

275+
### Advanced Usage With Composition
276+
277+
This example demonstrates the power of component composition by combining `Dropdown`, `Input`, and animation capabilities to create a searchable multi-select dropdown with smooth transitions:
278+
279+
```svelte
280+
<script lang="ts">
281+
import { Dropdown, Input, Root, filter } from '@svelte-atoms/core';
282+
import { flip } from 'svelte/animate';
283+
284+
// Sample data
285+
let data = [
286+
{ id: 1, value: 'apple', text: 'Apple' },
287+
{ id: 2, value: 'banana', text: 'Banana' },
288+
{ id: 3, value: 'cherry', text: 'Cherry' },
289+
{ id: 4, value: 'date', text: 'Date' },
290+
{ id: 5, value: 'elderberry', text: 'Elderberry' }
291+
];
292+
293+
let open = $state(false);
294+
// Filter items based on search query
295+
const dd = filter(
296+
() => data,
297+
(query, item) => item.text.toLowerCase().includes(query.toLowerCase())
298+
);
299+
</script>
300+
301+
<Root class="items-center justify-center p-4">
302+
<!-- Multi-select dropdown with search functionality -->
303+
<Dropdown.Root
304+
bind:open
305+
multiple
306+
keys={data.map((item) => item.value)}
307+
onquerychange={(q) => (dd.query = q)}
308+
>
309+
{#snippet children({ dropdown })}
310+
<!-- Compose Dropdown.Trigger with Input.Root for a custom trigger -->
311+
<Dropdown.Trigger
312+
base={Input.Root}
313+
class="h-auto min-h-12 max-w-sm min-w-sm items-center gap-2 rounded-sm px-4 transition-colors duration-200"
314+
onclick={(ev) => {
315+
ev.preventDefault();
316+
317+
dropdown.state.open();
318+
}}
319+
>
320+
<!-- Display selected values with animation -->
321+
{#each dropdown?.state?.selectedItems ?? [] as item (item.id)}
322+
<div animate:flip={{ duration: 200 }}>
323+
<ADropdown.Value value={item.value} class="text-foreground/80">
324+
{item.text}
325+
</ADropdown.Value>
326+
</div>
327+
{/each}
328+
329+
<!-- Inline search input within the trigger -->
330+
<Dropdown.Query class="flex-1 px-1" placeholder="Search for fruits..." />
331+
</Dropdown.Trigger>
332+
333+
<!-- Dropdown list with filtered items -->
334+
<Dropdown.List>
335+
{#each dd.current as item (item.id)}
336+
<div animate:flip={{ duration: 200 }}>
337+
<Dropdown.Item value={item.value}>{item.text}</Dropdown.Item>
338+
</div>
339+
{/each}
340+
</Dropdown.List>
341+
{/snippet}
342+
</Dropdown.Root>
343+
</Root>
344+
```
345+
346+
**Key composition features demonstrated:**
347+
348+
- **Component Fusion**: Using `base={Input.Root}` to compose Dropdown.Trigger with Input styling and behavior
349+
- **Snippet Patterns**: Accessing internal state through snippets for custom rendering
350+
- **Reactive Filtering**: Combining search query state with reactive effects for real-time filtering
351+
- **Smooth Animations**: Using Svelte's `flip` animation for seamless list transitions
352+
- **Multi-Select State**: Managing complex selection state through the Bond pattern
353+
271354
---
272355

273356
## 📖 Documentation

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@svelte-atoms/core",
3-
"version": "1.0.0-alpha.19",
3+
"version": "1.0.0-alpha.20",
44
"description": "A modular, accessible, and extensible Svelte UI component library.",
55
"repository": {
66
"type": "git",

src/lib/components/atom/html-atom.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838
return cls.replace('$preset', cn(preset?.class));
3939
}
4040
41-
return toClassValue.apply(bond, [cls]);
41+
return toClassValue.apply(bond, [cls, bond]);
4242
});
4343
}
4444
45-
return [preset?.class ?? '', toClassValue.apply(bond, [klass])];
45+
return [preset?.class ?? '', toClassValue.apply(bond, [klass, bond])];
4646
});
4747
4848
const _base = $derived(base ?? preset?.base);

src/lib/components/dropdown/dropdown-query.svelte

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
<script lang="ts" generics="T extends keyof HTMLElementTagNameMap = 'div', S extends Shell = Shell">
2-
import { onMount, type Component } from 'svelte';
2+
import { onMount } from 'svelte';
33
import { DropdownBond } from './bond.svelte';
44
import { Input } from '$svelte-atoms/core/components/input';
5-
import { toClassValue, cn } from '$svelte-atoms/core/utils';
65
76
const bond = DropdownBond.get() as DropdownBond;
87
@@ -41,14 +40,9 @@
4140

4241
<Input.Value
4342
bind:value={bond.state.query}
44-
preset="dropdown.query"
45-
class={['inline-flex w-min flex-1 py-1', '$preset', klass]}
4643
{bond}
47-
onpointerdown={(ev) => {
48-
ev.stopPropagation();
49-
50-
bond.state.open();
51-
}}
44+
preset="dropdown.query"
45+
class={['inline-flex h-auto w-auto flex-1 py-1', '$preset', klass]}
5246
onmount={onmount?.bind(bond.state)}
5347
ondestroy={ondestroy?.bind(bond.state)}
5448
enter={enter?.bind(bond.state)}
Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script module>
22
import { defineMeta } from '@storybook/addon-svelte-csf';
33
import { Dropdown as ADropdown } from '.';
4-
import { Root as DropdownRoot } from './atoms';
54
import Root from '$svelte-atoms/core/components/root/root.svelte';
65
import { Input } from '$svelte-atoms/core/components/input';
76
import { flip } from 'svelte/animate';
@@ -24,10 +23,11 @@
2423
let open = $state(false);
2524
2625
const data = $state([
27-
{ id: '1', value: 'ar', text: 'Arabic' },
28-
{ id: '2', value: 'en', text: 'English' },
29-
{ id: '3', value: 'sp', text: 'Spanish' },
30-
{ id: '4', value: 'it', text: 'Italian' }
26+
{ id: 1, value: 'apple', text: 'Apple' },
27+
{ id: 2, value: 'banana', text: 'Banana' },
28+
{ id: 3, value: 'cherry', text: 'Cherry' },
29+
{ id: 4, value: 'date', text: 'Date' },
30+
{ id: 5, value: 'elderberry', text: 'Elderberry' }
3131
]);
3232
3333
const dd = filter(
@@ -38,37 +38,46 @@
3838

3939
<Story name="Dropdown" args={{}}>
4040
<Root class="items-center justify-center p-4">
41+
<!-- Multi-select dropdown with search functionality -->
4142
<ADropdown.Root
4243
bind:open
4344
keys={data.map((item) => item.value)}
4445
multiple
4546
onquerychange={(q) => (dd.query = q)}
4647
>
47-
<ADropdown.Trigger
48-
base={Input.Root}
49-
class="hover:bg-foreground/5 active:bg-foreground/10 max-w-sm min-w-sm items-center gap-2 rounded-sm px-4 transition-colors duration-200"
50-
>
51-
<ADropdown.Values>
52-
{#snippet children({ items })}
53-
{#each items as item (item.id)}
54-
<div animate:flip={{ duration: 200 }}>
55-
<ADropdown.Value value={item.value} class="text-foreground/80"
56-
>{item.text} - {item.value}</ADropdown.Value
57-
>
58-
</div>
59-
{/each}
60-
{/snippet}
61-
</ADropdown.Values>
48+
{#snippet children({ dropdown })}
49+
<!-- Compose ADropdown.Trigger with Input.Root for a custom trigger -->
50+
<ADropdown.Trigger
51+
base={Input.Root}
52+
class="h-auto min-h-12 max-w-sm min-w-sm items-center gap-2 rounded-sm px-4 transition-colors duration-200"
53+
onclick={(ev) => {
54+
ev.preventDefault();
6255

63-
<ADropdown.Query class="flex-1 px-1" placeholder={'Search for items'} />
64-
</ADropdown.Trigger>
65-
<ADropdown.List>
66-
{#each dd.current as item (item.id)}
67-
<div animate:flip={{ duration: 200 }}>
68-
<ADropdown.Item value={item.value}>{item.text}</ADropdown.Item>
69-
</div>
70-
{/each}
71-
</ADropdown.List>
56+
dropdown.state.open();
57+
}}
58+
>
59+
<!-- Display selected values with animation -->
60+
{#each dropdown?.state?.selectedItems ?? [] as item (item.id)}
61+
<div animate:flip={{ duration: 200 }}>
62+
<ADropdown.Value value={item.value} class="text-foreground/80">
63+
{item.text}
64+
</ADropdown.Value>
65+
</div>
66+
{/each}
67+
68+
<!-- Inline search input within the trigger -->
69+
<ADropdown.Query class="flex-1 px-1" placeholder="Search for fruits..." />
70+
</ADropdown.Trigger>
71+
72+
<!-- ADropdown list with filtered items -->
73+
<ADropdown.List>
74+
{#each dd.current as item (item.id)}
75+
<div animate:flip={{ duration: 200 }}>
76+
<ADropdown.Item value={item.value}>{item.text}</ADropdown.Item>
77+
</div>
78+
{/each}
79+
</ADropdown.List>
80+
{/snippet}
7281
</ADropdown.Root>
7382
</Root>
7483
</Story>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
export * as Dropdown from './atoms';
2+
23
export {
34
DropdownBond,
45
type DropdownBondElements,
56
DropdownBondState,
67
type DropdownStateProps
78
} from './bond.svelte';
9+
10+
export { filter } from './runes.svelte';

src/lib/components/input/input-value.svelte

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
export type InputPortals = 'input.l0' | 'input.l1' | 'input.l2' | 'input.l3';
33
44
export type InputProps = {
5-
value?: string;
5+
value?: ClassValue;
66
files?: File[];
77
date?: Date | null;
88
number?: number;
@@ -19,7 +19,7 @@
1919
import type { HTMLInputTypeAttribute } from 'svelte/elements';
2020
import { on } from '$svelte-atoms/core/attachments/event.svelte';
2121
import { getPreset } from '$svelte-atoms/core/context';
22-
import { toClassValue } from '$svelte-atoms/core/utils';
22+
import { cn, toClassValue, type ClassValue } from '$svelte-atoms/core/utils';
2323
import type { PresetModuleName } from '$svelte-atoms/core/context/preset.svelte';
2424
import { InputBond } from './bond.svelte';
2525
@@ -94,11 +94,11 @@
9494
}
9595
}
9696
}
97-
class={[
97+
class={cn(
9898
'h-full w-full flex-1 bg-transparent px-2 leading-1 outline-none',
9999
preset?.class,
100-
toClassValue(bond, klass)
101-
]}
100+
toClassValue(klass, bond)
101+
)}
102102
onchange={handleChange}
103103
oninput={handleInput}
104104
{...valueProps}

0 commit comments

Comments
 (0)