Skip to content

Commit de074cb

Browse files
committed
feat: implement Foldable component and update preview_tiles page layout
1 parent 111242a commit de074cb

3 files changed

Lines changed: 202 additions & 48 deletions

File tree

src/lib/component/Foldable.svelte

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<script lang="ts">
2+
import { type Snippet } from 'svelte';
3+
import { slide } from 'svelte/transition';
4+
5+
let {
6+
title,
7+
children,
8+
open,
9+
disabled
10+
}: {
11+
title: string;
12+
children?: Snippet;
13+
open?: boolean;
14+
disabled?: boolean;
15+
} = $props();
16+
const iconOpen: string = '';
17+
const iconClosed: string = '';
18+
19+
const contentId = `foldable-${crypto.getRandomValues(new Uint32Array(1))[0].toString(36)}`;
20+
21+
function toggle() {
22+
if (disabled) return;
23+
open = !open;
24+
}
25+
</script>
26+
27+
<div class="foldable">
28+
<button
29+
type="button"
30+
class="foldable__header"
31+
aria-expanded={open}
32+
aria-controls={contentId}
33+
onclick={toggle}
34+
{disabled}
35+
>
36+
<span class="foldable__icon" aria-hidden="true">{open ? iconOpen : iconClosed}</span>
37+
<span class="foldable__title">{title}</span>
38+
</button>
39+
40+
{#if open}
41+
<div class="foldable__content" id={contentId} role="region" aria-label={title} transition:slide>
42+
{@render children?.()}
43+
</div>
44+
{/if}
45+
</div>
46+
47+
<style>
48+
.foldable {
49+
--foldable-border: var(--color-border, #ddd);
50+
--foldable-bg: var(--color-bg, #f8f9fa);
51+
--foldable-header-bg: var(--color-header-bg, #eee);
52+
--foldable-radius: 4px;
53+
margin: 0.5rem 0.5rem 1rem 0.5rem;
54+
box-sizing: border-box;
55+
}
56+
57+
.foldable__header {
58+
all: unset;
59+
display: flex;
60+
align-items: center;
61+
gap: 0.4rem;
62+
width: 100%;
63+
padding: 0.3rem 0.3rem;
64+
cursor: pointer;
65+
background: var(--foldable-header-bg);
66+
border: 1px solid var(--foldable-border);
67+
border-radius: var(--foldable-radius);
68+
font-size: 0.9rem;
69+
line-height: 1.2;
70+
user-select: none;
71+
box-sizing: border-box;
72+
}
73+
.foldable__header:focus-visible {
74+
outline: 2px solid #2684ff;
75+
outline-offset: 2px;
76+
}
77+
.foldable__header:disabled {
78+
opacity: 0.5;
79+
cursor: default;
80+
}
81+
82+
.foldable__icon {
83+
width: 1em;
84+
display: inline-flex;
85+
justify-content: center;
86+
transition: transform 0.2s;
87+
}
88+
89+
.foldable__title {
90+
flex: 1;
91+
font-weight: 500;
92+
white-space: nowrap;
93+
overflow: hidden;
94+
text-overflow: ellipsis;
95+
}
96+
97+
.foldable__content {
98+
padding: 0.5rem 0.75rem 0.75rem;
99+
border: 1px solid var(--foldable-border);
100+
border-top: none;
101+
border-radius: 0 0 var(--foldable-radius) var(--foldable-radius);
102+
background: var(--foldable-bg);
103+
font-size: 0.85rem;
104+
}
105+
</style>

src/lib/component/Map.svelte

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,6 @@
4444
let style = backgroundStyle;
4545
let overlayLayerIds: string[] = [];
4646
47-
if (overlay) {
48-
const source = await getTileSource(overlay);
49-
const overlayStyle = source.getStyle();
50-
overlayLayerIds = overlayStyle.layers?.map((layer) => layer.id) ?? [];
51-
style = overlayStyles(style, overlayStyle);
52-
bounds = source.getBounds() ?? bounds;
53-
}
54-
5547
map = new maplibre.Map({
5648
container,
5749
style,
@@ -73,34 +65,51 @@
7365
});
7466
7567
canvas = map.getCanvas();
76-
77-
if (inspectOverlay && overlayLayerIds.length > 0) {
78-
map.on('mousemove', overlayLayerIds, (e) => {
79-
const properties = (e.features ?? []).map((feature) => {
80-
const props: PropertyEntry[] = [];
81-
for (const key in feature.properties) {
82-
props.push({ name: key, value: String(feature.properties[key]) });
83-
}
84-
props.sort((a, b) => a.name.localeCompare(b.name));
85-
return props;
86-
});
87-
if (JSON.stringify(properties) !== JSON.stringify(selectedProperties)) {
88-
selectedProperties = properties;
89-
}
90-
canvas!.style.cursor = 'pointer';
91-
});
92-
map.on('mouseleave', overlayLayerIds, () => {
93-
selectedProperties = [];
94-
canvas!.style.cursor = '';
95-
});
96-
}
9768
});
9869
9970
// cleanup (runs automatically when dependencies change or component unmounts)
10071
onDestroy(() => {
10172
map?.remove();
10273
map = null;
10374
});
75+
76+
$effect(() => {
77+
if (!map) return;
78+
let style = backgroundStyle;
79+
let overlayLayerIds: string[] = [];
80+
81+
if (overlay) {
82+
getTileSource(overlay).then((source) => {
83+
const overlayStyle = source.getStyle();
84+
overlayLayerIds = overlayStyle.layers?.map((layer) => layer.id) ?? [];
85+
style = overlayStyles(backgroundStyle, overlayStyle);
86+
map!.setStyle(style);
87+
88+
if (inspectOverlay && overlayLayerIds.length > 0) {
89+
map!.on('mousemove', overlayLayerIds, (e) => {
90+
const properties = (e.features ?? []).map((feature) => {
91+
const props: PropertyEntry[] = [];
92+
for (const key in feature.properties) {
93+
props.push({ name: key, value: String(feature.properties[key]) });
94+
}
95+
props.sort((a, b) => a.name.localeCompare(b.name));
96+
return props;
97+
});
98+
if (JSON.stringify(properties) !== JSON.stringify(selectedProperties)) {
99+
selectedProperties = properties;
100+
}
101+
canvas!.style.cursor = 'pointer';
102+
});
103+
map!.on('mouseleave', overlayLayerIds, () => {
104+
selectedProperties = [];
105+
canvas!.style.cursor = '';
106+
});
107+
}
108+
});
109+
} else {
110+
map.setStyle(style);
111+
}
112+
});
104113
</script>
105114

106115
<div bind:this={container} class="map-container"></div>
Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,72 @@
11
<script lang="ts">
22
import type { TilesInitRequest } from '$lib/api/types';
33
import FileSelector from '$lib/component/FileSelector.svelte';
4+
import Foldable from '$lib/component/Foldable.svelte';
45
import Map from '$lib/component/Map.svelte';
56
import type { InferOutput } from 'valibot';
67
7-
let showModal = $state(true);
8+
let showModal = $state(false);
89
let file: string | undefined = $state(undefined);
910
let overlay: InferOutput<typeof TilesInitRequest> | undefined = $state(undefined);
1011
1112
$effect(() => {
12-
if (file) {
13-
showModal = false;
14-
overlay = { input: file };
15-
} else {
16-
showModal = true;
17-
overlay = undefined;
18-
}
13+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
14+
file;
15+
updateOverlay();
1916
});
17+
18+
function updateOverlay() {
19+
overlay = getOverlay();
20+
function getOverlay(): InferOutput<typeof TilesInitRequest> | undefined {
21+
if (file) {
22+
return { input: file };
23+
}
24+
return undefined;
25+
}
26+
}
2027
</script>
2128

22-
{#if !file}
23-
<h3>Select a tiles container:</h3>
24-
<button onclick={() => (showModal = true)}>Open File Selector</button>
25-
<FileSelector
26-
bind:showModal
27-
bind:file
28-
fileFilter={(name) => /\.(versa|mb|pm)tiles$/.test(name)}
29-
/>
30-
{:else}
31-
<Map backgroundMap="GrayBright" {overlay} inspectOverlay={true}></Map>
32-
{/if}
29+
<div class="wrapper">
30+
<div class="sidebar">
31+
<Foldable title="Selected File" open={true}>
32+
{#if file}
33+
<p>{file}</p>
34+
{:else}
35+
<p>No file selected.</p>
36+
{/if}
37+
38+
<button onclick={() => (showModal = true)}>Open File Selector</button>
39+
<FileSelector
40+
bind:showModal
41+
bind:file
42+
fileFilter={(name) => /\.(versa|mb|pm)tiles$/.test(name)}
43+
/>
44+
</Foldable>
45+
<Foldable title="Add data"></Foldable>
46+
<Foldable title="Filter layers"></Foldable>
47+
<Foldable title="Filter properties"></Foldable>
48+
<Foldable title="Filter area"></Foldable>
49+
<Foldable title="Add Metadata"></Foldable>
50+
</div>
51+
<div class="map-container">
52+
<Map backgroundMap="GrayBright" {overlay} inspectOverlay={true}></Map>
53+
</div>
54+
</div>
55+
56+
<style>
57+
.wrapper {
58+
display: flex;
59+
height: 100%;
60+
}
61+
.sidebar {
62+
width: 300px;
63+
box-sizing: border-box;
64+
border-right: 1px solid var(--color-border, #ddd);
65+
background-color: var(--color-bg, #f8f9fa);
66+
overflow-y: auto;
67+
}
68+
.map-container {
69+
flex: 1;
70+
position: relative;
71+
}
72+
</style>

0 commit comments

Comments
 (0)