@miozu/jera 0.0.2 → 0.4.2

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 (82) hide show
  1. package/CLAUDE.md +734 -0
  2. package/README.md +219 -1
  3. package/llms.txt +97 -0
  4. package/package.json +54 -14
  5. package/src/actions/index.js +375 -0
  6. package/src/components/docs/CodeBlock.svelte +203 -0
  7. package/src/components/docs/DocSection.svelte +120 -0
  8. package/src/components/docs/PropsTable.svelte +136 -0
  9. package/src/components/docs/SplitPane.svelte +98 -0
  10. package/src/components/docs/index.js +14 -0
  11. package/src/components/feedback/Alert.svelte +234 -0
  12. package/src/components/feedback/EmptyState.svelte +179 -0
  13. package/src/components/feedback/ProgressBar.svelte +116 -0
  14. package/src/components/feedback/Skeleton.svelte +107 -0
  15. package/src/components/feedback/Spinner.svelte +77 -0
  16. package/src/components/feedback/Toast.svelte +261 -0
  17. package/src/components/forms/Checkbox.svelte +147 -0
  18. package/src/components/forms/Dropzone.svelte +248 -0
  19. package/src/components/forms/FileUpload.svelte +266 -0
  20. package/src/components/forms/IconInput.svelte +184 -0
  21. package/src/components/forms/Input.svelte +121 -0
  22. package/src/components/forms/NumberInput.svelte +225 -0
  23. package/src/components/forms/PinInput.svelte +169 -0
  24. package/src/components/forms/Radio.svelte +143 -0
  25. package/src/components/forms/RadioGroup.svelte +62 -0
  26. package/src/components/forms/RangeSlider.svelte +212 -0
  27. package/src/components/forms/SearchInput.svelte +175 -0
  28. package/src/components/forms/Select.svelte +324 -0
  29. package/src/components/forms/Switch.svelte +159 -0
  30. package/src/components/forms/Textarea.svelte +122 -0
  31. package/src/components/navigation/Accordion.svelte +65 -0
  32. package/src/components/navigation/AccordionItem.svelte +146 -0
  33. package/src/components/navigation/NavigationContainer.svelte +344 -0
  34. package/src/components/navigation/Sidebar.svelte +334 -0
  35. package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
  36. package/src/components/navigation/SidebarAccountItem.svelte +492 -0
  37. package/src/components/navigation/SidebarGroup.svelte +230 -0
  38. package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
  39. package/src/components/navigation/SidebarItem.svelte +210 -0
  40. package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
  41. package/src/components/navigation/SidebarPopover.svelte +145 -0
  42. package/src/components/navigation/SidebarSearch.svelte +236 -0
  43. package/src/components/navigation/SidebarSection.svelte +158 -0
  44. package/src/components/navigation/SidebarToggle.svelte +86 -0
  45. package/src/components/navigation/Tabs.svelte +239 -0
  46. package/src/components/navigation/WorkspaceMenu.svelte +416 -0
  47. package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
  48. package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
  49. package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
  50. package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
  51. package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
  52. package/src/components/navigation/index.js +22 -0
  53. package/src/components/overlays/ConfirmDialog.svelte +272 -0
  54. package/src/components/overlays/Dropdown.svelte +153 -0
  55. package/src/components/overlays/DropdownDivider.svelte +23 -0
  56. package/src/components/overlays/DropdownItem.svelte +97 -0
  57. package/src/components/overlays/Modal.svelte +232 -0
  58. package/src/components/overlays/Popover.svelte +206 -0
  59. package/src/components/primitives/Avatar.svelte +132 -0
  60. package/src/components/primitives/Badge.svelte +118 -0
  61. package/src/components/primitives/Button.svelte +214 -0
  62. package/src/components/primitives/Card.svelte +104 -0
  63. package/src/components/primitives/Divider.svelte +105 -0
  64. package/src/components/primitives/LazyImage.svelte +104 -0
  65. package/src/components/primitives/Link.svelte +122 -0
  66. package/src/components/primitives/Stat.svelte +197 -0
  67. package/src/components/primitives/StatusBadge.svelte +122 -0
  68. package/src/index.js +183 -0
  69. package/src/tokens/colors.css +157 -0
  70. package/src/tokens/effects.css +128 -0
  71. package/src/tokens/index.css +81 -0
  72. package/src/tokens/spacing.css +49 -0
  73. package/src/tokens/typography.css +79 -0
  74. package/src/utils/cn.svelte.js +175 -0
  75. package/src/utils/highlighter.js +124 -0
  76. package/src/utils/index.js +22 -0
  77. package/src/utils/navigation.svelte.js +423 -0
  78. package/src/utils/reactive.svelte.js +328 -0
  79. package/src/utils/sidebar.svelte.js +211 -0
  80. package/jera.js +0 -135
  81. package/www/components/jera/Input/Input.svelte +0 -63
  82. package/www/components/jera/Input/index.js +0 -1
