@mrintel/villain-ui 0.3.0 → 0.6.3

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 (128) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3490 -1296
  3. package/dist/components/buttons/Button.svelte +27 -33
  4. package/dist/components/buttons/Button.svelte.d.ts +4 -1
  5. package/dist/components/buttons/ButtonGroup.svelte +17 -30
  6. package/dist/components/buttons/FloatingActionButton.svelte +20 -44
  7. package/dist/components/buttons/FloatingActionButton.svelte.d.ts +2 -1
  8. package/dist/components/buttons/IconButton.svelte +23 -53
  9. package/dist/components/buttons/IconButton.svelte.d.ts +2 -1
  10. package/dist/components/buttons/LinkButton.svelte +24 -37
  11. package/dist/components/buttons/LinkButton.svelte.d.ts +4 -1
  12. package/dist/components/buttons/buttonClasses.d.ts +5 -0
  13. package/dist/components/buttons/buttonClasses.js +8 -3
  14. package/dist/components/cards/Card.svelte +60 -46
  15. package/dist/components/cards/Card.svelte.d.ts +6 -2
  16. package/dist/components/cards/Container.svelte +17 -33
  17. package/dist/components/cards/Divider.svelte +36 -52
  18. package/dist/components/cards/Divider.svelte.d.ts +2 -0
  19. package/dist/components/cards/Grid.svelte +55 -44
  20. package/dist/components/cards/Panel.svelte +18 -32
  21. package/dist/components/cards/Panel.svelte.d.ts +2 -1
  22. package/dist/components/cards/SectionHeader.svelte +24 -38
  23. package/dist/components/cards/SectionHeader.svelte.d.ts +1 -0
  24. package/dist/components/data/Avatar.svelte +48 -67
  25. package/dist/components/data/Badge.svelte +45 -32
  26. package/dist/components/data/Badge.svelte.d.ts +7 -1
  27. package/dist/components/data/CalendarGrid.svelte +433 -0
  28. package/dist/components/data/CalendarGrid.svelte.d.ts +25 -0
  29. package/dist/components/data/CalendarGrid.types.d.ts +7 -0
  30. package/dist/components/data/CalendarGrid.types.js +1 -0
  31. package/dist/components/data/CodeBlock.svelte +119 -121
  32. package/dist/components/data/CodeBlock.svelte.d.ts +8 -0
  33. package/dist/components/data/List.svelte +87 -64
  34. package/dist/components/data/List.svelte.d.ts +7 -0
  35. package/dist/components/data/Pagination.svelte +121 -123
  36. package/dist/components/data/Pagination.svelte.d.ts +5 -0
  37. package/dist/components/data/Sparkline.svelte +117 -0
  38. package/dist/components/data/Sparkline.svelte.d.ts +43 -0
  39. package/dist/components/data/Stat.svelte +92 -103
  40. package/dist/components/data/Table.svelte +443 -76
  41. package/dist/components/data/Table.svelte.d.ts +23 -2
  42. package/dist/components/data/Table.types.d.ts +14 -0
  43. package/dist/components/data/Table.types.js +1 -0
  44. package/dist/components/data/Tag.svelte +51 -53
  45. package/dist/components/data/Tag.svelte.d.ts +5 -1
  46. package/dist/components/data/index.d.ts +4 -0
  47. package/dist/components/data/index.js +2 -0
  48. package/dist/components/forms/Checkbox.svelte +39 -51
  49. package/dist/components/forms/Checkbox.svelte.d.ts +3 -1
  50. package/dist/components/forms/DatePicker.svelte +61 -0
  51. package/dist/components/forms/DatePicker.svelte.d.ts +15 -0
  52. package/dist/components/forms/DateTimePicker.svelte +63 -0
  53. package/dist/components/forms/DateTimePicker.svelte.d.ts +16 -0
  54. package/dist/components/forms/FileUpload.svelte +136 -164
  55. package/dist/components/forms/FileUpload.svelte.d.ts +1 -0
  56. package/dist/components/forms/Input.svelte +282 -57
  57. package/dist/components/forms/Input.svelte.d.ts +9 -3
  58. package/dist/components/forms/InputGroup.svelte +7 -7
  59. package/dist/components/forms/RadioGroup.svelte +77 -87
  60. package/dist/components/forms/RadioGroup.svelte.d.ts +3 -1
  61. package/dist/components/forms/RangeSlider.svelte +90 -116
  62. package/dist/components/forms/Select.svelte +106 -71
  63. package/dist/components/forms/Select.svelte.d.ts +3 -1
  64. package/dist/components/forms/Switch.svelte +44 -56
  65. package/dist/components/forms/Switch.svelte.d.ts +3 -1
  66. package/dist/components/forms/Textarea.svelte +52 -57
  67. package/dist/components/forms/Textarea.svelte.d.ts +3 -1
  68. package/dist/components/forms/TimePicker.svelte +63 -0
  69. package/dist/components/forms/TimePicker.svelte.d.ts +16 -0
  70. package/dist/components/forms/formClasses.d.ts +3 -0
  71. package/dist/components/forms/formClasses.js +3 -0
  72. package/dist/components/forms/index.d.ts +3 -0
  73. package/dist/components/forms/index.js +3 -0
  74. package/dist/components/navigation/Breadcrumbs.svelte +56 -59
  75. package/dist/components/navigation/Breadcrumbs.svelte.d.ts +1 -0
  76. package/dist/components/navigation/ContextMenu.svelte +133 -83
  77. package/dist/components/navigation/ContextMenu.svelte.d.ts +8 -1
  78. package/dist/components/navigation/DropdownMenu.svelte +139 -80
  79. package/dist/components/navigation/DropdownMenu.svelte.d.ts +8 -1
  80. package/dist/components/navigation/Menu.svelte +72 -48
  81. package/dist/components/navigation/Navbar.svelte +111 -32
  82. package/dist/components/navigation/Navbar.svelte.d.ts +6 -0
  83. package/dist/components/navigation/Sidebar.svelte +236 -35
  84. package/dist/components/navigation/Sidebar.svelte.d.ts +2 -0
  85. package/dist/components/navigation/Tabs.svelte +86 -54
  86. package/dist/components/navigation/Tabs.svelte.d.ts +5 -1
  87. package/dist/components/overlays/Alert.svelte +81 -99
  88. package/dist/components/overlays/Alert.svelte.d.ts +5 -1
  89. package/dist/components/overlays/CommandPalette.svelte +182 -217
  90. package/dist/components/overlays/Drawer.svelte +158 -167
  91. package/dist/components/overlays/Drawer.svelte.d.ts +3 -1
  92. package/dist/components/overlays/Dropdown.svelte +62 -30
  93. package/dist/components/overlays/Dropdown.svelte.d.ts +2 -0
  94. package/dist/components/overlays/Modal.svelte +125 -130
  95. package/dist/components/overlays/Modal.svelte.d.ts +3 -1
  96. package/dist/components/overlays/Popover.svelte +106 -131
  97. package/dist/components/overlays/ProgressBar.svelte +29 -45
  98. package/dist/components/overlays/SkeletonLoader.svelte +66 -82
  99. package/dist/components/overlays/Spinner.svelte +33 -43
  100. package/dist/components/overlays/Toast.svelte +111 -140
  101. package/dist/components/overlays/Toast.svelte.d.ts +3 -0
  102. package/dist/components/overlays/Tooltip.svelte +94 -115
  103. package/dist/components/overlays/Tooltip.svelte.d.ts +3 -1
  104. package/dist/components/typography/Code.svelte +10 -14
  105. package/dist/components/typography/Heading.svelte +15 -22
  106. package/dist/components/typography/Heading.svelte.d.ts +1 -0
  107. package/dist/components/typography/Text.svelte +21 -24
  108. package/dist/components/typography/Text.svelte.d.ts +2 -1
  109. package/dist/components/utilities/Accordion.svelte +54 -67
  110. package/dist/components/utilities/Accordion.svelte.d.ts +4 -1
  111. package/dist/components/utilities/Carousel.svelte +124 -152
  112. package/dist/components/utilities/Collapse.svelte +46 -60
  113. package/dist/components/utilities/Hero.svelte +42 -0
  114. package/dist/components/utilities/Hero.svelte.d.ts +10 -0
  115. package/dist/components/utilities/Portal.svelte +47 -72
  116. package/dist/components/utilities/ScrollArea.svelte +33 -41
  117. package/dist/components/utilities/SystemConsole.svelte +310 -0
  118. package/dist/components/utilities/SystemConsole.svelte.d.ts +20 -0
  119. package/dist/components/utilities/SystemInterface.svelte +726 -0
  120. package/dist/components/utilities/SystemInterface.svelte.d.ts +19 -0
  121. package/dist/components/utilities/index.d.ts +4 -0
  122. package/dist/components/utilities/index.js +3 -0
  123. package/dist/components/utilities/utilities.types.d.ts +46 -0
  124. package/dist/components/utilities/utilities.types.js +4 -0
  125. package/dist/index.d.ts +49 -4
  126. package/dist/index.js +4 -4
  127. package/dist/theme.css +2821 -218
  128. package/package.json +83 -76
