@smilodon/svelte 1.0.0 → 1.1.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.
Files changed (2) hide show
  1. package/README.md +570 -0
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  Production-ready, accessible select component for Svelte applications. Part of the [Smilodon](https://github.com/navidrezadoost/smilodon) UI toolkit.
4
4
 
5
+ ## 📖 Documentation
6
+
7
+ **For comprehensive documentation covering all features, styling options, and advanced patterns:**
8
+
9
+ 👉 **[Complete Svelte Guide](./COMPLETE-GUIDE.md)** 👈
10
+
11
+ The complete guide includes:
12
+ - ✅ All 60+ CSS variables for complete customization
13
+ - ✅ Svelte-specific patterns (reactive statements, stores, bind:value)
14
+ - ✅ Complete API reference with TypeScript types
15
+ - ✅ Svelte stores integration (writable, derived)
16
+ - ✅ Custom renderers returning HTML strings
17
+ - ✅ Theme examples with :global() styling
18
+ - ✅ Advanced patterns (Context API, reactive dependencies)
19
+ - ✅ Troubleshooting and accessibility information
20
+
21
+ ---
22
+
5
23
  ## Features
6
24
 
7
25
  - ✨ **Single & Multi-Select** - Choose one or multiple options
@@ -320,6 +338,558 @@ interface GroupedItem {
320
338
  }
321
339
  ```
322
340
 
341
+ ---
342
+
343
+ ## 🎯 Two Ways to Specify Options
344
+
345
+ Smilodon Svelte provides **two powerful approaches** for defining select options, each optimized for different use cases:
346
+
347
+ ### Method 1: Data-Driven (Object Arrays) 📊
348
+
349
+ **Use when**: You have structured data and want simple, declarative option rendering.
350
+
351
+ **Advantages**:
352
+ - ✅ Simple and declarative - Svelte-friendly
353
+ - ✅ Auto-conversion from strings/numbers
354
+ - ✅ Perfect for basic dropdowns
355
+ - ✅ Works seamlessly with Svelte stores
356
+ - ✅ Extremely performant (millions of items)
357
+ - ✅ Built-in search and filtering
358
+ - ✅ Full TypeScript type safety
359
+
360
+ **Examples**:
361
+
362
+ ```svelte
363
+ <script lang="ts">
364
+ import { Select } from '@smilodon/svelte';
365
+
366
+ // Example 1: Simple object array
367
+ let value = '';
368
+
369
+ const items = [
370
+ { value: '1', label: 'Apple' },
371
+ { value: '2', label: 'Banana' },
372
+ { value: '3', label: 'Cherry' }
373
+ ];
374
+ </script>
375
+
376
+ <Select
377
+ items={items}
378
+ bind:value
379
+ placeholder="Select a fruit..."
380
+ />
381
+ ```
382
+
383
+ ```svelte
384
+ <script lang="ts">
385
+ import { Select } from '@smilodon/svelte';
386
+
387
+ // Example 2: With metadata and disabled options
388
+ let country = '';
389
+
390
+ const countries = [
391
+ { value: 'us', label: 'United States', disabled: false },
392
+ { value: 'ca', label: 'Canada', disabled: false },
393
+ { value: 'mx', label: 'Mexico', disabled: true }
394
+ ];
395
+ </script>
396
+
397
+ <Select
398
+ items={countries}
399
+ bind:value={country}
400
+ placeholder="Select a country..."
401
+ />
402
+ ```
403
+
404
+ ```svelte
405
+ <script lang="ts">
406
+ import { Select } from '@smilodon/svelte';
407
+
408
+ // Example 3: With grouping
409
+ let food = '';
410
+
411
+ const foods = [
412
+ { value: 'apple', label: 'Apple', group: 'Fruits' },
413
+ { value: 'banana', label: 'Banana', group: 'Fruits' },
414
+ { value: 'carrot', label: 'Carrot', group: 'Vegetables' },
415
+ { value: 'broccoli', label: 'Broccoli', group: 'Vegetables' }
416
+ ];
417
+ </script>
418
+
419
+ <Select
420
+ items={foods}
421
+ bind:value={food}
422
+ placeholder="Select food..."
423
+ />
424
+ ```
425
+
426
+ ```svelte
427
+ <script lang="ts">
428
+ import { Select } from '@smilodon/svelte';
429
+
430
+ // Example 4: Auto-conversion from strings
431
+ let color = '';
432
+ const colors = ['Red', 'Green', 'Blue', 'Yellow'];
433
+ </script>
434
+
435
+ <Select
436
+ items={colors}
437
+ bind:value={color}
438
+ placeholder="Select a color..."
439
+ />
440
+ ```
441
+
442
+ ```svelte
443
+ <script lang="ts">
444
+ import { Select } from '@smilodon/svelte';
445
+
446
+ // Example 5: Auto-conversion from numbers
447
+ let size: number | string = '';
448
+ const sizes = [10, 20, 30, 40, 50];
449
+ </script>
450
+
451
+ <Select
452
+ items={sizes}
453
+ bind:value={size}
454
+ placeholder="Select size..."
455
+ />
456
+ ```
457
+
458
+ ```svelte
459
+ <script lang="ts">
460
+ import { Select } from '@smilodon/svelte';
461
+
462
+ // Example 6: Large datasets with reactive statement
463
+ let id = '';
464
+
465
+ $: items = Array.from({ length: 100_000 }, (_, i) => ({
466
+ value: i.toString(),
467
+ label: `Item ${i + 1}`
468
+ }));
469
+ </script>
470
+
471
+ <Select
472
+ items={items}
473
+ bind:value={id}
474
+ virtualized
475
+ placeholder="Select from 100K items..."
476
+ />
477
+ ```
478
+
479
+ ### Method 2: Component-Driven (Custom Renderers) 🎨
480
+
481
+ **Use when**: You need rich, interactive option content with custom HTML/styling.
482
+
483
+ **Advantages**:
484
+ - ✅ Full control over option rendering
485
+ - ✅ Rich content (images, icons, badges, multi-line text)
486
+ - ✅ Custom HTML and styling
487
+ - ✅ Reactive data binding
488
+ - ✅ Conditional rendering based on item data
489
+ - ✅ Works with Svelte stores
490
+ - ✅ Perfect for complex UIs (user cards, product listings, etc.)
491
+
492
+ **How it works**: Provide an `optionTemplate` function that returns HTML string for each option.
493
+
494
+ **Examples**:
495
+
496
+ ```svelte
497
+ <script lang="ts">
498
+ import { Select, type SelectItem } from '@smilodon/svelte';
499
+
500
+ // Example 1: Simple custom template with icons
501
+ interface Language extends SelectItem {
502
+ icon: string;
503
+ description: string;
504
+ }
505
+
506
+ let lang = '';
507
+
508
+ const languages: Language[] = [
509
+ { value: 'js', label: 'JavaScript', icon: '🟨', description: 'Dynamic scripting language' },
510
+ { value: 'py', label: 'Python', icon: '🐍', description: 'General-purpose programming' },
511
+ { value: 'rs', label: 'Rust', icon: '🦀', description: 'Systems programming language' }
512
+ ];
513
+
514
+ const languageRenderer = (item: Language, index: number) => `
515
+ <div style="display: flex; align-items: center; gap: 12px;">
516
+ <span style="font-size: 24px;">${item.icon}</span>
517
+ <div>
518
+ <div style="font-weight: 600;">${item.label}</div>
519
+ <div style="font-size: 12px; color: #6b7280;">${item.description}</div>
520
+ </div>
521
+ </div>
522
+ `;
523
+ </script>
524
+
525
+ <Select
526
+ items={languages}
527
+ bind:value={lang}
528
+ optionTemplate={languageRenderer}
529
+ placeholder="Select a language..."
530
+ />
531
+ ```
532
+
533
+ ```svelte
534
+ <script lang="ts">
535
+ import { Select, type SelectItem } from '@smilodon/svelte';
536
+
537
+ // Example 2: User selection with avatars
538
+ interface User extends SelectItem {
539
+ email: string;
540
+ avatar: string;
541
+ role: 'Admin' | 'User' | 'Moderator';
542
+ }
543
+
544
+ let userId = '';
545
+
546
+ const users: User[] = [
547
+ {
548
+ value: '1',
549
+ label: 'John Doe',
550
+ email: 'john@example.com',
551
+ avatar: 'https://i.pravatar.cc/150?img=1',
552
+ role: 'Admin'
553
+ },
554
+ {
555
+ value: '2',
556
+ label: 'Jane Smith',
557
+ email: 'jane@example.com',
558
+ avatar: 'https://i.pravatar.cc/150?img=2',
559
+ role: 'User'
560
+ },
561
+ {
562
+ value: '3',
563
+ label: 'Bob Johnson',
564
+ email: 'bob@example.com',
565
+ avatar: 'https://i.pravatar.cc/150?img=3',
566
+ role: 'Moderator'
567
+ }
568
+ ];
569
+
570
+ const userRenderer = (item: User) => `
571
+ <div style="display: flex; align-items: center; gap: 12px; padding: 4px 0;">
572
+ <img
573
+ src="${item.avatar}"
574
+ alt="${item.label}"
575
+ style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;"
576
+ />
577
+ <div style="flex: 1;">
578
+ <div style="font-weight: 600; color: #1f2937;">${item.label}</div>
579
+ <div style="font-size: 13px; color: #6b7280;">${item.email}</div>
580
+ </div>
581
+ <span style="
582
+ padding: 4px 8px;
583
+ background: ${item.role === 'Admin' ? '#dbeafe' : '#f3f4f6'};
584
+ color: ${item.role === 'Admin' ? '#1e40af' : '#374151'};
585
+ border-radius: 12px;
586
+ font-size: 11px;
587
+ font-weight: 600;
588
+ ">${item.role}</span>
589
+ </div>
590
+ `;
591
+ </script>
592
+
593
+ <Select
594
+ items={users}
595
+ bind:value={userId}
596
+ optionTemplate={userRenderer}
597
+ placeholder="Select a user..."
598
+ />
599
+ ```
600
+
601
+ ```svelte
602
+ <script lang="ts">
603
+ import { Select, type SelectItem } from '@smilodon/svelte';
604
+
605
+ // Example 3: Product selection with images and pricing
606
+ interface Product extends SelectItem {
607
+ price: number;
608
+ stock: number;
609
+ image: string;
610
+ badge?: string;
611
+ }
612
+
613
+ let productId = '';
614
+
615
+ const products: Product[] = [
616
+ {
617
+ value: 'p1',
618
+ label: 'Premium Laptop',
619
+ price: 1299.99,
620
+ stock: 15,
621
+ image: 'https://via.placeholder.com/60',
622
+ badge: 'Best Seller'
623
+ },
624
+ {
625
+ value: 'p2',
626
+ label: 'Wireless Mouse',
627
+ price: 29.99,
628
+ stock: 150,
629
+ image: 'https://via.placeholder.com/60'
630
+ },
631
+ {
632
+ value: 'p3',
633
+ label: 'Mechanical Keyboard',
634
+ price: 89.99,
635
+ stock: 0,
636
+ image: 'https://via.placeholder.com/60',
637
+ badge: 'Out of Stock'
638
+ }
639
+ ];
640
+
641
+ const productRenderer = (item: Product) => `
642
+ <div style="display: flex; align-items: center; gap: 12px; opacity: ${item.stock === 0 ? '0.5' : '1'};">
643
+ <img
644
+ src="${item.image}"
645
+ alt="${item.label}"
646
+ style="width: 60px; height: 60px; border-radius: 8px; object-fit: cover; border: 1px solid #e5e7eb;"
647
+ />
648
+ <div style="flex: 1;">
649
+ <div style="display: flex; align-items: center; gap: 8px;">
650
+ <span style="font-weight: 600; color: #1f2937;">${item.label}</span>
651
+ ${item.badge ? `
652
+ <span style="
653
+ padding: 2px 6px;
654
+ background: ${item.badge === 'Best Seller' ? '#dcfce7' : '#fee2e2'};
655
+ color: ${item.badge === 'Best Seller' ? '#166534' : '#991b1b'};
656
+ border-radius: 4px;
657
+ font-size: 10px;
658
+ font-weight: 600;
659
+ ">${item.badge}</span>
660
+ ` : ''}
661
+ </div>
662
+ <div style="margin-top: 4px; display: flex; justify-content: space-between; align-items: center;">
663
+ <span style="font-size: 16px; font-weight: 700; color: #059669;">$${item.price.toFixed(2)}</span>
664
+ <span style="font-size: 12px; color: #6b7280;">${item.stock > 0 ? `${item.stock} in stock` : 'Out of stock'}</span>
665
+ </div>
666
+ </div>
667
+ </div>
668
+ `;
669
+ </script>
670
+
671
+ <Select
672
+ items={products}
673
+ bind:value={productId}
674
+ optionTemplate={productRenderer}
675
+ placeholder="Select a product..."
676
+ />
677
+ ```
678
+
679
+ ```svelte
680
+ <script lang="ts">
681
+ import { Select, type SelectItem } from '@smilodon/svelte';
682
+
683
+ // Example 4: Status indicators with conditional styling
684
+ interface Task extends SelectItem {
685
+ status: 'completed' | 'in-progress' | 'pending';
686
+ priority: 'high' | 'medium' | 'low';
687
+ assignee: string;
688
+ }
689
+
690
+ let taskId = '';
691
+
692
+ const tasks: Task[] = [
693
+ { value: 't1', label: 'Design Homepage', status: 'completed', priority: 'high', assignee: 'John' },
694
+ { value: 't2', label: 'API Integration', status: 'in-progress', priority: 'high', assignee: 'Jane' },
695
+ { value: 't3', label: 'Write Documentation', status: 'pending', priority: 'medium', assignee: 'Bob' },
696
+ { value: 't4', label: 'Bug Fixes', status: 'in-progress', priority: 'low', assignee: 'Alice' }
697
+ ];
698
+
699
+ const statusConfig = {
700
+ 'completed': { bg: '#dcfce7', color: '#166534', icon: '✓' },
701
+ 'in-progress': { bg: '#dbeafe', color: '#1e40af', icon: '⟳' },
702
+ 'pending': { bg: '#fef3c7', color: '#92400e', icon: '○' }
703
+ };
704
+
705
+ const priorityColors = {
706
+ 'high': '#ef4444',
707
+ 'medium': '#f59e0b',
708
+ 'low': '#10b981'
709
+ };
710
+
711
+ const taskRenderer = (item: Task) => {
712
+ const status = statusConfig[item.status];
713
+ return `
714
+ <div style="display: flex; align-items: center; gap: 10px; padding: 4px 0;">
715
+ <div style="
716
+ width: 24px;
717
+ height: 24px;
718
+ border-radius: 50%;
719
+ background: ${status.bg};
720
+ color: ${status.color};
721
+ display: flex;
722
+ align-items: center;
723
+ justify-content: center;
724
+ font-weight: bold;
725
+ ">${status.icon}</div>
726
+ <div style="flex: 1;">
727
+ <div style="font-weight: 600; color: #1f2937;">${item.label}</div>
728
+ <div style="font-size: 12px; color: #6b7280; margin-top: 2px;">
729
+ Assigned to ${item.assignee}
730
+ </div>
731
+ </div>
732
+ <div style="
733
+ width: 8px;
734
+ height: 8px;
735
+ border-radius: 50%;
736
+ background: ${priorityColors[item.priority]};
737
+ " title="${item.priority} priority"></div>
738
+ </div>
739
+ `;
740
+ };
741
+ </script>
742
+
743
+ <Select
744
+ items={tasks}
745
+ bind:value={taskId}
746
+ optionTemplate={taskRenderer}
747
+ placeholder="Select a task..."
748
+ />
749
+ ```
750
+
751
+ ```svelte
752
+ <script lang="ts">
753
+ import { Select, type SelectItem } from '@smilodon/svelte';
754
+ import { writable } from 'svelte/store';
755
+
756
+ // Example 5: Using Svelte stores
757
+ interface Tag extends SelectItem {
758
+ color: string;
759
+ count: number;
760
+ }
761
+
762
+ const tag = writable('');
763
+
764
+ const tags: Tag[] = [
765
+ { value: 'react', label: 'React', color: 'blue', count: 1250 },
766
+ { value: 'vue', label: 'Vue', color: 'green', count: 850 },
767
+ { value: 'angular', label: 'Angular', color: 'red', count: 420 }
768
+ ];
769
+
770
+ const tagRenderer = (item: Tag) => `
771
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 8px;">
772
+ <div style="display: flex; align-items: center; gap: 8px;">
773
+ <span style="width: 12px; height: 12px; border-radius: 50%; background: ${item.color};"></span>
774
+ <span style="font-weight: 600; color: #1f2937;">${item.label}</span>
775
+ </div>
776
+ <span style="font-size: 14px; color: #6b7280;">${item.count} posts</span>
777
+ </div>
778
+ `;
779
+ </script>
780
+
781
+ <Select
782
+ items={tags}
783
+ bind:value={$tag}
784
+ optionTemplate={tagRenderer}
785
+ placeholder="Select a tag..."
786
+ />
787
+ ```
788
+
789
+ ### Comparison: When to Use Each Method
790
+
791
+ | Feature | Method 1: Object Arrays | Method 2: Custom Renderers |
792
+ |---------|------------------------|---------------------------|
793
+ | **Setup Complexity** | ⭐ Simple | ⭐⭐ Moderate |
794
+ | **Rendering Speed** | ⭐⭐⭐ Fastest | ⭐⭐ Fast |
795
+ | **Visual Customization** | ⭐⭐ Limited | ⭐⭐⭐ Unlimited |
796
+ | **Svelte Integration** | ⭐⭐⭐ Seamless | ⭐⭐⭐ Seamless |
797
+ | **Store Support** | ⭐⭐⭐ Full | ⭐⭐⭐ Full |
798
+ | **TypeScript Support** | ⭐⭐⭐ Full | ⭐⭐⭐ Full |
799
+ | **Performance (1M items)** | ⭐⭐⭐ Excellent | ⭐⭐ Good |
800
+ | **Learning Curve** | ⭐ Easy | ⭐⭐ Medium |
801
+
802
+ **Best Practices**:
803
+
804
+ ✅ **Use Method 1 (Object Arrays) when**:
805
+ - You need simple text-based options
806
+ - Performance is critical (millions of items)
807
+ - You want minimal code
808
+ - Built-in search/filter is sufficient
809
+ - Working with external APIs returning plain data
810
+
811
+ ✅ **Use Method 2 (Custom Renderers) when**:
812
+ - You need images, icons, or badges
813
+ - Options require multiple lines of text
814
+ - Custom styling/layout is important
815
+ - Conditional rendering based on data
816
+ - Rich user experience is priority
817
+ - Need to integrate with Svelte stores in rendering
818
+
819
+ ### Combining Both Methods
820
+
821
+ You can start with Method 1 and add Method 2 later as your UI evolves:
822
+
823
+ ```svelte
824
+ <script lang="ts">
825
+ import { Select } from '@smilodon/svelte';
826
+
827
+ let value = '';
828
+
829
+ // Start simple
830
+ const items = ['Option 1', 'Option 2', 'Option 3'];
831
+
832
+ // Later, add custom rendering without changing items
833
+ const customRenderer = (item: any, index: number) => `
834
+ <div style="padding: 8px; background: ${index % 2 ? '#f9fafb' : 'white'};">
835
+ <strong>${item.label || item}</strong>
836
+ </div>
837
+ `;
838
+ </script>
839
+
840
+ <Select
841
+ items={items}
842
+ bind:value
843
+ optionTemplate={customRenderer}
844
+ />
845
+ ```
846
+
847
+ ### Performance Tips
848
+
849
+ **For Method 1**:
850
+ - Use reactive statements (`$:`) to memoize large item arrays
851
+ - Enable `virtualized` prop for 1000+ items
852
+ - Enable `infiniteScroll` for dynamic loading
853
+
854
+ **For Method 2**:
855
+ - Keep renderer function pure (no side effects)
856
+ - Avoid heavy computations in renderer
857
+ - Cache renderer functions when possible
858
+ - Use template literals for cleaner HTML strings
859
+
860
+ ```svelte
861
+ <script lang="ts">
862
+ import { Select } from '@smilodon/svelte';
863
+
864
+ let value = '';
865
+
866
+ // Memoize items with reactive statement
867
+ $: items = Array.from({ length: 10000 }, (_, i) => ({
868
+ value: i.toString(),
869
+ label: `Item ${i + 1}`,
870
+ description: `Description for item ${i + 1}`
871
+ }));
872
+
873
+ // Pure renderer function
874
+ const renderer = (item: any, index: number) => `
875
+ <div>
876
+ <div style="font-weight: 600;">${item.label}</div>
877
+ <div style="font-size: 12px; color: #666;">${item.description}</div>
878
+ </div>
879
+ `;
880
+ </script>
881
+
882
+ <Select
883
+ items={items}
884
+ bind:value
885
+ optionTemplate={renderer}
886
+ virtualized
887
+ estimatedItemHeight={60}
888
+ />
889
+ ```
890
+
891
+ ---
892
+
323
893
  ## Styling
324
894
 
325
895
  The component uses CSS variables for easy customization:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smilodon/svelte",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Production-ready, accessible select component for Svelte - part of the Smilodon UI toolkit",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.mjs",