@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,236 @@
1
+ <!--
2
+ @component SidebarSearch
3
+
4
+ Search functionality for sidebar with expand/collapse behavior.
5
+ Matches dash.selify.ai search with filtering capabilities.
6
+
7
+ @example
8
+ <SidebarSearch
9
+ bind:query={searchQuery}
10
+ bind:expanded={searchExpanded}
11
+ placeholder="Search navigation..."
12
+ onSearch={(query) => filterNavigation(query)}
13
+ />
14
+ -->
15
+ <script>
16
+ import { getContext } from 'svelte';
17
+ import { fade, fly } from 'svelte/transition';
18
+ import { SIDEBAR_CONTEXT_KEY } from '../../utils/sidebar.svelte.js';
19
+
20
+ let {
21
+ query = $bindable(''),
22
+ expanded = $bindable(false),
23
+ placeholder = 'Search...',
24
+ onSearch = null,
25
+ onClear = null,
26
+ class: className = ''
27
+ } = $props();
28
+
29
+ const sidebar = getContext(SIDEBAR_CONTEXT_KEY);
30
+ const isCollapsed = $derived(sidebar?.collapsed ?? false);
31
+
32
+ let searchInput;
33
+
34
+ function expandSearch() {
35
+ expanded = true;
36
+ // Focus input after transition
37
+ setTimeout(() => {
38
+ if (searchInput) searchInput.focus();
39
+ }, 50);
40
+ }
41
+
42
+ function collapseSearch() {
43
+ expanded = false;
44
+ query = '';
45
+ if (onClear) onClear();
46
+ }
47
+
48
+ function handleInput(event) {
49
+ query = event.target.value;
50
+ if (onSearch) onSearch(query);
51
+ }
52
+
53
+ function handleKeydown(event) {
54
+ if (event.key === 'Escape') {
55
+ collapseSearch();
56
+ }
57
+ }
58
+
59
+ function handleBlur() {
60
+ // Collapse if query is empty after a delay
61
+ setTimeout(() => {
62
+ if (!query.trim()) {
63
+ expanded = false;
64
+ }
65
+ }, 200);
66
+ }
67
+ </script>
68
+
69
+ <div class="search-section {className}">
70
+ {#if expanded}
71
+ <div class="search-input-wrapper" transition:fly={{ x: -20, duration: 200 }}>
72
+ <svg
73
+ class="search-icon"
74
+ xmlns="http://www.w3.org/2000/svg"
75
+ width="16"
76
+ height="16"
77
+ viewBox="0 0 24 24"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ stroke-width="2"
81
+ stroke-linecap="round"
82
+ stroke-linejoin="round"
83
+ >
84
+ <circle cx="11" cy="11" r="8"></circle>
85
+ <path d="m21 21-4.35-4.35"></path>
86
+ </svg>
87
+ <input
88
+ bind:this={searchInput}
89
+ type="text"
90
+ bind:value={query}
91
+ {placeholder}
92
+ class="search-input"
93
+ oninput={handleInput}
94
+ onblur={handleBlur}
95
+ onkeydown={handleKeydown}
96
+ />
97
+ <button
98
+ class="search-close"
99
+ onclick={collapseSearch}
100
+ title="Clear search"
101
+ >
102
+ <svg
103
+ xmlns="http://www.w3.org/2000/svg"
104
+ width="14"
105
+ height="14"
106
+ viewBox="0 0 24 24"
107
+ fill="none"
108
+ stroke="currentColor"
109
+ stroke-width="2"
110
+ stroke-linecap="round"
111
+ stroke-linejoin="round"
112
+ >
113
+ <line x1="18" y1="6" x2="6" y2="18"></line>
114
+ <line x1="6" y1="6" x2="18" y2="18"></line>
115
+ </svg>
116
+ </button>
117
+ </div>
118
+ {:else}
119
+ <button
120
+ class="search-toggle"
121
+ onclick={expandSearch}
122
+ title="Search navigation"
123
+ transition:fly={{ x: -20, duration: 200, delay: 50 }}
124
+ >
125
+ <svg
126
+ xmlns="http://www.w3.org/2000/svg"
127
+ width="16"
128
+ height="16"
129
+ viewBox="0 0 24 24"
130
+ fill="none"
131
+ stroke="currentColor"
132
+ stroke-width="2"
133
+ stroke-linecap="round"
134
+ stroke-linejoin="round"
135
+ >
136
+ <circle cx="11" cy="11" r="8"></circle>
137
+ <path d="m21 21-4.35-4.35"></path>
138
+ </svg>
139
+ {#if !isCollapsed}
140
+ <span class="search-label" transition:fly={{ x: -15, duration: 200, delay: 75 }}>
141
+ Search
142
+ </span>
143
+ {/if}
144
+ </button>
145
+ {/if}
146
+ </div>
147
+
148
+ <style>
149
+ .search-section {
150
+ padding: 0.5rem;
151
+ border-bottom: 1px solid color-mix(in srgb, var(--color-base03, #565E78) 30%, transparent);
152
+ }
153
+
154
+ .search-toggle {
155
+ width: 100%;
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 0.75rem;
159
+ padding: 0.5rem 0.75rem;
160
+ font-size: 0.875rem;
161
+ color: var(--color-text, var(--color-base06, #F3F4F7));
162
+ cursor: pointer;
163
+ border-radius: 0.375rem;
164
+ transition: all 200ms ease;
165
+ border: none;
166
+ background: transparent;
167
+ font-family: inherit;
168
+ text-align: left;
169
+ }
170
+
171
+ .search-toggle:hover {
172
+ color: var(--color-primary, var(--color-base0D, #83D2FC));
173
+ background-color: color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) 10%, transparent);
174
+ }
175
+
176
+ .search-label {
177
+ flex: 1;
178
+ text-align: left;
179
+ font-weight: 500;
180
+ }
181
+
182
+ .search-input-wrapper {
183
+ position: relative;
184
+ display: flex;
185
+ align-items: center;
186
+ padding: 0 0.75rem;
187
+ }
188
+
189
+ .search-icon {
190
+ position: absolute;
191
+ left: 1.25rem;
192
+ color: var(--color-text-muted, var(--color-base04, #737E99));
193
+ pointer-events: none;
194
+ z-index: 10;
195
+ }
196
+
197
+ .search-input {
198
+ width: 100%;
199
+ padding: 0.5rem 0.5rem 0.5rem 2.25rem;
200
+ background-color: var(--color-surface-alt, var(--color-base02, #3E4359));
201
+ border: 1px solid color-mix(in srgb, var(--color-base03, #565E78) 40%, transparent);
202
+ border-radius: 0.375rem;
203
+ font-size: 0.875rem;
204
+ color: var(--color-text, var(--color-base06, #F3F4F7));
205
+ transition: all 200ms ease;
206
+ font-family: inherit;
207
+ }
208
+
209
+ .search-input::placeholder {
210
+ color: var(--color-text-muted, var(--color-base04, #737E99));
211
+ }
212
+
213
+ .search-input:focus {
214
+ outline: none;
215
+ border-color: var(--color-primary, var(--color-base0D, #83D2FC));
216
+ background-color: var(--color-surface, var(--color-base01, #2C3040));
217
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) 20%, transparent);
218
+ }
219
+
220
+ .search-close {
221
+ position: absolute;
222
+ right: 1.25rem;
223
+ padding: 0.25rem;
224
+ border-radius: 0.25rem;
225
+ color: var(--color-text-muted, var(--color-base05, #D0D2DB));
226
+ cursor: pointer;
227
+ border: none;
228
+ background: transparent;
229
+ transition: all 200ms ease;
230
+ }
231
+
232
+ .search-close:hover {
233
+ background-color: color-mix(in srgb, var(--color-base03, #565E78) 50%, transparent);
234
+ color: var(--color-text, var(--color-base06, #F3F4F7));
235
+ }
236
+ </style>
@@ -0,0 +1,158 @@
1
+ <!--
2
+ @component SidebarSection
3
+
4
+ Grouped section within sidebar with optional title.
5
+ Shows title when expanded, shows divider when collapsed.
6
+
7
+ @example
8
+ <SidebarSection title="Management">
9
+ <SidebarItem href="/settings" icon={Settings} label="Settings" />
10
+ <SidebarItem href="/billing" icon={CreditCard} label="Billing" />
11
+ </SidebarSection>
12
+ -->
13
+ <script>
14
+ import { getContext } from 'svelte';
15
+ import { fade, fly, slide } from 'svelte/transition';
16
+ import { cubicOut } from 'svelte/easing';
17
+ import { SIDEBAR_CONTEXT_KEY } from '../../utils/sidebar.svelte.js';
18
+
19
+ let {
20
+ title = '',
21
+ count = null,
22
+ expandable = false,
23
+ expanded = $bindable(true),
24
+ class: className = '',
25
+ children
26
+ } = $props();
27
+
28
+ const sidebar = getContext(SIDEBAR_CONTEXT_KEY);
29
+ const isCollapsed = $derived(sidebar?.collapsed ?? false);
30
+
31
+ function toggleExpanded() {
32
+ if (expandable) {
33
+ expanded = !expanded;
34
+ }
35
+ }
36
+ </script>
37
+
38
+ <div class="sidebar-section {className}">
39
+ {#if title}
40
+ {#if !isCollapsed}
41
+ {#if expandable}
42
+ <button
43
+ class="section-header expandable"
44
+ onclick={toggleExpanded}
45
+ aria-expanded={expanded}
46
+ transition:fade={{ duration: 150 }}
47
+ >
48
+ <span class="section-title">{title}</span>
49
+ {#if count !== null}
50
+ <span class="section-count">{count}</span>
51
+ {/if}
52
+ <svg
53
+ class="section-chevron {expanded ? 'rotate-180' : ''}"
54
+ xmlns="http://www.w3.org/2000/svg"
55
+ width="14"
56
+ height="14"
57
+ viewBox="0 0 24 24"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ stroke-width="2"
61
+ stroke-linecap="round"
62
+ stroke-linejoin="round"
63
+ >
64
+ <polyline points="6 9 12 15 18 9"></polyline>
65
+ </svg>
66
+ </button>
67
+ {:else}
68
+ <div class="section-header" transition:fade={{ duration: 150 }}>
69
+ <span class="section-title">{title}</span>
70
+ {#if count !== null}
71
+ <span class="section-count">{count}</span>
72
+ {/if}
73
+ </div>
74
+ {/if}
75
+ {:else}
76
+ <div class="section-divider"></div>
77
+ {/if}
78
+ {/if}
79
+
80
+ {#if !expandable || expanded}
81
+ <ul class="nav-list" transition:slide={{ duration: 200, easing: cubicOut }}>
82
+ {@render children?.()}
83
+ </ul>
84
+ {/if}
85
+ </div>
86
+
87
+ <style>
88
+ .sidebar-section {
89
+ padding: 0;
90
+ }
91
+
92
+ .section-header {
93
+ padding: 0.25rem 0.75rem;
94
+ margin-bottom: 0.25rem;
95
+ font-size: 0.75rem;
96
+ font-weight: 600;
97
+ color: var(--color-text-muted, var(--color-base05, #D0D2DB));
98
+ text-align: left;
99
+ text-transform: uppercase;
100
+ letter-spacing: 0.05em;
101
+ white-space: nowrap;
102
+ overflow: hidden;
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 0.5rem;
106
+ }
107
+
108
+ .section-header.expandable {
109
+ cursor: pointer;
110
+ transition: all 200ms ease;
111
+ border: none;
112
+ background: transparent;
113
+ font-family: inherit;
114
+ width: 100%;
115
+ justify-content: flex-start;
116
+ }
117
+
118
+ .section-header.expandable:hover {
119
+ color: var(--color-text, var(--color-base06, #F3F4F7));
120
+ }
121
+
122
+ .section-title {
123
+ flex: 1;
124
+ text-align: left;
125
+ }
126
+
127
+ .section-count {
128
+ padding: 0.125rem 0.375rem;
129
+ font-size: 0.625rem;
130
+ background-color: var(--color-surface-alt, var(--color-base02, #3E4359));
131
+ color: var(--color-text-muted, var(--color-base05, #D0D2DB));
132
+ border-radius: 9999px;
133
+ flex-shrink: 0;
134
+ }
135
+
136
+ .section-chevron {
137
+ color: var(--color-text-muted, var(--color-base05, #D0D2DB));
138
+ transition: all 200ms ease;
139
+ flex-shrink: 0;
140
+ }
141
+
142
+ .section-chevron.rotate-180 {
143
+ transform: rotate(180deg);
144
+ }
145
+
146
+ .section-divider {
147
+ margin: 0.5rem 0.75rem;
148
+ border-bottom: 1px solid color-mix(in srgb, var(--color-base03, #565E78) 30%, transparent);
149
+ }
150
+
151
+ .nav-list {
152
+ display: flex;
153
+ flex-direction: column;
154
+ list-style: none;
155
+ margin: 0;
156
+ padding: 0;
157
+ }
158
+ </style>
@@ -0,0 +1,86 @@
1
+ <!--
2
+ @component SidebarToggle
3
+
4
+ Toggle button for collapsing/expanding the sidebar.
5
+ Automatically gets collapsed state from Sidebar context.
6
+
7
+ @example
8
+ <Sidebar bind:collapsed>
9
+ ...
10
+ {#snippet footer()}
11
+ <SidebarToggle />
12
+ {/snippet}
13
+ </Sidebar>
14
+
15
+ @example With custom styling
16
+ <SidebarToggle class="my-custom-toggle" />
17
+ -->
18
+ <script>
19
+ import { getContext } from 'svelte';
20
+ import { SIDEBAR_CONTEXT_KEY } from '../../utils/sidebar.svelte.js';
21
+
22
+ let {
23
+ class: className = ''
24
+ } = $props();
25
+
26
+ const sidebar = getContext(SIDEBAR_CONTEXT_KEY);
27
+ const isCollapsed = $derived(sidebar?.collapsed ?? false);
28
+
29
+ function handleClick() {
30
+ sidebar?.toggle?.();
31
+ }
32
+ </script>
33
+
34
+ <button
35
+ type="button"
36
+ class="sidebar-toggle {className}"
37
+ onclick={handleClick}
38
+ title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
39
+ aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
40
+ aria-expanded={!isCollapsed}
41
+ >
42
+ <svg
43
+ class="toggle-icon"
44
+ class:collapsed={isCollapsed}
45
+ xmlns="http://www.w3.org/2000/svg"
46
+ width="16"
47
+ height="16"
48
+ viewBox="0 0 24 24"
49
+ fill="none"
50
+ stroke="currentColor"
51
+ stroke-width="2"
52
+ stroke-linecap="round"
53
+ stroke-linejoin="round"
54
+ >
55
+ <path d="m15 18-6-6 6-6"></path>
56
+ </svg>
57
+ </button>
58
+
59
+ <style>
60
+ .sidebar-toggle {
61
+ width: 100%;
62
+ padding: 0.625rem;
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ color: var(--color-text-muted, var(--color-base05, #D0D2DB));
67
+ background: transparent;
68
+ border: none;
69
+ border-radius: 0.5rem;
70
+ cursor: pointer;
71
+ transition: all 150ms ease;
72
+ }
73
+
74
+ .sidebar-toggle:hover {
75
+ color: var(--color-text, var(--color-base06, #F3F4F7));
76
+ background-color: var(--color-surface-alt, var(--color-base02, #3E4359));
77
+ }
78
+
79
+ .toggle-icon {
80
+ transition: transform 200ms ease;
81
+ }
82
+
83
+ .toggle-icon.collapsed {
84
+ transform: rotate(180deg);
85
+ }
86
+ </style>
@@ -0,0 +1,239 @@
1
+ <!--
2
+ @component Tabs
3
+
4
+ Tabbed navigation component.
5
+
6
+ @example Basic usage
7
+ <Tabs
8
+ tabs={[
9
+ { id: 'tab1', label: 'Overview' },
10
+ { id: 'tab2', label: 'Settings' },
11
+ { id: 'tab3', label: 'Analytics' }
12
+ ]}
13
+ bind:active={activeTab}
14
+ />
15
+
16
+ @example With icons
17
+ <Tabs
18
+ tabs={[
19
+ { id: 'home', label: 'Home', icon: HomeIcon },
20
+ { id: 'settings', label: 'Settings', icon: SettingsIcon }
21
+ ]}
22
+ />
23
+ -->
24
+ <script>
25
+ let {
26
+ tabs = [],
27
+ active = $bindable(null),
28
+ variant = 'default',
29
+ size = 'md',
30
+ fullWidth = false,
31
+ onchange = () => {},
32
+ class: className = ''
33
+ } = $props();
34
+
35
+ // Initialize active to first tab if not set
36
+ $effect(() => {
37
+ if (active === null && tabs.length > 0) {
38
+ active = tabs[0].id;
39
+ }
40
+ });
41
+
42
+ function selectTab(tab) {
43
+ if (tab.disabled) return;
44
+ active = tab.id;
45
+ onchange(tab);
46
+ }
47
+
48
+ function handleKeydown(e, tab, index) {
49
+ let nextIndex = index;
50
+
51
+ if (e.key === 'ArrowRight') {
52
+ nextIndex = (index + 1) % tabs.length;
53
+ } else if (e.key === 'ArrowLeft') {
54
+ nextIndex = (index - 1 + tabs.length) % tabs.length;
55
+ } else if (e.key === 'Home') {
56
+ nextIndex = 0;
57
+ } else if (e.key === 'End') {
58
+ nextIndex = tabs.length - 1;
59
+ } else {
60
+ return;
61
+ }
62
+
63
+ e.preventDefault();
64
+ const nextTab = tabs[nextIndex];
65
+ if (!nextTab.disabled) {
66
+ selectTab(nextTab);
67
+ // Focus the next tab button
68
+ const buttons = e.currentTarget.parentElement.querySelectorAll('[role="tab"]');
69
+ buttons[nextIndex]?.focus();
70
+ }
71
+ }
72
+ </script>
73
+
74
+ <div
75
+ class="tabs tabs-{variant} tabs-{size} {className}"
76
+ class:tabs-full-width={fullWidth}
77
+ role="tablist"
78
+ >
79
+ {#each tabs as tab, index}
80
+ <button
81
+ type="button"
82
+ role="tab"
83
+ class="tab"
84
+ class:tab-active={active === tab.id}
85
+ class:tab-disabled={tab.disabled}
86
+ aria-selected={active === tab.id}
87
+ aria-disabled={tab.disabled}
88
+ tabindex={active === tab.id ? 0 : -1}
89
+ onclick={() => selectTab(tab)}
90
+ onkeydown={(e) => handleKeydown(e, tab, index)}
91
+ >
92
+ {#if tab.icon}
93
+ <span class="tab-icon">
94
+ <svelte:component this={tab.icon} size={16} />
95
+ </span>
96
+ {/if}
97
+ {#if tab.label}
98
+ <span class="tab-label">{tab.label}</span>
99
+ {/if}
100
+ {#if tab.badge !== undefined}
101
+ <span class="tab-badge">{tab.badge}</span>
102
+ {/if}
103
+ </button>
104
+ {/each}
105
+ </div>
106
+
107
+ <style>
108
+ .tabs {
109
+ display: inline-flex;
110
+ gap: 0.25rem;
111
+ background: var(--color-base01);
112
+ border-radius: 0.5rem;
113
+ padding: 0.25rem;
114
+ }
115
+
116
+ .tabs-full-width {
117
+ display: flex;
118
+ width: 100%;
119
+ }
120
+
121
+ .tabs-full-width .tab {
122
+ flex: 1;
123
+ justify-content: center;
124
+ }
125
+
126
+ .tab {
127
+ display: inline-flex;
128
+ align-items: center;
129
+ gap: 0.5rem;
130
+ padding: 0.5rem 1rem;
131
+ background: transparent;
132
+ border: none;
133
+ border-radius: 0.375rem;
134
+ font-size: 0.875rem;
135
+ font-weight: 500;
136
+ color: var(--color-base05);
137
+ cursor: pointer;
138
+ transition: all 0.15s ease;
139
+ white-space: nowrap;
140
+ }
141
+
142
+ /* Size variants */
143
+ .tabs-sm .tab {
144
+ padding: 0.375rem 0.75rem;
145
+ font-size: 0.75rem;
146
+ }
147
+
148
+ .tabs-lg .tab {
149
+ padding: 0.625rem 1.25rem;
150
+ font-size: 1rem;
151
+ }
152
+
153
+ .tab:hover:not(.tab-disabled) {
154
+ color: var(--color-base07);
155
+ background: var(--color-base02);
156
+ }
157
+
158
+ .tab:focus-visible {
159
+ outline: none;
160
+ box-shadow: 0 0 0 2px var(--color-base0D);
161
+ }
162
+
163
+ .tab-active {
164
+ background: var(--color-base02);
165
+ color: var(--color-base07);
166
+ box-shadow: var(--shadow-sm);
167
+ }
168
+
169
+ .tab-disabled {
170
+ opacity: 0.5;
171
+ cursor: not-allowed;
172
+ }
173
+
174
+ .tab-icon {
175
+ display: flex;
176
+ align-items: center;
177
+ }
178
+
179
+ .tab-badge {
180
+ display: inline-flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ min-width: 1.25rem;
184
+ height: 1.25rem;
185
+ padding: 0 0.375rem;
186
+ background: var(--color-base03);
187
+ border-radius: 9999px;
188
+ font-size: 0.75rem;
189
+ font-weight: 600;
190
+ }
191
+
192
+ .tab-active .tab-badge {
193
+ background: var(--color-base0D);
194
+ color: var(--color-base07);
195
+ }
196
+
197
+ /* Underline variant */
198
+ .tabs-underline {
199
+ background: transparent;
200
+ padding: 0;
201
+ gap: 0;
202
+ border-bottom: 1px solid var(--color-base03);
203
+ }
204
+
205
+ .tabs-underline .tab {
206
+ border-radius: 0;
207
+ margin-bottom: -1px;
208
+ border-bottom: 2px solid transparent;
209
+ }
210
+
211
+ .tabs-underline .tab-active {
212
+ background: transparent;
213
+ box-shadow: none;
214
+ border-bottom-color: var(--color-base0D);
215
+ color: var(--color-base0D);
216
+ }
217
+
218
+ .tabs-underline .tab:hover:not(.tab-disabled) {
219
+ background: transparent;
220
+ border-bottom-color: var(--color-base03);
221
+ }
222
+
223
+ /* Pills variant */
224
+ .tabs-pills {
225
+ background: transparent;
226
+ padding: 0;
227
+ gap: 0.5rem;
228
+ }
229
+
230
+ .tabs-pills .tab {
231
+ border: 1px solid var(--color-base03);
232
+ }
233
+
234
+ .tabs-pills .tab-active {
235
+ background: var(--color-base0D);
236
+ border-color: var(--color-base0D);
237
+ color: var(--color-base07);
238
+ }
239
+ </style>