@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.
- package/CLAUDE.md +734 -0
- package/README.md +219 -1
- package/llms.txt +97 -0
- package/package.json +54 -14
- package/src/actions/index.js +375 -0
- package/src/components/docs/CodeBlock.svelte +203 -0
- package/src/components/docs/DocSection.svelte +120 -0
- package/src/components/docs/PropsTable.svelte +136 -0
- package/src/components/docs/SplitPane.svelte +98 -0
- package/src/components/docs/index.js +14 -0
- package/src/components/feedback/Alert.svelte +234 -0
- package/src/components/feedback/EmptyState.svelte +179 -0
- package/src/components/feedback/ProgressBar.svelte +116 -0
- package/src/components/feedback/Skeleton.svelte +107 -0
- package/src/components/feedback/Spinner.svelte +77 -0
- package/src/components/feedback/Toast.svelte +261 -0
- package/src/components/forms/Checkbox.svelte +147 -0
- package/src/components/forms/Dropzone.svelte +248 -0
- package/src/components/forms/FileUpload.svelte +266 -0
- package/src/components/forms/IconInput.svelte +184 -0
- package/src/components/forms/Input.svelte +121 -0
- package/src/components/forms/NumberInput.svelte +225 -0
- package/src/components/forms/PinInput.svelte +169 -0
- package/src/components/forms/Radio.svelte +143 -0
- package/src/components/forms/RadioGroup.svelte +62 -0
- package/src/components/forms/RangeSlider.svelte +212 -0
- package/src/components/forms/SearchInput.svelte +175 -0
- package/src/components/forms/Select.svelte +324 -0
- package/src/components/forms/Switch.svelte +159 -0
- package/src/components/forms/Textarea.svelte +122 -0
- package/src/components/navigation/Accordion.svelte +65 -0
- package/src/components/navigation/AccordionItem.svelte +146 -0
- package/src/components/navigation/NavigationContainer.svelte +344 -0
- package/src/components/navigation/Sidebar.svelte +334 -0
- package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
- package/src/components/navigation/SidebarAccountItem.svelte +492 -0
- package/src/components/navigation/SidebarGroup.svelte +230 -0
- package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
- package/src/components/navigation/SidebarItem.svelte +210 -0
- package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
- package/src/components/navigation/SidebarPopover.svelte +145 -0
- package/src/components/navigation/SidebarSearch.svelte +236 -0
- package/src/components/navigation/SidebarSection.svelte +158 -0
- package/src/components/navigation/SidebarToggle.svelte +86 -0
- package/src/components/navigation/Tabs.svelte +239 -0
- package/src/components/navigation/WorkspaceMenu.svelte +416 -0
- package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
- package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
- package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
- package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
- package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
- package/src/components/navigation/index.js +22 -0
- package/src/components/overlays/ConfirmDialog.svelte +272 -0
- package/src/components/overlays/Dropdown.svelte +153 -0
- package/src/components/overlays/DropdownDivider.svelte +23 -0
- package/src/components/overlays/DropdownItem.svelte +97 -0
- package/src/components/overlays/Modal.svelte +232 -0
- package/src/components/overlays/Popover.svelte +206 -0
- package/src/components/primitives/Avatar.svelte +132 -0
- package/src/components/primitives/Badge.svelte +118 -0
- package/src/components/primitives/Button.svelte +214 -0
- package/src/components/primitives/Card.svelte +104 -0
- package/src/components/primitives/Divider.svelte +105 -0
- package/src/components/primitives/LazyImage.svelte +104 -0
- package/src/components/primitives/Link.svelte +122 -0
- package/src/components/primitives/Stat.svelte +197 -0
- package/src/components/primitives/StatusBadge.svelte +122 -0
- package/src/index.js +183 -0
- package/src/tokens/colors.css +157 -0
- package/src/tokens/effects.css +128 -0
- package/src/tokens/index.css +81 -0
- package/src/tokens/spacing.css +49 -0
- package/src/tokens/typography.css +79 -0
- package/src/utils/cn.svelte.js +175 -0
- package/src/utils/highlighter.js +124 -0
- package/src/utils/index.js +22 -0
- package/src/utils/navigation.svelte.js +423 -0
- package/src/utils/reactive.svelte.js +328 -0
- package/src/utils/sidebar.svelte.js +211 -0
- package/jera.js +0 -135
- package/www/components/jera/Input/Input.svelte +0 -63
- package/www/components/jera/Input/index.js +0 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax highlighting utility using Shiki v3 with CSS variables theme.
|
|
3
|
+
* Provides cached highlighter instance for performance.
|
|
4
|
+
* Handles the case when shiki is not installed as an optional dependency.
|
|
5
|
+
*
|
|
6
|
+
* @module @miozu/jera/utils/highlighter
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Cache the highlighter to avoid recreating it for each request
|
|
10
|
+
let highlighterPromise = null;
|
|
11
|
+
let shikiAvailable = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default languages supported by the highlighter
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_LANGUAGES = [
|
|
17
|
+
'javascript',
|
|
18
|
+
'typescript',
|
|
19
|
+
'svelte',
|
|
20
|
+
'jsx',
|
|
21
|
+
'tsx',
|
|
22
|
+
'html',
|
|
23
|
+
'css',
|
|
24
|
+
'json',
|
|
25
|
+
'bash',
|
|
26
|
+
'shell',
|
|
27
|
+
'markdown',
|
|
28
|
+
'python',
|
|
29
|
+
'sql',
|
|
30
|
+
'yaml',
|
|
31
|
+
'toml'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if shiki is available
|
|
36
|
+
* @returns {Promise<boolean>}
|
|
37
|
+
*/
|
|
38
|
+
async function isShikiAvailable() {
|
|
39
|
+
if (shikiAvailable !== null) return shikiAvailable;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await import('shiki');
|
|
43
|
+
shikiAvailable = true;
|
|
44
|
+
} catch {
|
|
45
|
+
shikiAvailable = false;
|
|
46
|
+
}
|
|
47
|
+
return shikiAvailable;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get or create the Shiki highlighter with CSS variables theme.
|
|
52
|
+
* The highlighter is cached and reused for performance.
|
|
53
|
+
*
|
|
54
|
+
* @returns {Promise<import('shiki').Highlighter|null>}
|
|
55
|
+
*/
|
|
56
|
+
export async function getHighlighter() {
|
|
57
|
+
if (!await isShikiAvailable()) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!highlighterPromise) {
|
|
62
|
+
highlighterPromise = (async () => {
|
|
63
|
+
const shiki = await import('shiki');
|
|
64
|
+
|
|
65
|
+
// Create a CSS variables theme that uses CSS custom properties
|
|
66
|
+
const cssVarsTheme = shiki.createCssVariablesTheme({
|
|
67
|
+
name: 'css-variables',
|
|
68
|
+
variablePrefix: '--shiki-',
|
|
69
|
+
variableDefaults: {},
|
|
70
|
+
fontStyle: true
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return shiki.createHighlighter({
|
|
74
|
+
themes: [cssVarsTheme],
|
|
75
|
+
langs: DEFAULT_LANGUAGES
|
|
76
|
+
});
|
|
77
|
+
})();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return highlighterPromise;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Highlight code using Shiki with CSS variables theme.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} code - The code to highlight
|
|
87
|
+
* @param {string} [lang='text'] - The language for syntax highlighting
|
|
88
|
+
* @returns {Promise<string>} HTML string with highlighted code
|
|
89
|
+
*/
|
|
90
|
+
export async function highlightCode(code, lang = 'text') {
|
|
91
|
+
try {
|
|
92
|
+
const highlighter = await getHighlighter();
|
|
93
|
+
|
|
94
|
+
if (!highlighter) {
|
|
95
|
+
// Shiki not available, return basic escaped code
|
|
96
|
+
return `<pre class="shiki"><code>${escapeHtml(code)}</code></pre>`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const html = highlighter.codeToHtml(code.trim(), {
|
|
100
|
+
lang: lang || 'text',
|
|
101
|
+
theme: 'css-variables'
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return html;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error('Highlighting error:', err);
|
|
107
|
+
// Fallback to basic escaping
|
|
108
|
+
return `<pre class="shiki"><code>${escapeHtml(code)}</code></pre>`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Escape HTML special characters
|
|
114
|
+
* @param {string} str
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
function escapeHtml(str) {
|
|
118
|
+
return str
|
|
119
|
+
.replace(/&/g, '&')
|
|
120
|
+
.replace(/</g, '<')
|
|
121
|
+
.replace(/>/g, '>')
|
|
122
|
+
.replace(/"/g, '"')
|
|
123
|
+
.replace(/'/g, ''');
|
|
124
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jera Utilities
|
|
3
|
+
*
|
|
4
|
+
* Core utilities for class composition and reactive state management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { cn, rcn, cv, mergeClasses, when, match } from './cn.svelte.js';
|
|
8
|
+
export {
|
|
9
|
+
createReactive,
|
|
10
|
+
createDerived,
|
|
11
|
+
ThemeState,
|
|
12
|
+
getTheme,
|
|
13
|
+
resetTheme,
|
|
14
|
+
createComponentState,
|
|
15
|
+
createIdGenerator,
|
|
16
|
+
generateId
|
|
17
|
+
} from './reactive.svelte.js';
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
createSidebarState,
|
|
21
|
+
SIDEBAR_CONTEXT_KEY
|
|
22
|
+
} from './sidebar.svelte.js';
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavigationState - Enterprise-grade reactive state management for navigation
|
|
3
|
+
*
|
|
4
|
+
* Provides centralized state management for complex navigation scenarios including:
|
|
5
|
+
* - Multi-level recursive navigation
|
|
6
|
+
* - Search and filtering across all items
|
|
7
|
+
* - Expansion state management
|
|
8
|
+
* - Active state detection
|
|
9
|
+
* - Breadcrumb generation
|
|
10
|
+
* - Permission-based visibility
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const nav = new NavigationState({
|
|
14
|
+
* items: navigationConfig,
|
|
15
|
+
* persistKey: 'my-app-nav',
|
|
16
|
+
* searchable: true,
|
|
17
|
+
* recursive: true
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { browser } from '$app/environment';
|
|
22
|
+
|
|
23
|
+
export class NavigationState {
|
|
24
|
+
// Core reactive state
|
|
25
|
+
items = $state([]);
|
|
26
|
+
searchQuery = $state('');
|
|
27
|
+
expandedSections = $state({});
|
|
28
|
+
expandedItems = $state({});
|
|
29
|
+
activeItem = $state(null);
|
|
30
|
+
activePath = $state([]);
|
|
31
|
+
|
|
32
|
+
// Configuration
|
|
33
|
+
config = $state({
|
|
34
|
+
persistKey: null,
|
|
35
|
+
searchable: true,
|
|
36
|
+
recursive: true,
|
|
37
|
+
maxDepth: 10,
|
|
38
|
+
autoExpand: false,
|
|
39
|
+
caseSensitiveSearch: false
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Computed properties
|
|
43
|
+
filteredItems = $derived.by(() => this.#filterItems(this.items, this.searchQuery));
|
|
44
|
+
breadcrumbs = $derived.by(() => this.#generateBreadcrumbs(this.activeItem));
|
|
45
|
+
totalItemCount = $derived.by(() => this.#countItems(this.items));
|
|
46
|
+
visibleItemCount = $derived.by(() => this.#countItems(this.filteredItems));
|
|
47
|
+
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
// Apply configuration
|
|
50
|
+
this.config = { ...this.config, ...options };
|
|
51
|
+
|
|
52
|
+
if (options.items) {
|
|
53
|
+
this.items = this.#normalizeItems(options.items);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Load persisted state
|
|
57
|
+
if (this.config.persistKey && browser) {
|
|
58
|
+
this.#loadPersistedState();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Auto-save state when it changes
|
|
62
|
+
if (this.config.persistKey && browser) {
|
|
63
|
+
$effect(() => {
|
|
64
|
+
this.#savePersistedState();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Update navigation items with intelligent merging
|
|
71
|
+
*/
|
|
72
|
+
setItems(items) {
|
|
73
|
+
this.items = this.#normalizeItems(items);
|
|
74
|
+
|
|
75
|
+
// Auto-expand if configured
|
|
76
|
+
if (this.config.autoExpand) {
|
|
77
|
+
this.#autoExpandItems();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set active item by ID or path
|
|
83
|
+
*/
|
|
84
|
+
setActive(identifier, shouldExpand = true) {
|
|
85
|
+
const item = typeof identifier === 'string'
|
|
86
|
+
? this.#findItemById(identifier)
|
|
87
|
+
: identifier;
|
|
88
|
+
|
|
89
|
+
if (item) {
|
|
90
|
+
this.activeItem = item;
|
|
91
|
+
this.activePath = this.#getItemPath(item);
|
|
92
|
+
|
|
93
|
+
if (shouldExpand) {
|
|
94
|
+
this.#expandToItem(item);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Search navigation items
|
|
101
|
+
*/
|
|
102
|
+
search(query) {
|
|
103
|
+
this.searchQuery = query;
|
|
104
|
+
|
|
105
|
+
// Auto-expand search results
|
|
106
|
+
if (query && this.config.autoExpand) {
|
|
107
|
+
this.#expandSearchResults();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clear search and collapse expanded results
|
|
113
|
+
*/
|
|
114
|
+
clearSearch() {
|
|
115
|
+
this.searchQuery = '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Toggle section expansion
|
|
120
|
+
*/
|
|
121
|
+
toggleSection(sectionId) {
|
|
122
|
+
this.expandedSections[sectionId] = !this.expandedSections[sectionId];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Toggle item expansion (for recursive items)
|
|
127
|
+
*/
|
|
128
|
+
toggleItem(itemId) {
|
|
129
|
+
this.expandedItems[itemId] = !this.expandedItems[itemId];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Expand all sections and items
|
|
134
|
+
*/
|
|
135
|
+
expandAll() {
|
|
136
|
+
this.#walkItems(this.items, (item) => {
|
|
137
|
+
if (item.children?.length > 0) {
|
|
138
|
+
this.expandedItems[item.id] = true;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Expand all sections
|
|
143
|
+
Object.keys(this.expandedSections).forEach(key => {
|
|
144
|
+
this.expandedSections[key] = true;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Collapse all sections and items
|
|
150
|
+
*/
|
|
151
|
+
collapseAll() {
|
|
152
|
+
this.expandedItems = {};
|
|
153
|
+
Object.keys(this.expandedSections).forEach(key => {
|
|
154
|
+
this.expandedSections[key] = false;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get navigation item by ID
|
|
160
|
+
*/
|
|
161
|
+
getItem(id) {
|
|
162
|
+
return this.#findItemById(id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get all children of an item
|
|
167
|
+
*/
|
|
168
|
+
getChildren(itemId) {
|
|
169
|
+
const item = this.#findItemById(itemId);
|
|
170
|
+
return item?.children || [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get parent of an item
|
|
175
|
+
*/
|
|
176
|
+
getParent(itemId) {
|
|
177
|
+
return this.#findParentItem(itemId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get path to root for an item
|
|
182
|
+
*/
|
|
183
|
+
getPath(itemId) {
|
|
184
|
+
const item = this.#findItemById(itemId);
|
|
185
|
+
return item ? this.#getItemPath(item) : [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if item is visible based on permissions
|
|
190
|
+
*/
|
|
191
|
+
isVisible(item) {
|
|
192
|
+
if (!item.permissions) return true;
|
|
193
|
+
|
|
194
|
+
// Override in subclass or provide permissions checker
|
|
195
|
+
return this.config.permissionChecker
|
|
196
|
+
? this.config.permissionChecker(item.permissions)
|
|
197
|
+
: true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if item is active
|
|
202
|
+
*/
|
|
203
|
+
isActive(item) {
|
|
204
|
+
return this.activeItem?.id === item.id;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if item is in active path
|
|
209
|
+
*/
|
|
210
|
+
isInActivePath(item) {
|
|
211
|
+
return this.activePath.some(pathItem => pathItem.id === item.id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if section is expanded
|
|
216
|
+
*/
|
|
217
|
+
isSectionExpanded(sectionId) {
|
|
218
|
+
return this.expandedSections[sectionId] ?? false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if item is expanded
|
|
223
|
+
*/
|
|
224
|
+
isItemExpanded(itemId) {
|
|
225
|
+
return this.expandedItems[itemId] ?? false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Private methods
|
|
229
|
+
#normalizeItems(items) {
|
|
230
|
+
const normalized = [];
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < items.length; i++) {
|
|
233
|
+
const item = items[i];
|
|
234
|
+
const normalizedItem = {
|
|
235
|
+
id: item.id || `item-${i}`,
|
|
236
|
+
label: item.label || '',
|
|
237
|
+
href: item.href || null,
|
|
238
|
+
icon: item.icon || null,
|
|
239
|
+
badge: item.badge || null,
|
|
240
|
+
children: item.children ? this.#normalizeItems(item.children) : [],
|
|
241
|
+
permissions: item.permissions || null,
|
|
242
|
+
metadata: item.metadata || {},
|
|
243
|
+
depth: item.depth || 0,
|
|
244
|
+
parent: item.parent || null,
|
|
245
|
+
...item
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Set parent references for children
|
|
249
|
+
normalizedItem.children.forEach(child => {
|
|
250
|
+
child.parent = normalizedItem.id;
|
|
251
|
+
child.depth = normalizedItem.depth + 1;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
normalized.push(normalizedItem);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return normalized;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#filterItems(items, query) {
|
|
261
|
+
if (!query) return items;
|
|
262
|
+
|
|
263
|
+
const filtered = [];
|
|
264
|
+
const searchLower = this.config.caseSensitiveSearch ? query : query.toLowerCase();
|
|
265
|
+
|
|
266
|
+
for (const item of items) {
|
|
267
|
+
const matches = this.#itemMatches(item, searchLower);
|
|
268
|
+
const filteredChildren = this.#filterItems(item.children || [], query);
|
|
269
|
+
|
|
270
|
+
if (matches || filteredChildren.length > 0) {
|
|
271
|
+
filtered.push({
|
|
272
|
+
...item,
|
|
273
|
+
children: filteredChildren,
|
|
274
|
+
_matchesSearch: matches
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return filtered;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#itemMatches(item, searchQuery) {
|
|
283
|
+
const searchFields = [
|
|
284
|
+
item.label,
|
|
285
|
+
item.description,
|
|
286
|
+
item.metadata?.tags?.join(' '),
|
|
287
|
+
item.metadata?.keywords?.join(' ')
|
|
288
|
+
].filter(Boolean);
|
|
289
|
+
|
|
290
|
+
const searchText = this.config.caseSensitiveSearch
|
|
291
|
+
? searchFields.join(' ')
|
|
292
|
+
: searchFields.join(' ').toLowerCase();
|
|
293
|
+
|
|
294
|
+
return searchText.includes(searchQuery);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#findItemById(id, items = this.items) {
|
|
298
|
+
for (const item of items) {
|
|
299
|
+
if (item.id === id) return item;
|
|
300
|
+
|
|
301
|
+
if (item.children) {
|
|
302
|
+
const found = this.#findItemById(id, item.children);
|
|
303
|
+
if (found) return found;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#findParentItem(itemId, items = this.items, parent = null) {
|
|
310
|
+
for (const item of items) {
|
|
311
|
+
if (item.id === itemId) return parent;
|
|
312
|
+
|
|
313
|
+
if (item.children) {
|
|
314
|
+
const found = this.#findParentItem(itemId, item.children, item);
|
|
315
|
+
if (found) return found;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#getItemPath(item) {
|
|
322
|
+
const path = [];
|
|
323
|
+
let current = item;
|
|
324
|
+
|
|
325
|
+
while (current) {
|
|
326
|
+
path.unshift(current);
|
|
327
|
+
current = current.parent ? this.#findItemById(current.parent) : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return path;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#expandToItem(item) {
|
|
334
|
+
const path = this.#getItemPath(item);
|
|
335
|
+
|
|
336
|
+
path.forEach(pathItem => {
|
|
337
|
+
if (pathItem.parent) {
|
|
338
|
+
this.expandedItems[pathItem.parent] = true;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#expandSearchResults() {
|
|
344
|
+
this.#walkItems(this.filteredItems, (item) => {
|
|
345
|
+
if (item._matchesSearch && item.parent) {
|
|
346
|
+
this.expandedItems[item.parent] = true;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
#autoExpandItems() {
|
|
352
|
+
this.#walkItems(this.items, (item) => {
|
|
353
|
+
if (item.children?.length > 0) {
|
|
354
|
+
this.expandedItems[item.id] = true;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#walkItems(items, callback) {
|
|
360
|
+
for (const item of items) {
|
|
361
|
+
callback(item);
|
|
362
|
+
if (item.children) {
|
|
363
|
+
this.#walkItems(item.children, callback);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#countItems(items) {
|
|
369
|
+
let count = 0;
|
|
370
|
+
this.#walkItems(items, () => count++);
|
|
371
|
+
return count;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#generateBreadcrumbs(activeItem) {
|
|
375
|
+
if (!activeItem) return [];
|
|
376
|
+
return this.#getItemPath(activeItem);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#loadPersistedState() {
|
|
380
|
+
try {
|
|
381
|
+
const saved = localStorage.getItem(this.config.persistKey);
|
|
382
|
+
if (saved) {
|
|
383
|
+
const state = JSON.parse(saved);
|
|
384
|
+
this.expandedSections = state.expandedSections || {};
|
|
385
|
+
this.expandedItems = state.expandedItems || {};
|
|
386
|
+
this.searchQuery = state.searchQuery || '';
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.warn('Failed to load navigation state:', error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#savePersistedState() {
|
|
394
|
+
try {
|
|
395
|
+
const state = {
|
|
396
|
+
expandedSections: this.expandedSections,
|
|
397
|
+
expandedItems: this.expandedItems,
|
|
398
|
+
searchQuery: this.searchQuery
|
|
399
|
+
};
|
|
400
|
+
localStorage.setItem(this.config.persistKey, JSON.stringify(state));
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.warn('Failed to save navigation state:', error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Create a navigation state instance with common defaults
|
|
409
|
+
*/
|
|
410
|
+
export function createNavigationState(options = {}) {
|
|
411
|
+
return new NavigationState({
|
|
412
|
+
searchable: true,
|
|
413
|
+
recursive: true,
|
|
414
|
+
maxDepth: 10,
|
|
415
|
+
autoExpand: false,
|
|
416
|
+
...options
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Navigation context key for Svelte context
|
|
422
|
+
*/
|
|
423
|
+
export const NAVIGATION_CONTEXT_KEY = 'navigation-state';
|