@@ -1,152 +1,124 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
-
4
- interface CarouselItem {
5
- id: string;
6
- content: Snippet | string;
7
- }
8
-
9
- interface Props {
10
- items: CarouselItem[];
11
- currentIndex?: number;
12
- autoplay?: boolean;
13
- autoplayInterval?: number;
14
- showDots?: boolean;
15
- showArrows?: boolean;
16
- }
17
-
18
- let {
19
- items,
20
- currentIndex = $bindable(0),
21
- autoplay = false,
22
- autoplayInterval = 3000,
23
- showDots = true,
24
- showArrows = true
25
- }: Props = $props();
26
-
27
- let startX = $state(0);
28
- let currentX = $state(0);
29
- let isDragging = $state(false);
30
-
31
- function goToNext() {
32
- if (!items.length) return;
33
- currentIndex = (currentIndex + 1) % items.length;
34
- }
35
-
36
- function goToPrev() {
37
- if (!items.length) return;
38
- currentIndex = (currentIndex - 1 + items.length) % items.length;
39
- }
40
-
41
- function goToIndex(index: number) {
42
- if (items.length === 0 || index < 0 || index >= items.length) return;
43
- currentIndex = index;
44
- }
45
-
46
- function handleTouchStart(event: TouchEvent) {
47
- isDragging = true;
48
- startX = event.touches[0].clientX;
49
- currentX = startX;
50
- }
51
-
52
- function handleTouchMove(event: TouchEvent) {
53
- if (!isDragging) return;
54
- currentX = event.touches[0].clientX;
55
- }
56
-
57
- function handleTouchEnd() {
58
- if (!isDragging) return;
59
- if (!items.length) {
60
- isDragging = false;
61
- return;
62
- }
63
- isDragging = false;
64
-
65
- const diff = startX - currentX;
66
- const threshold = 50;
67
-
68
- if (diff > threshold) {
69
- goToNext();
70
- } else if (diff < -threshold) {
71
- goToPrev();
72
- }
73
-
74
- startX = 0;
75
- currentX = 0;
76
- }
77
-
78
- $effect(() => {
79
- if (!autoplay || items.length < 2) return;
80
-
81
- const interval = setInterval(goToNext, autoplayInterval);
82
-
83
- return () => {
84
- clearInterval(interval);
85
- };
86
- });
87
-
88
- // Clamp currentIndex when items change
89
- $effect(() => {
90
- if (items.length && (currentIndex < 0 || currentIndex >= items.length)) {
91
- currentIndex = 0;
92
- }
93
- });
94
- </script>
95
-
96
- <div class="glass-panel rounded-lg overflow-hidden relative">
97
- <div
98
- class="flex transition-transform duration-300 ease-luxe"
99
- style="transform: translateX(-{currentIndex * 100}%);"
100
- ontouchstart={handleTouchStart}
101
- ontouchmove={handleTouchMove}
102
- ontouchend={handleTouchEnd}
103
- >
104
- {#each items as item (item.id)}
105
- <div class="min-w-full flex items-center justify-center p-8">
106
- {#if typeof item.content === 'string'}
107
- <div class="text-text">{item.content}</div>
108
- {:else}
109
- {@render item.content()}
110
- {/if}
111
- </div>
112
- {/each}
113
- </div>
114
-
115
- {#if showArrows && items.length}
116
- <button
117
- type="button"
118
- onclick={goToPrev}
119
- class="absolute left-4 top-1/2 -translate-y-1/2 glass-panel rounded-full p-2 text-text hover:accent-glow transition-all"
120
- aria-label="Previous item"
121
- >
122
- <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
123
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
124
- </svg>
125
- </button>
126
-
127
- <button
128
- type="button"
129
- onclick={goToNext}
130
- class="absolute right-4 top-1/2 -translate-y-1/2 glass-panel rounded-full p-2 text-text hover:accent-glow transition-all"
131
- aria-label="Next item"
132
- >
133
- <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
134
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
135
- </svg>
136
- </button>
137
- {/if}
138
-
139
- {#if showDots && items.length}
140
- <div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
141
- {#each items as item, index (item.id)}
142
- <button
143
- type="button"
144
- onclick={() => goToIndex(index)}
145
- class="w-2 h-2 rounded-full transition-all {index === currentIndex ? 'accent-glow bg-accent w-8' : 'bg-text-muted'}"
146
- aria-label="Go to item {index + 1}"
147
- >
148
- </button>
149
- {/each}
150
- </div>
151
- {/if}
152
- </div>
1
+ <script lang="ts">let { items, currentIndex = $bindable(0), autoplay = false, autoplayInterval = 3000, showDots = true, showArrows = true } = $props();
2
+ let startX = $state(0);
3
+ let currentX = $state(0);
4
+ let isDragging = $state(false);
5
+ function goToNext() {
6
+ if (!items.length)
7
+ return;
8
+ currentIndex = (currentIndex + 1) % items.length;
9
+ }
10
+ function goToPrev() {
11
+ if (!items.length)
12
+ return;
13
+ currentIndex = (currentIndex - 1 + items.length) % items.length;
14
+ }
15
+ function goToIndex(index) {
16
+ if (items.length === 0 || index < 0 || index >= items.length)
17
+ return;
18
+ currentIndex = index;
19
+ }
20
+ function handleTouchStart(event) {
21
+ isDragging = true;
22
+ startX = event.touches[0].clientX;
23
+ currentX = startX;
24
+ }
25
+ function handleTouchMove(event) {
26
+ if (!isDragging)
27
+ return;
28
+ currentX = event.touches[0].clientX;
29
+ }
30
+ function handleTouchEnd() {
31
+ if (!isDragging)
32
+ return;
33
+ if (!items.length) {
34
+ isDragging = false;
35
+ return;
36
+ }
37
+ isDragging = false;
38
+ const diff = startX - currentX;
39
+ const threshold = 50;
40
+ if (diff > threshold) {
41
+ goToNext();
42
+ }
43
+ else if (diff < -threshold) {
44
+ goToPrev();
45
+ }
46
+ startX = 0;
47
+ currentX = 0;
48
+ }
49
+ $effect(() => {
50
+ if (!autoplay || items.length < 2)
51
+ return;
52
+ const interval = setInterval(goToNext, autoplayInterval);
53
+ return () => {
54
+ clearInterval(interval);
55
+ };
56
+ });
57
+ // Clamp currentIndex when items change
58
+ $effect(() => {
59
+ if (items.length && (currentIndex < 0 || currentIndex >= items.length)) {
60
+ currentIndex = 0;
61
+ }
62
+ });
63
+ export {};
64
+ </script>
65
+
66
+ <div class="relative">
67
+ <div class="panel-raised rounded-[var(--radius-lg)] overflow-hidden">
68
+ <div
69
+ class="flex transition-transform duration-300 ease-luxe"
70
+ style="transform: translateX(-{currentIndex * 100}%);"
71
+ ontouchstart={handleTouchStart}
72
+ ontouchmove={handleTouchMove}
73
+ ontouchend={handleTouchEnd}
74
+ >
75
+ {#each items as item (item.id)}
76
+ <div class="min-w-full flex items-center justify-center p-8">
77
+ {#if typeof item.content === 'string'}
78
+ <div class="text-text">{item.content}</div>
79
+ {:else}
80
+ {@render item.content()}
81
+ {/if}
82
+ </div>
83
+ {/each}
84
+ </div>
85
+ </div>
86
+
87
+ {#if showArrows && items.length}
88
+ <button
89
+ type="button"
90
+ onclick={goToPrev}
91
+ class="absolute left-2 top-1/2 -translate-y-1/2 obsidian-surface metal-edge rounded-full p-2 text-text hover:accent-glow transition-all"
92
+ aria-label="Previous item"
93
+ >
94
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
95
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
96
+ </svg>
97
+ </button>
98
+
99
+ <button
100
+ type="button"
101
+ onclick={goToNext}
102
+ class="absolute right-2 top-1/2 -translate-y-1/2 obsidian-surface metal-edge rounded-full p-2 text-text hover:accent-glow transition-all"
103
+ aria-label="Next item"
104
+ >
105
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
106
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
107
+ </svg>
108
+ </button>
109
+ {/if}
110
+
111
+ {#if showDots && items.length}
112
+ <div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
113
+ {#each items as item, index (item.id)}
114
+ <button
115
+ type="button"
116
+ onclick={() => goToIndex(index)}
117
+ class="w-2 h-2 rounded-full transition-all {index === currentIndex ? 'accent-glow bg-accent w-8' : 'bg-text-muted'}"
118
+ aria-label="Go to item {index + 1}"
119
+ >
120
+ </button>
121
+ {/each}
122
+ </div>
123
+ {/if}
124
+ </div>
@@ -1,60 +1,46 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { createId } from '../../lib/internal/id.js';
4
-
5
- interface Props {
6
- open: boolean;
7
- title: string;
8
- children?: Snippet;
9
- onToggle?: () => void;
10
- }
11
-
12
- let { open, title, children, onToggle }: Props = $props();
13
-
14
- let contentElement = $state<HTMLDivElement>();
15
- let openLocal = $state(open);
16
-
17
- const headerId = createId('collapse-header');
18
- const contentId = createId('collapse-content');
19
-
20
- function toggleLocal() {
21
- openLocal = !openLocal;
22
- }
23
-
24
- let effectiveOpen = $derived(onToggle ? open : openLocal);
25
- let maxHeight = $derived(effectiveOpen && contentElement ? `${contentElement.scrollHeight}px` : '0px');
26
- </script>
27
-
28
- <div class="border border-border rounded-md overflow-hidden {effectiveOpen ? 'glass-panel' : ''}">
29
- <button
30
- id={headerId}
31
- type="button"
32
- onclick={onToggle ?? toggleLocal}
33
- class="w-full flex items-center justify-between p-4 text-left text-text font-medium hover:bg-base-3 transition-colors"
34
- aria-expanded={effectiveOpen}
35
- aria-controls={contentId}
36
- >
37
- <span>{title}</span>
38
- <svg
39
- class="w-5 h-5 transition-transform duration-300 ease-luxe {effectiveOpen ? 'rotate-180' : ''}"
40
- fill="none"
41
- stroke="currentColor"
42
- viewBox="0 0 24 24"
43
- >
44
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
45
- </svg>
46
- </button>
47
-
48
- <div
49
- bind:this={contentElement}
50
- id={contentId}
51
- class="overflow-hidden transition-[max-height] duration-300 ease-luxe"
52
- style="max-height: {maxHeight};"
53
- role="region"
54
- aria-labelledby={headerId}
55
- >
56
- <div class="p-4 text-text-soft border-t border-border">
57
- {@render children?.()}
58
- </div>
59
- </div>
60
- </div>
1
+ <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
+ let { open, title, children, onToggle } = $props();
3
+ let contentElement = $state();
4
+ let openLocal = $state(open);
5
+ const headerId = createId('collapse-header');
6
+ const contentId = createId('collapse-content');
7
+ function toggleLocal() {
8
+ openLocal = !openLocal;
9
+ }
10
+ let effectiveOpen = $derived(onToggle ? open : openLocal);
11
+ let maxHeight = $derived(effectiveOpen && contentElement ? `${contentElement.scrollHeight}px` : '0px');
12
+ </script>
13
+
14
+ <div class="border border-border rounded-md overflow-hidden {effectiveOpen ? 'panel-raised' : ''}">
15
+ <button
16
+ id={headerId}
17
+ type="button"
18
+ onclick={onToggle ?? toggleLocal}
19
+ class="w-full flex items-center justify-between p-4 text-left text-text font-medium hover:bg-base-3 transition-colors"
20
+ aria-expanded={effectiveOpen}
21
+ aria-controls={contentId}
22
+ >
23
+ <span>{title}</span>
24
+ <svg
25
+ class="w-5 h-5 transition-transform duration-300 ease-luxe {effectiveOpen ? 'rotate-180' : ''}"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ viewBox="0 0 24 24"
29
+ >
30
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
31
+ </svg>
32
+ </button>
33
+
34
+ <div
35
+ bind:this={contentElement}
36
+ id={contentId}
37
+ class="overflow-hidden transition-[max-height] duration-300 ease-luxe"
38
+ style="max-height: {maxHeight};"
39
+ role="region"
40
+ aria-labelledby={headerId}
41
+ >
42
+ <div class="p-4 text-text-soft border-t border-border">
43
+ {@render children?.()}
44
+ </div>
45
+ </div>
46
+ </div>
@@ -0,0 +1,42 @@
1
+ <script lang="ts">"use strict";
2
+ let { title, text, features, children, class: className = '', ...restProps } = $props();
3
+ </script>
4
+
5
+ <div class="hero-section {className}" {...restProps}>
6
+ {#if title}
7
+ <div class="mb-4">
8
+ {@render title()}
9
+ </div>
10
+ {/if}
11
+
12
+ {#if text}
13
+ <div class="mb-8 mx-auto max-w-3xl">
14
+ {@render text()}
15
+ </div>
16
+ {/if}
17
+
18
+ {#if features}
19
+ <div class="feature-tags">
20
+ {@render features()}
21
+ </div>
22
+ {/if}
23
+
24
+ {#if children}
25
+ {@render children()}
26
+ {/if}
27
+ </div>
28
+
29
+ <style>
30
+ .feature-tags {
31
+ display: flex;
32
+ gap: 1rem;
33
+ justify-content: center;
34
+ flex-wrap: wrap;
35
+ margin-top: 2rem;
36
+ }
37
+
38
+ .hero-section {
39
+ text-align: center;
40
+ padding-bottom: 3rem;
41
+ margin-bottom: 3rem;
42
+ }</style>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ title?: import('svelte').Snippet;
3
+ text?: import('svelte').Snippet;
4
+ features?: import('svelte').Snippet;
5
+ children?: import('svelte').Snippet;
6
+ class?: string;
7
+ }
8
+ declare const Hero: import("svelte").Component<Props, {}, "">;
9
+ type Hero = ReturnType<typeof Hero>;
10
+ export default Hero;
@@ -1,72 +1,47 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
-
4
- /**
5
- * Portal component - teleports content to a different DOM location.
6
- *
7
- * **Focus Management**: This component handles DOM manipulation only.
8
- * For focus-sensitive content (modals, drawers, toasts), the parent component
9
- * should manage focus using the patterns from Modal.svelte and Drawer.svelte:
10
- * - Store previousFocus before opening
11
- * - Focus appropriate element after mount
12
- * - Restore focus on cleanup
13
- *
14
- * **Stacking Coordination**: Multiple portals can coexist. For z-index stacking,
15
- * manage via CSS classes on the portal children or implement a portal manager.
16
- */
17
- interface Props {
18
- target?: HTMLElement | string;
19
- children?: Snippet;
20
- }
21
-
22
- let { target = 'body', children }: Props = $props();
23
-
24
- let mountElement = $state<HTMLElement | null>(null);
25
-
26
- function portal(node: HTMLElement, currentTarget: HTMLElement) {
27
- currentTarget.appendChild(node);
28
- return {
29
- update(newTarget: HTMLElement) {
30
- // Handle target prop changes by moving node to new parent
31
- if (newTarget !== currentTarget) {
32
- newTarget.appendChild(node);
33
- }
34
- },
35
- destroy() {
36
- if (node.parentNode) {
37
- node.parentNode.removeChild(node);
38
- }
39
- }
40
- };
41
- }
42
-
43
- $effect(() => {
44
- if (typeof document === 'undefined') return;
45
-
46
- // Resolve target element
47
- const targetElement = typeof target === 'string'
48
- ? document.querySelector<HTMLElement>(target)
49
- : target;
50
-
51
- if (!targetElement) return;
52
-
53
- // Create container div
54
- const container = document.createElement('div');
55
- targetElement.appendChild(container);
56
- mountElement = container;
57
-
58
- return () => {
59
- // Cleanup: remove container from DOM
60
- container.remove();
61
- mountElement = null;
62
- };
63
- });
64
- </script>
65
-
66
- {#if mountElement}
67
- {#if children}
68
- <div use:portal={mountElement}>
69
- {@render children()}
70
- </div>
71
- {/if}
72
- {/if}
1
+ <script lang="ts">let { target = 'body', children } = $props();
2
+ let mountElement = $state(null);
3
+ function portal(node, currentTarget) {
4
+ currentTarget.appendChild(node);
5
+ return {
6
+ update(newTarget) {
7
+ // Handle target prop changes by moving node to new parent
8
+ if (newTarget !== currentTarget) {
9
+ newTarget.appendChild(node);
10
+ }
11
+ },
12
+ destroy() {
13
+ if (node.parentNode) {
14
+ node.parentNode.removeChild(node);
15
+ }
16
+ }
17
+ };
18
+ }
19
+ $effect(() => {
20
+ if (typeof document === 'undefined')
21
+ return;
22
+ // Resolve target element
23
+ const targetElement = typeof target === 'string'
24
+ ? document.querySelector(target)
25
+ : target;
26
+ if (!targetElement)
27
+ return;
28
+ // Create container div
29
+ const container = document.createElement('div');
30
+ targetElement.appendChild(container);
31
+ mountElement = container;
32
+ return () => {
33
+ // Cleanup: remove container from DOM
34
+ container.remove();
35
+ mountElement = null;
36
+ };
37
+ });
38
+ export {};
39
+ </script>
40
+
41
+ {#if mountElement}
42
+ {#if children}
43
+ <div use:portal={mountElement}>
44
+ {@render children()}
45
+ </div>
46
+ {/if}
47
+ {/if}
@@ -1,41 +1,33 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
-
4
- interface Props {
5
- height?: string;
6
- children?: Snippet;
7
- }
8
-
9
- let { height = '400px', children }: Props = $props();
10
- </script>
11
-
12
- <div
13
- class="overflow-y-auto"
14
- style="
15
- height: {height};
16
- scrollbar-width: thin;
17
- scrollbar-color: var(--color-accent) var(--color-base-3);
18
- "
19
- >
20
- {@render children?.()}
21
- </div>
22
-
23
- <style>
24
- div::-webkit-scrollbar {
25
- width: 8px;
26
- }
27
-
28
- div::-webkit-scrollbar-track {
29
- background: var(--color-base-3);
30
- border-radius: var(--radius-sm);
31
- }
32
-
33
- div::-webkit-scrollbar-thumb {
34
- background: var(--color-accent);
35
- border-radius: var(--radius-sm);
36
- }
37
-
38
- div::-webkit-scrollbar-thumb:hover {
39
- background: var(--color-accent-soft);
40
- }
41
- </style>
1
+ <script lang="ts">let { height = '400px', children } = $props();
2
+ export {};
3
+ </script>
4
+
5
+ <div
6
+ class="overflow-y-auto"
7
+ style="
8
+ height: {height};
9
+ scrollbar-width: thin;
10
+ scrollbar-color: var(--color-accent) var(--color-base-3);
11
+ "
12
+ >
13
+ {@render children?.()}
14
+ </div>
15
+
16
+ <style>
17
+ div::-webkit-scrollbar {
18
+ width: 8px;
19
+ }
20
+
21
+ div::-webkit-scrollbar-track {
22
+ background: var(--color-base-3);
23
+ border-radius: var(--radius-sm);
24
+ }
25
+
26
+ div::-webkit-scrollbar-thumb {
27
+ background: var(--color-accent);
28
+ border-radius: var(--radius-sm);
29
+ }
30
+
31
+ div::-webkit-scrollbar-thumb:hover {
32
+ background: var(--color-accent-soft);
33
+ }</style>