@hkdigital/lib-core 0.4.3 → 0.4.5

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.
@@ -110,17 +110,18 @@ export function throwRethrowChainError() {
110
110
  }
111
111
 
112
112
  function level2Function() {
113
- try {
114
- level3Function();
115
- } catch (e) {
116
- rethrow('Error in level2Function', e);
117
- }
118
- }
119
-
120
- function level3Function() {
121
113
  throw new Error('Original error at deepest level');
114
+ // try {
115
+ // level3Function();
116
+ // } catch (e) {
117
+ // rethrow('Error in level2Function', e);
118
+ // }
122
119
  }
123
120
 
121
+ // function level3Function() {
122
+ // throw new Error('Original error at deepest level');
123
+ // }
124
+
124
125
  level1Function();
125
126
  }
126
127
 
@@ -134,4 +135,4 @@ export function throwRawValibotError() {
134
135
 
135
136
  // This will throw a ValiError directly from our valibotParser wrapper
136
137
  return v.parse(schema, invalidValue);
137
- }
138
+ }
@@ -0,0 +1,300 @@
1
+ <script>
2
+ import { goto } from '$app/navigation';
3
+
4
+ /**
5
+ * Reusable explorer component for navigating nested folder structures
6
+ * @type {{
7
+ * navigationData: Object,
8
+ * currentPath: string,
9
+ * getActiveSegments?: (segments: string[]) => void,
10
+ * getNavigateToLevelFunction?: (fn: Function) => void,
11
+ * rootName?: string,
12
+ * folderName?: string
13
+ * }}
14
+ */
15
+ let {
16
+ navigationData,
17
+ currentPath = '',
18
+ getActiveSegments,
19
+ getNavigateToLevelFunction,
20
+ rootName = 'Categories',
21
+ folderName
22
+ } = $props();
23
+
24
+ /** @type {string[]} */
25
+ let pathSegments = $derived(
26
+ currentPath ? currentPath.split('/').filter(Boolean) : []
27
+ );
28
+
29
+ /** @type {string[]} */
30
+ let interactiveSegments = $state([]);
31
+
32
+ /** @type {string[]} */
33
+ let activeSegments = $derived(
34
+ interactiveSegments.length > 0 ? interactiveSegments : pathSegments
35
+ );
36
+
37
+ // Notify parent of active segments changes - only when they change
38
+ let lastActiveSegments = [];
39
+ $effect(() => {
40
+ if (
41
+ getActiveSegments &&
42
+ JSON.stringify(activeSegments) !== JSON.stringify(lastActiveSegments)
43
+ ) {
44
+ lastActiveSegments = [...activeSegments];
45
+ getActiveSegments(activeSegments);
46
+ }
47
+ });
48
+
49
+ /** @type {Object[]} */
50
+ let breadcrumbColumns = $derived.by(() => {
51
+ const columns = [];
52
+ let current = navigationData;
53
+
54
+ // Root column with sorted items (folders first, then endpoints)
55
+ const rootItems = Object.values(current).map((item) => ({
56
+ name: item.name,
57
+ displayName: item.displayName || item.name,
58
+ isEndpoint: item.isEndpoint,
59
+ isSelected: activeSegments.length > 0 &&
60
+ activeSegments[0] === item.name
61
+ })).sort((a, b) => {
62
+ // Sort by type first (folders before endpoints)
63
+ if (a.isEndpoint !== b.isEndpoint) {
64
+ return a.isEndpoint ? 1 : -1;
65
+ }
66
+ // Then sort alphabetically by display name
67
+ return a.displayName.localeCompare(b.displayName);
68
+ });
69
+
70
+ columns.push({
71
+ title: rootName,
72
+ items: rootItems,
73
+ level: 0
74
+ });
75
+
76
+ // Build columns for each path segment
77
+ for (let i = 0; i < activeSegments.length; i++) {
78
+ const segment = activeSegments[i];
79
+
80
+ if (current[segment]) {
81
+ current = current[segment].children;
82
+
83
+ if (Object.keys(current).length > 0) {
84
+ const nextSegment = activeSegments[i + 1];
85
+
86
+ // Sort nested items (folders first, then endpoints)
87
+ const nestedItems = Object.values(current).map((item) => ({
88
+ name: item.name,
89
+ displayName: item.displayName || item.name,
90
+ isEndpoint: item.isEndpoint,
91
+ isSelected: nextSegment === item.name
92
+ })).sort((a, b) => {
93
+ // Sort by type first (folders before endpoints)
94
+ if (a.isEndpoint !== b.isEndpoint) {
95
+ return a.isEndpoint ? 1 : -1;
96
+ }
97
+ // Then sort alphabetically by display name
98
+ return a.displayName.localeCompare(b.displayName);
99
+ });
100
+
101
+ columns.push({
102
+ title: segment,
103
+ items: nestedItems,
104
+ level: i + 1
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ return columns;
111
+ });
112
+
113
+ /**
114
+ * Handle navigation to a folder or endpoint
115
+ * @param {number} level - Column level
116
+ * @param {string} itemName - Item name
117
+ * @param {boolean} isEndpoint - Whether this is a final endpoint
118
+ */
119
+ function handleNavigation(level, itemName, isEndpoint) {
120
+ // Build new path segments up to the selected level
121
+ const newSegments = [...activeSegments.slice(0, level), itemName];
122
+ const fullPath = newSegments.join('/');
123
+
124
+ // Always navigate to explorer URL - let the route system handle state
125
+ goto(`/explorer/${folderName}/${fullPath}`);
126
+ }
127
+
128
+ /**
129
+ * Handle breadcrumb navigation
130
+ * @param {number} level - Level to navigate back to
131
+ */
132
+ function navigateToLevel(level) {
133
+ if (level === 0) {
134
+ // Navigate back to root explorer
135
+ goto('/explorer');
136
+ } else {
137
+ // Navigate to specific level
138
+ const currentSegments =
139
+ interactiveSegments.length > 0 ? interactiveSegments : pathSegments;
140
+ const newSegments = currentSegments.slice(0, level);
141
+ const explorerPath = newSegments.join('/');
142
+ goto(`/explorer/${folderName}/${explorerPath}`);
143
+ }
144
+ }
145
+
146
+ // Expose the navigate function to parent
147
+ if (getNavigateToLevelFunction) {
148
+ getNavigateToLevelFunction(navigateToLevel);
149
+ }
150
+ </script>
151
+
152
+ <div class="explorer-container">
153
+ <!-- Dynamic columns -->
154
+ <div class="navigation-columns">
155
+ {#each breadcrumbColumns as column, columnIndex}
156
+ <div class="column" data-column={`level-${column.level}`}>
157
+ <h2 class="type-heading-h2 mb-20up">{column.title}</h2>
158
+ <nav class="folder-list">
159
+ {#each column.items as item}
160
+ <button
161
+ class="folder-item"
162
+ class:active={item.isSelected}
163
+ class:endpoint={item.isEndpoint}
164
+ onclick={() =>
165
+ handleNavigation(column.level, item.name, item.isEndpoint)}
166
+ >
167
+ <div class="item-content">
168
+ <div class="item-icon">
169
+ {#if item.isEndpoint}
170
+ <svg
171
+ class="external-icon"
172
+ viewBox="0 0 16 16"
173
+ fill="currentColor"
174
+ aria-hidden="true"
175
+ >
176
+ <path d="M3.5 2A1.5 1.5 0 002 3.5v9A1.5 1.5 0 003.5 14h9a1.5 1.5 0 001.5-1.5V9.75a.75.75 0 00-1.5 0v2.75a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25H6.5a.75.75 0 000-1.5h-3z"/>
177
+ <path d="M15.25 1a.75.75 0 00-.75-.75h-4.5a.75.75 0 000 1.5h2.69l-6.22 6.22a.75.75 0 101.06 1.06L13.75 3.31v2.69a.75.75 0 001.5 0V1z"/>
178
+ </svg>
179
+ {:else}
180
+ <svg
181
+ class="folder-icon"
182
+ viewBox="0 0 16 16"
183
+ fill="currentColor"
184
+ aria-hidden="true"
185
+ >
186
+ <path d="M1.75 2A1.75 1.75 0 000 3.75v8.5C0 13.216.784 14 1.75 14h12.5A1.75 1.75 0 0016 12.25v-7.5A1.75 1.75 0 0014.25 3H7.5L6.25 1.75A.75.75 0 005.75 1.5h-4A1.75 1.75 0 000 3.25V3.75z"/>
187
+ </svg>
188
+ {/if}
189
+ </div>
190
+ <span class="item-name">{item.displayName}</span>
191
+ </div>
192
+ </button>
193
+ {/each}
194
+
195
+ {#if column.items.length === 0}
196
+ <div class="empty-state">
197
+ <p class="type-base-sm">No items found</p>
198
+ </div>
199
+ {/if}
200
+ </nav>
201
+ </div>
202
+ {/each}
203
+ </div>
204
+ </div>
205
+
206
+ <style>
207
+ .explorer-container {
208
+ padding: 20px;
209
+ }
210
+
211
+ .navigation-columns {
212
+ display: grid;
213
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
214
+ gap: 30px;
215
+ align-items: start;
216
+ }
217
+
218
+ .column {
219
+ min-height: 200px;
220
+ }
221
+
222
+ .folder-list {
223
+ display: flex;
224
+ flex-direction: column;
225
+ gap: 4px;
226
+ }
227
+
228
+ .folder-item {
229
+ display: block;
230
+ width: 100%;
231
+ padding: 12px 16px;
232
+ background: none;
233
+ border: none;
234
+ text-align: left;
235
+ cursor: pointer;
236
+ color: var(--color-surface-700);
237
+ transition: all 0.2s;
238
+ font-size: 14px;
239
+ line-height: 1.4;
240
+ }
241
+
242
+ .folder-item:hover {
243
+ background-color: var(--color-surface-100);
244
+ }
245
+
246
+ .folder-item.active {
247
+ background-color: var(--color-primary-100);
248
+ color: var(--color-primary-700);
249
+ }
250
+
251
+ .folder-item.endpoint:hover {
252
+ background-color: var(--color-primary-50);
253
+ color: var(--color-primary-600);
254
+ }
255
+
256
+ .item-content {
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 8px;
260
+ }
261
+
262
+ .item-icon {
263
+ display: flex;
264
+ align-items: center;
265
+ width: 14px;
266
+ height: 14px;
267
+ flex-shrink: 0;
268
+ }
269
+
270
+ .item-name {
271
+ flex: 1;
272
+ }
273
+
274
+ .external-icon,
275
+ .folder-icon {
276
+ width: 14px;
277
+ height: 14px;
278
+ opacity: 0.6;
279
+ transition: opacity 0.2s;
280
+ }
281
+
282
+ .folder-item:hover .external-icon,
283
+ .folder-item:hover .folder-icon {
284
+ opacity: 0.8;
285
+ }
286
+
287
+ .folder-item.endpoint .external-icon {
288
+ color: var(--color-primary-600);
289
+ }
290
+
291
+ .folder-item .folder-icon {
292
+ color: var(--color-surface-500);
293
+ }
294
+
295
+ .empty-state {
296
+ padding: 20px;
297
+ text-align: center;
298
+ color: var(--color-surface-500);
299
+ }
300
+ </style>
@@ -0,0 +1,20 @@
1
+ export default Explorer;
2
+ type Explorer = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<{
5
+ navigationData: any;
6
+ currentPath: string;
7
+ getActiveSegments?: (segments: string[]) => void;
8
+ getNavigateToLevelFunction?: (fn: Function) => void;
9
+ rootName?: string;
10
+ folderName?: string;
11
+ }>): void;
12
+ };
13
+ declare const Explorer: import("svelte").Component<{
14
+ navigationData: any;
15
+ currentPath: string;
16
+ getActiveSegments?: (segments: string[]) => void;
17
+ getNavigateToLevelFunction?: (fn: Function) => void;
18
+ rootName?: string;
19
+ folderName?: string;
20
+ }, {}, "">;
@@ -0,0 +1,82 @@
1
+ <script>
2
+ import { enhance } from '$app/forms';
3
+
4
+ import { Panel } from '../../primitives.js';
5
+
6
+ /**
7
+ * @type {{
8
+ * scalingEnabled: boolean,
9
+ * onchange?: (enabled: boolean) => void,
10
+ * crumblePath?: import('svelte').Snippet
11
+ * }}
12
+ */
13
+ let { scalingEnabled = $bindable(), onchange, crumblePath } = $props();
14
+ </script>
15
+
16
+ <div class="examples-top-bar" data-component="top-bar">
17
+ <div class="container">
18
+ {#if crumblePath}
19
+ <div class="crumble-path">
20
+ {@render crumblePath()}
21
+ </div>
22
+ {/if}
23
+
24
+ <form
25
+ method="POST"
26
+ action="?/toggleScaling"
27
+ use:enhance={({ formData, cancel }) => {
28
+ const newValue = formData.get('scalingEnabled') === 'on';
29
+ scalingEnabled = newValue;
30
+ onchange?.(newValue);
31
+
32
+ return async ({ result }) => {
33
+ if (result.type === 'success') {
34
+ // Force page reload to apply scaling changes
35
+ window.location.reload();
36
+ }
37
+ };
38
+ }}
39
+ >
40
+ <label class="scaling-toggle">
41
+ <input
42
+ type="checkbox"
43
+ name="scalingEnabled"
44
+ bind:checked={scalingEnabled}
45
+ onchange={(e) => /** @type {HTMLInputElement} */ (e.target).form.requestSubmit()}
46
+ />
47
+ <span class="type-ui-sm">Enable Design Scaling</span>
48
+ </label>
49
+ </form>
50
+ </div>
51
+ </div>
52
+
53
+ <style src="./topbar.css">@reference '../../../../app.css';
54
+
55
+ [data-component="top-bar"] {
56
+ @apply bg-surface-100 border-surface-300 sticky top-0 z-10;
57
+ @apply p-8ht;
58
+
59
+ & .container {
60
+ @apply max-w-screen-xl mx-auto flex justify-between items-center;
61
+ }
62
+
63
+ & .scaling-toggle {
64
+ @apply flex items-center cursor-pointer;
65
+ @apply gap-8bt;
66
+
67
+ & input[type="checkbox"] {
68
+ @apply accent-primary-500;
69
+ }
70
+ }
71
+
72
+ & .crumble-path {
73
+ @apply flex items-center;
74
+ }
75
+ }
76
+
77
+ /* Dark mode support */
78
+ @media (prefers-color-scheme: dark) {
79
+ [data-component="top-bar"] {
80
+ @apply bg-surface-800 border-surface-600;
81
+ }
82
+ }</style>
@@ -0,0 +1,14 @@
1
+ export default TopBar;
2
+ type TopBar = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<{
5
+ scalingEnabled: boolean;
6
+ onchange?: (enabled: boolean) => void;
7
+ crumblePath?: Snippet<[]>;
8
+ }>): void;
9
+ };
10
+ declare const TopBar: import("svelte").Component<{
11
+ scalingEnabled: boolean;
12
+ onchange?: (enabled: boolean) => void;
13
+ crumblePath?: import("svelte").Snippet;
14
+ }, {}, "scalingEnabled">;
@@ -0,0 +1,2 @@
1
+ export { default as Explorer } from "./Explorer.svelte";
2
+ export { default as TopBar } from "./TopBar.svelte";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Dev explorer components
3
+ * @fileoverview Reusable components for navigating nested folder
4
+ * structures with dynamic routing support
5
+ */
6
+
7
+ export { default as Explorer } from './Explorer.svelte';
8
+ export { default as TopBar } from './TopBar.svelte';
@@ -0,0 +1,30 @@
1
+ @reference '../../../../app.css';
2
+
3
+ [data-component="top-bar"] {
4
+ @apply bg-surface-100 border-surface-300 sticky top-0 z-10;
5
+ @apply p-8ht;
6
+
7
+ & .container {
8
+ @apply max-w-screen-xl mx-auto flex justify-between items-center;
9
+ }
10
+
11
+ & .scaling-toggle {
12
+ @apply flex items-center cursor-pointer;
13
+ @apply gap-8bt;
14
+
15
+ & input[type="checkbox"] {
16
+ @apply accent-primary-500;
17
+ }
18
+ }
19
+
20
+ & .crumble-path {
21
+ @apply flex items-center;
22
+ }
23
+ }
24
+
25
+ /* Dark mode support */
26
+ @media (prefers-color-scheme: dark) {
27
+ [data-component="top-bar"] {
28
+ @apply bg-surface-800 border-surface-600;
29
+ }
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"