@hkdigital/lib-core 0.4.4 → 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.
- package/dist/ui/dev/explorer/Explorer.svelte +300 -0
- package/dist/ui/dev/explorer/Explorer.svelte.d.ts +20 -0
- package/dist/ui/dev/explorer/TopBar.svelte +82 -0
- package/dist/ui/dev/explorer/TopBar.svelte.d.ts +14 -0
- package/dist/ui/dev/explorer/index.d.ts +2 -0
- package/dist/ui/dev/explorer/index.js +8 -0
- package/dist/ui/dev/explorer/topbar.css +30 -0
- package/package.json +1 -1
|
@@ -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,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
|
+
}
|