@@ -0,0 +1,230 @@
1
+ <!--
2
+ @component NavigationSection
3
+
4
+ Enterprise navigation section block with recursive item support.
5
+ -->
6
+ <script>
7
+ import { getContext } from 'svelte';
8
+ import { slide, fly } from 'svelte/transition';
9
+ import { cubicOut } from 'svelte/easing';
10
+ import { SIDEBAR_CONTEXT_KEY } from '../../../utils/sidebar.svelte.js';
11
+ import { NAVIGATION_CONTEXT_KEY } from '../../../utils/navigation.svelte.js';
12
+ import SidebarNavigationItem from '../SidebarNavigationItem.svelte';
13
+
14
+ let {
15
+ block,
16
+ navigationState = null,
17
+ onEvent = null
18
+ } = $props();
19
+
20
+ const sidebar = getContext(SIDEBAR_CONTEXT_KEY);
21
+ const navContext = getContext(NAVIGATION_CONTEXT_KEY);
22
+ const navState = navigationState || navContext;
23
+
24
+ const isCollapsed = $derived(sidebar?.collapsed ?? false);
25
+
26
+ // Block configuration with defaults
27
+ const config = $derived({
28
+ title: block.title || '',
29
+ collapsible: block.collapsible ?? true,
30
+ defaultExpanded: block.defaultExpanded ?? true,
31
+ showCount: block.showCount ?? true,
32
+ maxDepth: block.maxDepth ?? 10,
33
+ searchable: block.searchable ?? true,
34
+ ...block.config
35
+ });
36
+
37
+ // Items from block configuration
38
+ const items = $derived(block.items || []);
39
+
40
+ // Filter items if searchable and search is active
41
+ const filteredItems = $derived.by(() => {
42
+ if (!config.searchable || !navState?.searchQuery) return items;
43
+ return navState.filteredItems.filter(item =>
44
+ items.some(blockItem => blockItem.id === item.id)
45
+ );
46
+ });
47
+
48
+ // Section expansion state
49
+ const sectionId = block.id;
50
+ const isExpanded = $derived(
51
+ config.collapsible
52
+ ? (navState?.isSectionExpanded(sectionId) ?? config.defaultExpanded)
53
+ : true
54
+ );
55
+
56
+ function toggleSection() {
57
+ if (!config.collapsible) return;
58
+
59
+ if (navState) {
60
+ navState.toggleSection(sectionId);
61
+ }
62
+
63
+ if (onEvent) {
64
+ onEvent('section_toggled', { sectionId, expanded: !isExpanded });
65
+ }
66
+ }
67
+
68
+ function handleItemNavigate(item, event) {
69
+ if (onEvent) {
70
+ const result = onEvent('item_navigate', { item, event });
71
+ if (result === false) return false;
72
+ }
73
+
74
+ return true; // Continue with default navigation
75
+ }
76
+
77
+ function handleItemToggle(item, expanded) {
78
+ if (onEvent) {
79
+ onEvent('item_toggled', { item, expanded });
80
+ }
81
+ }
82
+ </script>
83
+
84
+ <div class="nav-section-block">
85
+ {#if config.title && !isCollapsed}
86
+ <button
87
+ class="section-header"
88
+ class:collapsible={config.collapsible}
89
+ onclick={toggleSection}
90
+ disabled={!config.collapsible}
91
+ aria-expanded={config.collapsible ? isExpanded : undefined}
92
+ >
93
+ <span class="section-title">{config.title}</span>
94
+
95
+ {#if config.showCount && filteredItems.length > 0}
96
+ <span class="section-count">{filteredItems.length}</span>
97
+ {/if}
98
+
99
+ {#if config.collapsible}
100
+ <svg
101
+ class="section-chevron {isExpanded ? 'expanded' : 'collapsed'}"
102
+ xmlns="http://www.w3.org/2000/svg"
103
+ width="14"
104
+ height="14"
105
+ viewBox="0 0 24 24"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ stroke-width="2"
109
+ stroke-linecap="round"
110
+ stroke-linejoin="round"
111
+ >
112
+ <polyline points="6 9 12 15 18 9"></polyline>
113
+ </svg>
114
+ {/if}
115
+ </button>
116
+ {:else if config.title && isCollapsed}
117
+ <div class="section-divider" title={config.title}></div>
118
+ {/if}
119
+
120
+ {#if isExpanded}
121
+ <ul
122
+ class="section-items"
123
+ transition:slide={{ duration: 200, easing: cubicOut }}
124
+ >
125
+ {#each filteredItems as item (item.id)}
126
+ <SidebarNavigationItem
127
+ {item}
128
+ {navigationState}
129
+ maxDepth={config.maxDepth}
130
+ onNavigate={handleItemNavigate}
131
+ onToggle={handleItemToggle}
132
+ />
133
+ {/each}
134
+
135
+ {#if filteredItems.length === 0 && items.length > 0}
136
+ <li class="no-results">
137
+ <div class="no-results-content">
138
+ <span class="no-results-text">No matching items</span>
139
+ </div>
140
+ </li>
141
+ {/if}
142
+ </ul>
143
+ {/if}
144
+ </div>
145
+
146
+ <style>
147
+ .nav-section-block {
148
+ margin-bottom: var(--nav-block-spacing, 0.5rem);
149
+ }
150
+
151
+ .section-header {
152
+ width: 100%;
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 0.5rem;
156
+ padding: 0.5rem 0.75rem;
157
+ font-size: 0.75rem;
158
+ font-weight: 600;
159
+ color: var(--color-text-muted, var(--color-base04, #737E99));
160
+ text-transform: uppercase;
161
+ letter-spacing: 0.05em;
162
+ border: none;
163
+ background: transparent;
164
+ font-family: inherit;
165
+ justify-content: flex-start;
166
+ transition: all var(--nav-transition-duration, 200ms) var(--nav-transition-easing, ease);
167
+ cursor: default;
168
+ }
169
+
170
+ .section-header.collapsible {
171
+ cursor: pointer;
172
+ }
173
+
174
+ .section-header.collapsible:hover {
175
+ color: var(--color-text, var(--color-base05, #D0D2DB));
176
+ }
177
+
178
+ .section-title {
179
+ flex: 1;
180
+ text-align: left;
181
+ }
182
+
183
+ .section-count {
184
+ padding: 0.125rem 0.375rem;
185
+ font-size: 0.625rem;
186
+ background: var(--color-surface-alt, var(--color-base02, #3E4359));
187
+ color: var(--color-text-muted, var(--color-base05, #D0D2DB));
188
+ border-radius: 9999px;
189
+ flex-shrink: 0;
190
+ }
191
+
192
+ .section-chevron {
193
+ color: var(--color-text-muted, var(--color-base04, #737E99));
194
+ transition: all var(--nav-transition-duration, 200ms) var(--nav-transition-easing, ease);
195
+ flex-shrink: 0;
196
+ }
197
+
198
+ .section-chevron.expanded {
199
+ transform: rotate(180deg);
200
+ }
201
+
202
+ .section-divider {
203
+ margin: 0.5rem 0.75rem;
204
+ border-bottom: 1px solid color-mix(in srgb, var(--color-base03, #565E78) 30%, transparent);
205
+ height: 1px;
206
+ }
207
+
208
+ .section-items {
209
+ list-style: none;
210
+ margin: 0;
211
+ padding: 0;
212
+ display: flex;
213
+ flex-direction: column;
214
+ }
215
+
216
+ .no-results {
217
+ list-style: none;
218
+ }
219
+
220
+ .no-results-content {
221
+ padding: 1rem 0.75rem;
222
+ text-align: center;
223
+ }
224
+
225
+ .no-results-text {
226
+ font-size: 0.875rem;
227
+ color: var(--color-text-muted, var(--color-base05, #D0D2DB));
228
+ font-style: italic;
229
+ }
230
+ </style>
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Navigation Components
3
+ *
4
+ * Reusable navigation components including sidebars, tabs, and accordions.
5
+ */
6
+
7
+ // Sidebar Components
8
+ export { default as Sidebar } from './Sidebar.svelte';
9
+ export { default as SidebarSection } from './SidebarSection.svelte';
10
+ export { default as SidebarItem } from './SidebarItem.svelte';
11
+ export { default as SidebarGroup } from './SidebarGroup.svelte';
12
+ export { default as SidebarPopover } from './SidebarPopover.svelte';
13
+ export { default as SidebarAccountItem } from './SidebarAccountItem.svelte';
14
+ export { default as SidebarToggle } from './SidebarToggle.svelte';
15
+
16
+ // Enterprise Components
17
+ export { default as WorkspaceMenu } from './WorkspaceMenu.svelte';
18
+
19
+ // Other Navigation Components
20
+ export { default as Tabs } from './Tabs.svelte';
21
+ export { default as Accordion } from './Accordion.svelte';
22
+ export { default as AccordionItem } from './AccordionItem.svelte';
@@ -0,0 +1,272 @@
1
+ <!--
2
+ @component ConfirmDialog
3
+
4
+ A confirmation dialog with semantic variants.
5
+
6
+ @example Danger confirmation
7
+ <ConfirmDialog
8
+ bind:open={showDelete}
9
+ title="Delete Item"
10
+ message="Are you sure you want to delete this item? This action cannot be undone."
11
+ variant="danger"
12
+ confirmText="Delete"
13
+ onconfirm={handleDelete}
14
+ />
15
+
16
+ @example Warning confirmation
17
+ <ConfirmDialog
18
+ bind:open={showWarning}
19
+ title="Unsaved Changes"
20
+ message="You have unsaved changes. Are you sure you want to leave?"
21
+ variant="warning"
22
+ confirmText="Leave"
23
+ cancelText="Stay"
24
+ />
25
+
26
+ @example With custom icon
27
+ <ConfirmDialog bind:open={show} title="Confirm">
28
+ {#snippet icon()}
29
+ <CustomIcon size={24} />
30
+ {/snippet}
31
+ </ConfirmDialog>
32
+ -->
33
+ <script>
34
+ import Button from '../primitives/Button.svelte';
35
+ import { escapeKey } from '../../actions/index.js';
36
+
37
+ let {
38
+ open = $bindable(false),
39
+ title = 'Confirm Action',
40
+ message = 'Are you sure you want to proceed?',
41
+ confirmText = 'Confirm',
42
+ cancelText = 'Cancel',
43
+ variant = 'danger',
44
+ class: className = '',
45
+ icon,
46
+ onconfirm,
47
+ oncancel
48
+ } = $props();
49
+
50
+ const variantConfig = $derived({
51
+ danger: {
52
+ iconColor: 'var(--color-base08)',
53
+ iconBg: 'color-mix(in srgb, var(--color-base08) 10%, transparent)',
54
+ buttonVariant: 'danger'
55
+ },
56
+ warning: {
57
+ iconColor: 'var(--color-base0A)',
58
+ iconBg: 'color-mix(in srgb, var(--color-base0A) 10%, transparent)',
59
+ buttonVariant: 'primary'
60
+ },
61
+ success: {
62
+ iconColor: 'var(--color-base0B)',
63
+ iconBg: 'color-mix(in srgb, var(--color-base0B) 10%, transparent)',
64
+ buttonVariant: 'success'
65
+ },
66
+ info: {
67
+ iconColor: 'var(--color-base0D)',
68
+ iconBg: 'color-mix(in srgb, var(--color-base0D) 10%, transparent)',
69
+ buttonVariant: 'primary'
70
+ }
71
+ }[variant] || {
72
+ iconColor: 'var(--color-base0D)',
73
+ iconBg: 'color-mix(in srgb, var(--color-base0D) 10%, transparent)',
74
+ buttonVariant: 'primary'
75
+ });
76
+
77
+ function handleConfirm() {
78
+ onconfirm?.();
79
+ open = false;
80
+ }
81
+
82
+ function handleCancel() {
83
+ oncancel?.();
84
+ open = false;
85
+ }
86
+
87
+ function handleBackdropClick(e) {
88
+ if (e.target === e.currentTarget) {
89
+ handleCancel();
90
+ }
91
+ }
92
+ </script>
93
+
94
+ {#if open}
95
+ <div
96
+ class="confirm-backdrop {className}"
97
+ onclick={handleBackdropClick}
98
+ use:escapeKey={handleCancel}
99
+ role="dialog"
100
+ aria-modal="true"
101
+ aria-labelledby="confirm-title"
102
+ >
103
+ <div class="confirm-dialog">
104
+ <button
105
+ type="button"
106
+ class="confirm-close"
107
+ onclick={handleCancel}
108
+ aria-label="Close"
109
+ >
110
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
111
+ <line x1="18" y1="6" x2="6" y2="18"></line>
112
+ <line x1="6" y1="6" x2="18" y2="18"></line>
113
+ </svg>
114
+ </button>
115
+
116
+ <div class="confirm-content">
117
+ <div class="confirm-header">
118
+ <div
119
+ class="confirm-icon"
120
+ style="background: {variantConfig.iconBg}; color: {variantConfig.iconColor};"
121
+ >
122
+ {#if icon}
123
+ {@render icon()}
124
+ {:else if variant === 'danger'}
125
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
126
+ <polyline points="3 6 5 6 21 6"></polyline>
127
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
128
+ </svg>
129
+ {:else if variant === 'warning'}
130
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
131
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
132
+ <line x1="12" y1="9" x2="12" y2="13"></line>
133
+ <line x1="12" y1="17" x2="12.01" y2="17"></line>
134
+ </svg>
135
+ {:else if variant === 'success'}
136
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
137
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
138
+ <polyline points="22 4 12 14.01 9 11.01"></polyline>
139
+ </svg>
140
+ {:else}
141
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
142
+ <circle cx="12" cy="12" r="10"></circle>
143
+ <line x1="12" y1="16" x2="12" y2="12"></line>
144
+ <line x1="12" y1="8" x2="12.01" y2="8"></line>
145
+ </svg>
146
+ {/if}
147
+ </div>
148
+
149
+ <div class="confirm-text">
150
+ <h3 id="confirm-title" class="confirm-title">{title}</h3>
151
+ <p class="confirm-message">{message}</p>
152
+ </div>
153
+ </div>
154
+
155
+ <div class="confirm-actions">
156
+ <Button variant="ghost" size="sm" onclick={handleCancel}>
157
+ {cancelText}
158
+ </Button>
159
+ <Button variant={variantConfig.buttonVariant} size="sm" onclick={handleConfirm}>
160
+ {confirmText}
161
+ </Button>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ {/if}
167
+
168
+ <style>
169
+ .confirm-backdrop {
170
+ position: fixed;
171
+ inset: 0;
172
+ z-index: 100;
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ }
177
+
178
+ .confirm-backdrop::before {
179
+ content: '';
180
+ position: absolute;
181
+ inset: 0;
182
+ background: color-mix(in srgb, var(--color-base00) 80%, transparent);
183
+ backdrop-filter: blur(8px);
184
+ }
185
+
186
+ .confirm-dialog {
187
+ position: relative;
188
+ background: var(--color-base01);
189
+ border: 1px solid var(--color-base02);
190
+ border-radius: var(--radius-xl);
191
+ box-shadow: var(--shadow-2xl);
192
+ width: 100%;
193
+ max-width: 28rem;
194
+ margin: var(--space-4);
195
+ animation: dialog-enter 0.2s ease-out;
196
+ }
197
+
198
+ @keyframes dialog-enter {
199
+ from {
200
+ opacity: 0;
201
+ transform: scale(0.95) translateY(10px);
202
+ }
203
+ to {
204
+ opacity: 1;
205
+ transform: scale(1) translateY(0);
206
+ }
207
+ }
208
+
209
+ .confirm-close {
210
+ position: absolute;
211
+ top: var(--space-4);
212
+ right: var(--space-4);
213
+ padding: var(--space-1);
214
+ background: transparent;
215
+ border: none;
216
+ border-radius: var(--radius-lg);
217
+ color: var(--color-base04);
218
+ cursor: pointer;
219
+ transition: background 0.15s ease, color 0.15s ease;
220
+ }
221
+
222
+ .confirm-close:hover {
223
+ background: var(--color-base02);
224
+ color: var(--color-base05);
225
+ }
226
+
227
+ .confirm-content {
228
+ padding: var(--space-6);
229
+ }
230
+
231
+ .confirm-header {
232
+ display: flex;
233
+ align-items: flex-start;
234
+ gap: var(--space-4);
235
+ }
236
+
237
+ .confirm-icon {
238
+ flex-shrink: 0;
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ width: 3rem;
243
+ height: 3rem;
244
+ border-radius: var(--radius-lg);
245
+ }
246
+
247
+ .confirm-text {
248
+ flex: 1;
249
+ padding-top: var(--space-1);
250
+ }
251
+
252
+ .confirm-title {
253
+ margin: 0 0 var(--space-2) 0;
254
+ font-size: var(--text-lg);
255
+ font-weight: 600;
256
+ color: var(--color-base07);
257
+ }
258
+
259
+ .confirm-message {
260
+ margin: 0;
261
+ font-size: var(--text-sm);
262
+ color: var(--color-base04);
263
+ line-height: 1.5;
264
+ }
265
+
266
+ .confirm-actions {
267
+ display: flex;
268
+ gap: var(--space-3);
269
+ justify-content: flex-end;
270
+ margin-top: var(--space-6);
271
+ }
272
+ </style>
@@ -0,0 +1,153 @@
1
+ <!--
2
+ @component Dropdown
3
+
4
+ An action menu dropdown component.
5
+
6
+ @example
7
+ <Dropdown>
8
+ {#snippet trigger()}
9
+ <Button>Options</Button>
10
+ {/snippet}
11
+ <DropdownItem onclick={handleEdit}>Edit</DropdownItem>
12
+ <DropdownItem onclick={handleDelete} variant="danger">Delete</DropdownItem>
13
+ </Dropdown>
14
+ -->
15
+ <script>
16
+ import { clickOutside, escapeKey } from '../../actions/index.js';
17
+ import { cn } from '../../utils/cn.svelte.js';
18
+
19
+ let {
20
+ open = $bindable(false),
21
+ position = 'bottom-start',
22
+ trigger,
23
+ children,
24
+ class: className = ''
25
+ } = $props();
26
+
27
+ let dropdownEl = $state(null);
28
+
29
+ function toggle() {
30
+ open = !open;
31
+ }
32
+
33
+ function close() {
34
+ open = false;
35
+ }
36
+
37
+ // Position classes
38
+ const positionMap = {
39
+ 'bottom-start': 'dropdown-bottom-start',
40
+ 'bottom-end': 'dropdown-bottom-end',
41
+ 'bottom-center': 'dropdown-bottom-center',
42
+ 'top-start': 'dropdown-top-start',
43
+ 'top-end': 'dropdown-top-end',
44
+ 'top-center': 'dropdown-top-center'
45
+ };
46
+ </script>
47
+
48
+ <div
49
+ class={cn('dropdown', className)}
50
+ bind:this={dropdownEl}
51
+ use:clickOutside={close}
52
+ use:escapeKey={close}
53
+ >
54
+ <div class="dropdown-trigger" onclick={toggle}>
55
+ {@render trigger?.()}
56
+ </div>
57
+
58
+ {#if open}
59
+ <div class="dropdown-content {positionMap[position] || positionMap['bottom-start']}">
60
+ {@render children?.()}
61
+ </div>
62
+ {/if}
63
+ </div>
64
+
65
+ <style>
66
+ .dropdown {
67
+ position: relative;
68
+ display: inline-block;
69
+ }
70
+
71
+ .dropdown-trigger {
72
+ display: inline-flex;
73
+ }
74
+
75
+ .dropdown-content {
76
+ position: absolute;
77
+ z-index: 50;
78
+ min-width: 10rem;
79
+ padding: var(--space-1);
80
+ background: var(--color-base00);
81
+ border: 1px solid var(--color-base03);
82
+ border-radius: var(--radius-lg);
83
+ box-shadow: var(--shadow-lg);
84
+ animation: dropdown-enter 0.15s ease-out;
85
+ }
86
+
87
+ /* Position variants */
88
+ .dropdown-bottom-start {
89
+ top: 100%;
90
+ left: 0;
91
+ margin-top: var(--space-1);
92
+ }
93
+
94
+ .dropdown-bottom-end {
95
+ top: 100%;
96
+ right: 0;
97
+ margin-top: var(--space-1);
98
+ }
99
+
100
+ .dropdown-bottom-center {
101
+ top: 100%;
102
+ left: 50%;
103
+ transform: translateX(-50%);
104
+ margin-top: var(--space-1);
105
+ }
106
+
107
+ .dropdown-top-start {
108
+ bottom: 100%;
109
+ left: 0;
110
+ margin-bottom: var(--space-1);
111
+ }
112
+
113
+ .dropdown-top-end {
114
+ bottom: 100%;
115
+ right: 0;
116
+ margin-bottom: var(--space-1);
117
+ }
118
+
119
+ .dropdown-top-center {
120
+ bottom: 100%;
121
+ left: 50%;
122
+ transform: translateX(-50%);
123
+ margin-bottom: var(--space-1);
124
+ }
125
+
126
+ @keyframes dropdown-enter {
127
+ from {
128
+ opacity: 0;
129
+ transform: translateY(-4px);
130
+ }
131
+ to {
132
+ opacity: 1;
133
+ transform: translateY(0);
134
+ }
135
+ }
136
+
137
+ .dropdown-top-start,
138
+ .dropdown-top-end,
139
+ .dropdown-top-center {
140
+ animation-name: dropdown-enter-up;
141
+ }
142
+
143
+ @keyframes dropdown-enter-up {
144
+ from {
145
+ opacity: 0;
146
+ transform: translateY(4px);
147
+ }
148
+ to {
149
+ opacity: 1;
150
+ transform: translateY(0);
151
+ }
152
+ }
153
+ </style>
@@ -0,0 +1,23 @@
1
+ <!--
2
+ @component DropdownDivider
3
+
4
+ A divider within a Dropdown menu.
5
+
6
+ @example
7
+ <DropdownItem>Edit</DropdownItem>
8
+ <DropdownDivider />
9
+ <DropdownItem variant="danger">Delete</DropdownItem>
10
+ -->
11
+ <script>
12
+ let { class: className = '' } = $props();
13
+ </script>
14
+
15
+ <div class="dropdown-divider {className}" role="separator"></div>
16
+
17
+ <style>
18
+ .dropdown-divider {
19
+ height: 1px;
20
+ margin: var(--space-1) 0;
21
+ background: var(--color-base02);
22
+ }
23
+ </style>